Bash scripting และไฟล์ขนาดใหญ่ (ข้อผิดพลาด): อินพุตพร้อม read builtin จากการเปลี่ยนเส้นทางให้ผลลัพธ์ที่ไม่คาดคิด


16

ฉันมีปัญหาแปลก ๆ bashกับไฟล์ขนาดใหญ่และ นี่คือบริบท:

  • ฉันมีไฟล์ขนาดใหญ่: 75G และ 400,000,000 บรรทัดขึ้นไป (เป็นไฟล์บันทึกที่ไม่ดีฉันปล่อยให้มันเติบโต)
  • 10 ตัวอักษรแรกของแต่ละบรรทัดคือการประทับเวลาในรูปแบบ YYYY-MM-DD
  • ฉันต้องการแยกไฟล์: หนึ่งไฟล์ต่อวัน

ฉันลองใช้สคริปต์ต่อไปนี้ซึ่งใช้งานไม่ได้ คำถามของฉันเป็นเรื่องเกี่ยวกับสคริปต์นี้ไม่ได้ทำงานไม่ได้แก้ปัญหาทางเลือก

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

หลังจากตรวจแก้จุดบกพร่องฉันพบปัญหาในnew_fileตัวแปร สคริปต์นี้:

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

ให้ผลการร้อง (ฉันใส่xes เพื่อเก็บข้อมูลเป็นความลับตัวอักษรอื่น ๆ เป็นของจริง) สังเกตdhและสตริงที่สั้นกว่า:

...
  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    
...

มันไม่ได้เป็นปัญหาที่เกิดขึ้นในรูปแบบของไฟล์ของฉัน สคริปต์cut -c 1-10 file.log | uniq -cให้การประทับเวลาที่ถูกต้องเท่านั้น น่าสนใจส่วนหนึ่งของผลลัพธ์ข้างต้นกลายเป็นcut ... | uniq -c:

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

เราจะเห็นว่าหลังจากการนับ uniq 4474604สคริปต์เริ่มต้นของฉันล้มเหลว

ฉันตีขีด จำกัด ด้วยการทุบตีที่ฉันไม่รู้หรือไม่ฉันพบข้อผิดพลาดในการทุบตี (มันไม่น่าจะเกิดตะเข็บ) หรือฉันทำสิ่งผิดปกติหรือไม่?

อัปเดต :

ปัญหาเกิดขึ้นหลังจากอ่านไฟล์ 2G มันตะเข็บreadและการเปลี่ยนเส้นทางไม่ชอบไฟล์ขนาดใหญ่กว่า 2G แต่ยังคงค้นหาคำอธิบายที่แม่นยำยิ่งขึ้น

อัปเดต 2 :

ดูเหมือนว่าจะเป็นข้อบกพร่อง สามารถทำซ้ำได้ด้วย:

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

แต่มันใช้งานได้ดีเป็นวิธีแก้ปัญหา (มันตะเข็บที่ฉันพบการใช้ประโยชน์cat):

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

มีการยื่นบั๊กกับ GNU และ Debian เวอร์ชันที่ได้รับผลกระทบคือbash4.1.5 สำหรับ Debian Squeeze 6.0.2 และ 6.0.4

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

Update3:

ขอบคุณ Andreas Schwab ที่ตอบสนองอย่างรวดเร็วต่อรายงานข้อผิดพลาดของฉันนี่คือแพทช์ที่เป็นวิธีแก้ปัญหาความไม่เหมาะสมนี้ ไฟล์ที่ได้รับผลกระทบlib/sh/zread.cดังที่ Gilles ชี้ให้เห็นเร็วกว่านี้:

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;

ตัวแปรใช้ในการเก็บค่าตอบแทนของr lseekในฐานะที่เป็นlseekผลตอบแทนชดเชยจากจุดเริ่มต้นของไฟล์เมื่อมันเป็นมากกว่า 2GB ที่intคุ้มค่าเป็นลบซึ่งเป็นสาเหตุของการทดสอบif (r >= 0)ที่จะล้มเหลวที่มันควรจะมีประสบความสำเร็จ


1
คุณสามารถจำลองปัญหาด้วยชุดข้อมูลที่เล็กลงได้หรือไม่? มันเป็นสายอินพุตเดียวกันที่ทำให้เกิดปัญหาเหล่านี้หรือไม่?
larsks

@larks: คำถามที่ดี ปัญหาเริ่มต้นที่บรรทัด # 13.520.918 (จริง ๆ แล้วสองเท่าสำหรับการทดสอบที่ฉันทำ) ขนาดของไฟล์ก่อนหน้าบรรทัดนี้คือ 2.147.487.726 มันเห็นว่ามีการ จำกัด 32 บิตที่นี่ แต่ไม่ตรงกับที่เรามีมากกว่า 2 ^ 31 เล็กน้อย (2.147.483.648) แต่อยู่ที่ขีด จำกัด บัฟเฟอร์ 4K (2 ^ 31 + 4K = 2.147.487.744) บรรทัดก่อนหน้าและถัดไปเป็นบรรทัดปกติ 100 ถึง 200 อักขระ
jfg956

ทดสอบในไฟล์ที่ 2 (ขนาดใกล้เคียงกัน): ปัญหาเริ่มต้นที่บรรทัด # 13.522.712 และไฟล์คือ 2.147.498.679 ไบต์ขนาดใหญ่ก่อนบรรทัดนั้น มันตะเข็บเพื่อชี้ไปในทิศทางของข้อ จำกัด ของreadคำสั่งในการทุบตี
jfg956

คำตอบ:


13

คุณพบข้อผิดพลาดในการทุบตีแปลก ๆ เป็นข้อผิดพลาดที่รู้จักพร้อมการแก้ไขที่รู้จัก

โปรแกรมแสดงออฟเซ็ตในไฟล์เป็นตัวแปรในรูปแบบจำนวนเต็มบางชนิดที่มีขนาด จำกัด ในวันเก่าทุกคนที่ใช้intเพียงเกี่ยวกับทุกอย่างและintประเภทถูก จำกัด ที่ 32 บิตรวมทั้งบิตเครื่องหมายจึงสามารถเก็บค่าจาก -2147483648 เพื่อ 2147483647. ปัจจุบันมีความแตกต่างกันชื่อประเภทสำหรับสิ่งที่แตกต่างกันรวมทั้งoff_tการ ชดเชยในไฟล์

โดยค่าเริ่มต้นoff_tเป็นประเภท 32 บิตบนแพลตฟอร์ม 32 บิต (อนุญาตสูงสุด 2GB) และประเภท 64 บิตบนแพลตฟอร์ม 64 บิต (อนุญาตสูงสุด 8EB) แต่ก็เป็นธรรมดาที่จะรวบรวมโปรแกรมที่มีตัวเลือก largefile ซึ่งสวิตช์ชนิดoff_tที่จะเป็น 64 lseekบิตกว้างและทำให้การเรียกโปรแกรมการใช้งานที่เหมาะสมของการทำงานเช่น

ดูเหมือนว่าคุณกำลังรันทุบตีบนแพลตฟอร์ม 32 บิตและไบนารีทุบตีของคุณไม่ได้รับการคอมไพล์ด้วยการรองรับไฟล์ขนาดใหญ่ ตอนนี้เมื่อคุณอ่านบรรทัดจากไฟล์ปกติ bash ใช้บัฟเฟอร์ภายในเพื่ออ่านอักขระเป็นชุดสำหรับประสิทธิภาพ (สำหรับรายละเอียดเพิ่มเติมให้ดูที่แหล่งที่มาbuiltins/read.def) เมื่อสายเสร็จสมบูรณ์แล้วการเรียกใช้ bash lseekเพื่อกรอกลับไฟล์ย้อนกลับไปยังตำแหน่งของจุดสิ้นสุดของบรรทัดในกรณีที่โปรแกรมอื่น ๆ สนใจเกี่ยวกับตำแหน่งในไฟล์นั้น การเรียกlseekเกิดขึ้นในzsyncfcฟังก์ชั่นlib/sh/zread.cค่ะ

ฉันไม่ได้อ่านแหล่งที่มาอย่างละเอียด แต่ฉันคิดว่ามีบางอย่างไม่ราบรื่นเมื่อถึงจุดเปลี่ยนเมื่อค่าออฟเซ็ตสัมบูรณ์เป็นค่าลบ ดังนั้น bash จะจบลงด้วยการอ่านค่าออฟเซ็ตผิดเมื่อเติมบัฟเฟอร์หลังจากผ่านเครื่องหมาย 2GB

หากข้อสรุปของฉันผิดและ bash ของคุณกำลังใช้งานจริงบนแพลตฟอร์ม 64 บิตหรือคอมไพล์ด้วยการรองรับไฟล์ขนาดใหญ่นั่นเป็นข้อผิดพลาดอย่างแน่นอน กรุณารายงานไปยังการกระจายหรือต้นน้ำของคุณ

เชลล์ไม่ใช่เครื่องมือที่เหมาะสมในการประมวลผลไฟล์ขนาดใหญ่เช่นนี้ มันจะช้า ใช้ sed ถ้าเป็นไปได้มิฉะนั้น awk


1
Merci Gilles คำตอบที่ยอดเยี่ยม: สมบูรณ์พร้อมข้อมูลเพียงพอที่จะเข้าใจปัญหาแม้แต่กับคนที่ไม่มีพื้นหลัง CS ที่แข็งแกร่ง (32 บิต ... ) (larsks ช่วยในการตั้งคำถามเกี่ยวกับหมายเลขบรรทัดและควรได้รับการยอมรับ) หลังจากนั้นฉันก็มีปัญหา 32 บิตและดาวน์โหลดซอร์ส แต่ยังไม่ถึงระดับการวิเคราะห์นี้ อีกครั้ง Merci และ Bonne journée
jfg956

4

ฉันไม่รู้ว่าผิด แต่มันก็ซับซ้อนแน่นอน หากบรรทัดอินพุตของคุณมีลักษณะดังนี้:

YYYY-MM-DD some text ...

ถ้าอย่างนั้นก็ไม่มีเหตุผลอะไร:

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

คุณกำลังทำซับสตริงจำนวนมากเพื่อจบลงด้วยบางสิ่งที่ดูเหมือน ... อย่างที่มันมีอยู่แล้วในไฟล์ แล้วเรื่องนี้ล่ะ

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

นั่นแค่คว้า 10 ตัวอักษรแรกจากบรรทัด คุณสามารถแจกจ่ายด้วยbashทั้งหมดและเพียงแค่ใช้awk :

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

สิ่งนี้จะจับวันที่ใน $1 (คอลัมน์แรกที่คั่นด้วยช่องว่างในแต่ละบรรทัด) และใช้เพื่อสร้างชื่อไฟล์

โปรดทราบว่าเป็นไปได้ว่ามีไฟล์บันทึกปลอมในไฟล์ของคุณ นั่นคือปัญหาอาจเกิดจากการป้อนข้อมูลไม่ใช่สคริปต์ของคุณ คุณสามารถขยายawkสคริปต์เพื่อตั้งค่าสถานะบรรทัดปลอมเช่นนี้:

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
}
'

สิ่งนี้จะเขียนบรรทัดที่ตรงYYYY-MM-DDกับไฟล์บันทึกของคุณและตั้งค่าสถานะบรรทัดที่ไม่เริ่มต้นด้วยการประทับเวลาบน stdout


ไม่มีสายปลอมในไฟล์ของฉัน: cut -c 1-10 file.log | uniq -cให้ผลลัพธ์ที่คาดหวัง ฉันกำลังใช้${line:0:4}-${line:5:2}-${line:8:2}เพราะฉันจะวางไฟล์ในไดเรกทอรี${line:0:4}/${line:5:2}/${line:8:2}และฉันทำให้ปัญหาง่ายขึ้น (ฉันจะอัปเดตข้อความแจ้งปัญหา) ฉันรู้ว่าawkสามารถช่วยฉันได้ที่นี่ แต่ฉันประสบปัญหาอื่น ๆ เกี่ยวกับการใช้งาน สิ่งที่ฉันต้องการคือเข้าใจปัญหาด้วยbashไม่พบทางเลือกอื่น
jfg956

ดังที่คุณกล่าว ... หากคุณ "ทำให้" ปัญหาในคำถามง่ายขึ้นคุณอาจไม่ได้รับคำตอบที่คุณต้องการ ฉันยังคงคิดว่าการแก้ปัญหาด้วย bash ไม่ใช่วิธีที่เหมาะสมในการประมวลผลข้อมูลประเภทนี้ แต่ไม่มีเหตุผลที่จะไม่ทำงาน
larsks

ปัญหาที่ทำให้เข้าใจง่ายให้ผลลัพธ์ที่ไม่คาดคิดที่ฉันนำเสนอในคำถามดังนั้นฉันจึงไม่คิดว่ามันเป็นเรื่องธรรมดามากเกินไป นอกจากนี้ปัญหาที่ทำให้เข้าใจง่ายจะให้ผลลัพธ์ที่คล้ายกันกับcutข้อความที่ใช้งานได้ เมื่อฉันต้องการเปรียบเทียบแอปเปิ้ลกับแอปเปิ้ลไม่ใช่กับส้มฉันจึงต้องทำสิ่งต่าง ๆ ให้คล้ายกันมากที่สุด
jfg956

1
ผมออกจากคุณคำถามที่อาจช่วยให้คิดออกว่าสิ่งที่จะไปเป๋ที่ ...
larsks

2

ดูเหมือนสิ่งที่คุณต้องการทำคือ:

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

การcloseป้องกันไม่ให้ตารางไฟล์เปิดเติม


ขอบคุณสำหรับการแก้ปัญหา awk ฉันมาพร้อมกับสิ่งที่คล้ายกัน คำถามของฉันคือเข้าใจข้อ จำกัด ของ bash ไม่ใช่หาทางเลือกอื่น
jfg956
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.