Script Bash dan file besar (bug): input dengan read builtin dari redirection memberikan hasil yang tidak terduga


16

Saya memiliki masalah aneh dengan file besar dan bash. Inilah konteksnya:

  • Saya memiliki file besar: 75G dan 400.000.000 + baris (ini adalah file log, salah saya, saya biarkan tumbuh).
  • 10 karakter pertama dari setiap baris adalah stempel waktu dalam format YYYY-MM-DD.
  • Saya ingin membagi file itu: satu file per hari.

Saya mencoba dengan skrip berikut yang tidak berfungsi. Pertanyaan saya adalah tentang skrip ini tidak berfungsi, bukan solusi alternatif .

while read line; do
  new_file=${line:0:10}_file.log
  echo "$line" >> $new_file
done < file.log

Setelah debugging, saya menemukan masalah dalam new_filevariabel. Skrip ini:

while read line; do
  new_file=${line:0:10}_file.log
  echo $new_file
done < file.log | uniq -c

memberikan hasilnya di bawah (saya meletakkan xes untuk menjaga data rahasia, karakter lain adalah yang asli). Perhatikan dhstring dan lebih pendek:

...
  27402 2011-xx-x4
  27262 2011-xx-x5
  22514 2011-xx-x6
  17908 2011-xx-x7
...
3227382 2011-xx-x9
4474604 2011-xx-x0
1557680 2011-xx-x1
      1 2011-xx-x2
      3 2011-xx-x1
...
     12 2011-xx-x1
      1 2011-xx-dh
      1 2011-xx-x1
      1 208--
      1 2011-xx-x1
      1 2011-xx-dh
      1 2011-xx-x1    
...

Ini bukan masalah dalam format file saya . Script cut -c 1-10 file.log | uniq -chanya memberikan perangko waktu yang valid. Menariknya, bagian dari output di atas menjadi dengan cut ... | uniq -c:

3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1

Kita dapat melihat bahwa setelah hitungan uniq 4474604, skrip awal saya gagal.

Apakah saya mencapai batas dalam bash yang saya tidak tahu, apakah saya menemukan bug di bash (sepertinya tidak mungkin), atau apakah saya melakukan sesuatu yang salah?

Perbarui :

Masalahnya terjadi setelah membaca 2G file. Jahitan readdan pengalihan tidak suka file yang lebih besar dari 2G. Namun masih mencari penjelasan yang lebih tepat.

Pembaruan2 :

Ini pasti terlihat seperti bug. Itu dapat direproduksi dengan:

yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c

tetapi ini berfungsi dengan baik sebagai solusi (sepertinya saya menemukan penggunaan yang bermanfaat cat):

cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c 

Bug telah diajukan ke GNU dan Debian. Versi yang terpengaruh adalah bash4.1.5 pada Debian Squeeze 6.0.2 dan 6.0.4.

echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu

Pembaruan3:

Terima kasih kepada Andreas Schwab yang bereaksi cepat terhadap laporan bug saya, ini adalah tambalan yang merupakan solusi untuk perilaku buruk ini. File yang terpengaruh adalah lib/sh/zread.cseperti yang Gilles tunjukkan lebih cepat:

diff --git a/lib/sh/zread.c b/lib/sh/zread.c index 0fd1199..3731a41 100644
--- a/lib/sh/zread.c
+++ b/lib/sh/zread.c @@ -161,7 +161,7 @@ zsyncfd (fd)
      int fd; {   off_t off;
-  int r;
+  off_t r;

  off = lused - lind;   r = 0;

The rvariabel digunakan untuk menyimpan nilai kembali dari lseek. Ketika lseekmengembalikan offset dari awal file, ketika lebih dari 2GB, intnilainya negatif, yang menyebabkan tes if (r >= 0)gagal di tempat yang seharusnya berhasil.


1
Bisakah Anda mereplikasi masalah dengan set data input yang lebih kecil? Apakah selalu jalur input yang sama yang menghasilkan masalah ini?
larsks

@larks: pertanyaan bagus. Masalahnya selalu dimulai pada baris # 13.520.918 (sebenarnya dua kali untuk tes yang saya lakukan). Ukuran file sebelum baris ini adalah 2.147.487.726. Kelihatannya ada batas 32 bit di sini, tetapi tidak persis seperti kita sedikit lebih dari 2 ^ 31 (2.147.483.648), tetapi tepat pada batas buffer 4K (2 ^ 31 + 4K = 2.147.487.744). Baris sebelumnya dan berikutnya adalah garis normal 100 hingga 200 karakter.
jfg956

Diuji pada file ke-2 (berukuran hampir sama): masalahnya dimulai pada baris # 13.522.712, dan file tersebut berukuran 2.147.498.679 byte sebelum baris itu. Tampaknya menunjuk ke arah batas readpernyataan dalam bash.
jfg956

Jawaban:


13

Anda menemukan bug di bash, atau sejenisnya. Ini adalah bug yang dikenal dengan perbaikan yang dikenal.

Program mewakili offset dalam file sebagai variabel dalam beberapa tipe integer dengan ukuran terbatas. Di masa lalu, semua orang menggunakan inthampir semuanya, dan intjenisnya terbatas pada 32 bit, termasuk bit tanda, sehingga dapat menyimpan nilai dari -2147483648 hingga 2147483647. Saat ini ada berbagai jenis nama untuk berbagai hal , termasuk off_tuntuk offset dalam suatu file.

Secara default, off_tadalah tipe 32-bit pada platform 32-bit (memungkinkan hingga 2GB), dan tipe 64-bit pada platform 64-bit (memungkinkan hingga 8EB). Namun, itu umum untuk mengkompilasi program dengan opsi LARGEFILE, yang mengubah jenis off_tmenjadi 64 bit lebar dan membuat panggilan program implementasi yang sesuai dari fungsi seperti lseek.

Tampaknya Anda menjalankan bash pada platform 32-bit dan biner bash Anda tidak dikompilasi dengan dukungan file besar. Sekarang, ketika Anda membaca baris dari file biasa, bash menggunakan buffer internal untuk membaca karakter dalam batch untuk kinerja (untuk lebih jelasnya, lihat sumber di builtins/read.def). Ketika saluran selesai, bash panggilan lseekuntuk memundurkan file mengimbangi kembali ke posisi akhir baris, jika ada program lain yang peduli tentang posisi dalam file itu. Panggilan untuk lseekterjadi dalam zsyncfcfungsi di lib/sh/zread.c.

Saya belum membaca sumbernya secara mendetail, tetapi saya menduga bahwa sesuatu tidak terjadi dengan lancar pada titik transisi ketika offset absolut negatif. Jadi bash akhirnya membaca di offset yang salah saat mengisi ulang buffernya, setelah melewati tanda 2GB.

Jika kesimpulan saya salah dan bash Anda sebenarnya berjalan pada platform 64-bit atau dikompilasi dengan dukungan bigfile, itu pasti bug. Silakan laporkan ke distribusi Anda atau ke hulu .

Shell bukanlah alat yang tepat untuk memproses file besar seperti itu. Ini akan lambat. Gunakan sed jika mungkin, jika tidak awk.


1
Merci Gilles. Jawaban bagus: lengkap, dengan informasi yang cukup untuk memahami masalah ini bahkan untuk orang-orang tanpa latar belakang CS yang kuat (32 bit ...). (larsks juga membantu mempertanyakan nomor baris, dan harus diakui.) Setelah itu, saya juga mengalami masalah 32 bit dan mengunduh sumbernya, tetapi belum sampai pada tingkat analisis ini. Merci ulangan, dan bonne journée.
jfg956

4

Saya tidak tahu tentang yang salah, tapi itu jelas berbelit-belit. Jika jalur input Anda terlihat seperti ini:

YYYY-MM-DD some text ...

Maka benar-benar tidak ada alasan untuk ini:

new_file=${line:0:4}-${line:5:2}-${line:8:2}_file.log

Anda melakukan banyak pekerjaan substring untuk menghasilkan sesuatu yang terlihat ... persis seperti yang terlihat dalam file. Bagaimana dengan ini?

while read line; do
  new_file="${line:0:10}_file.log"
  echo "$line" >> $new_file
done

Itu hanya mengambil 10 karakter pertama dari garis. Anda juga bisa membuang bashsepenuhnya dan cukup gunakan awk:

awk '{print > ($1 "_file.log")}' < file.log

Ini mengambil tanggal di $1(kolom spasi-dibatasi pertama di setiap baris) dan menggunakannya untuk menghasilkan nama file.

Perhatikan bahwa ada kemungkinan bahwa ada beberapa garis log palsu di file Anda. Artinya, masalahnya mungkin dengan input, bukan skrip Anda. Anda dapat memperluas awkskrip untuk menandai garis palsu seperti ini:

awk '
$1 ~ /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ {
    print > ($1 "_file.log")
    next
}

{
    print "INVALID:", $0
}
'

Ini menulis baris yang cocok YYYY-MM-DDdengan file log Anda, dan menandai baris yang tidak dimulai dengan stempel waktu di stdout.


Tidak ada baris palsu di file saya: cut -c 1-10 file.log | uniq -cmemberi saya hasil yang diharapkan. Saya menggunakan ${line:0:4}-${line:5:2}-${line:8:2}karena saya akan meletakkan file dalam direktori ${line:0:4}/${line:5:2}/${line:8:2}, dan saya menyederhanakan masalah (saya akan memperbarui pernyataan masalah). Saya tahu awkdapat membantu saya di sini, tetapi saya mengalami masalah lain dalam menggunakannya. Yang saya inginkan adalah memahami masalahnya bash, bukan mencari solusi alternatif.
jfg956

Seperti yang Anda katakan ... jika Anda "menyederhanakan" masalah dalam pertanyaan, Anda mungkin tidak akan mendapatkan jawaban yang Anda inginkan. Saya masih berpikir bahwa menyelesaikan ini dengan bash sebenarnya bukan cara yang tepat untuk memproses data semacam ini, tetapi tidak ada alasan mengapa hal itu tidak berhasil.
larsks

Masalah yang disederhanakan memberikan hasil yang tidak terduga yang saya sajikan dalam pertanyaan, jadi saya tidak berpikir bahwa ini adalah penyederhanaan yang berlebihan. Selain itu, masalah yang disederhanakan memberikan hasil yang sama dengan cutpernyataan yang berfungsi. Karena saya ingin membandingkan apel dengan apel, bukan dengan jeruk, saya harus membuat hal-hal yang sama mungkin.
jfg956

1
Saya meninggalkan Anda sebuah pertanyaan yang mungkin membantu mencari tahu di mana segala sesuatunya serba salah ...
larsks

2

Kedengarannya seperti apa yang ingin Anda lakukan adalah:

awk '
{  filename = substr($0, 0, 10) "_file.log";  # input format same as output format
   if (filename != lastfile) {
       close(lastfile);
       print 'finished writing to', lastfile;
   }
   print >> filename;
   lastfile=filename;
}' file.log

Itu closemembuat tabel file terbuka dari mengisi.


Terima kasih atas solusinya. Saya sudah datang dengan sesuatu yang serupa. Pertanyaan saya adalah untuk memahami batasan bash, bukan untuk menemukan solusi alternatif.
jfg956
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.