Bash scripting và các tệp lớn (bug): đầu vào với phần dựng sẵn đọc từ một chuyển hướng cho kết quả không mong muốn


16

Tôi có một vấn đề lạ với các tập tin lớn và bash. Đây là bối cảnh:

  • Tôi có một tệp lớn: 75G và hơn 400.000.000 dòng (đó là tệp nhật ký, xấu của tôi, tôi để nó phát triển).
  • 10 ký tự đầu tiên của mỗi dòng là dấu thời gian ở định dạng YYYY-MM-DD.
  • Tôi muốn chia tập tin đó: một tập tin mỗi ngày.

Tôi đã thử với đoạn script sau không hoạt động. Câu hỏi của tôi là về kịch bản này không hoạt động, không phải giải pháp thay thế .

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

Sau khi gỡ lỗi, tôi tìm thấy vấn đề trong new_filebiến. Kịch bản này:

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

đưa ra kết quả dưới đây (Tôi đặt xes để giữ bí mật dữ liệu, các ký tự khác là dữ liệu thực). Lưu ý dhvà các chuỗi ngắn hơn:

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

Nó không phải là một vấn đề trong định dạng của tập tin của tôi . Kịch bản cut -c 1-10 file.log | uniq -cchỉ cung cấp tem thời gian hợp lệ. Thật thú vị, một phần của đầu ra trên trở thành cut ... | uniq -c:

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

Chúng ta có thể thấy rằng sau khi đếm uniq 4474604, kịch bản ban đầu của tôi đã thất bại.

Tôi đã đạt đến một giới hạn trong bash mà tôi không biết, tôi đã tìm thấy một lỗi trong bash (nó không có khả năng), hoặc tôi đã làm gì sai?

Cập nhật :

Vấn đề xảy ra sau khi đọc 2G của tập tin. Nó đường nối readvà chuyển hướng không thích các tệp lớn hơn 2G. Nhưng vẫn đang tìm kiếm một lời giải thích chính xác hơn.

Cập nhật2 :

Nó chắc chắn trông giống như một lỗi. Nó có thể được sao chép với:

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

nhưng điều này hoạt động tốt như một cách giải quyết (nó là một cách sử dụng hữu ích cat):

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

Một lỗi đã được gửi đến GNU và Debian. Các phiên bản bị ảnh hưởng là bash4.1.5 trên Debian Squeeze 6.0.2 và 6.0.4.

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

Cập nhật 3:

Nhờ Andreas Schwab, người đã phản ứng nhanh chóng với báo cáo lỗi của tôi, đây là bản vá là giải pháp cho hành vi sai trái này. Các tập tin bị ảnh hưởng là lib/sh/zread.cnhư Gilles đã chỉ ra sớm hơn:

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;

Các rbiến được sử dụng để giữ giá trị trả lại lseek. Vì lseektrả về phần bù từ phần đầu của tệp, khi nó có dung lượng trên 2 GB, intgiá trị âm, điều này khiến cho bài kiểm tra if (r >= 0)thất bại ở nơi đáng lẽ phải thành công.


1
Bạn có thể tái tạo vấn đề với các bộ dữ liệu đầu vào nhỏ hơn không? Có phải nó luôn luôn là cùng một dòng đầu vào dẫn đến những vấn đề này?
larsks

@larks: câu hỏi hay. Vấn đề luôn bắt đầu ở dòng # 13,520.918 (thực tế hai lần đối với các bài kiểm tra tôi đã làm). Kích thước của tệp trước dòng này là 2.147.487.726. Nó có một giới hạn 32 bit ở đây, nhưng không chính xác như chúng ta chỉ hơn 2 ^ 31 (2.147.483.648), nhưng ngay ở giới hạn bộ đệm 4K (2 ^ 31 + 4K = 2.147.487.744). Các dòng trước và tiếp theo là 100 đến 200 ký tự bình thường.
jfg956

Đã thử nghiệm trên tệp thứ 2 (có cùng kích thước): sự cố bắt đầu ở dòng # 13,522,712 và tệp có kích thước 2.147.498.679 byte trước dòng đó. Nó nối các điểm theo hướng giới hạn của readcâu lệnh trong bash.
jfg956

Câu trả lời:


13

Bạn đã tìm thấy một lỗi trong bash, các loại. Đó là một lỗi đã biết với một sửa chữa đã biết.

Các chương trình biểu thị một phần bù trong một tệp dưới dạng một biến trong một số kiểu số nguyên có kích thước hữu hạn. Trong những ngày cũ, tất cả mọi người sử dụng intcho chỉ là về tất cả mọi thứ, và các intloại bị hạn chế đến 32 bit, bao gồm cả bit dấu, vì vậy nó có thể lưu trữ các giá trị từ -2147483648 đến 2147483647. Ngày nay có khác nhau tên kiểu cho những thứ khác nhau , trong đó có off_tmột bù vào một tập tin.

Theo mặc định, off_tlà loại 32 bit trên nền tảng 32 bit (cho phép tối đa 2 GB) và loại 64 bit trên nền tảng 64 bit (cho phép tối đa 8EB). Tuy nhiên, việc biên dịch các chương trình với tùy chọn LARGEFILE là phổ biến, giúp chuyển loại off_tthành rộng 64 bit và làm cho chương trình gọi các triển khai phù hợp của các chức năng như lseek.

Có vẻ như bạn đang chạy bash trên nền tảng 32 bit và tệp nhị phân bash của bạn không được biên dịch với sự hỗ trợ tệp lớn. Bây giờ, khi bạn đọc một dòng từ một tệp thông thường, bash sử dụng bộ đệm bên trong để đọc các ký tự theo lô để thực hiện (để biết thêm chi tiết, xem nguồn trong builtins/read.def). Khi dòng hoàn thành, bash gọi lseekđể tua lại tệp bù vào vị trí cuối dòng, trong trường hợp một số chương trình khác quan tâm đến vị trí trong tệp đó. Các cuộc gọi để lseekxảy ra trong zsyncfcchức năng trong lib/sh/zread.c.

Tôi đã không đọc nguồn chi tiết nhiều, nhưng tôi cho rằng có điều gì đó không diễn ra suôn sẻ tại thời điểm chuyển đổi khi độ lệch tuyệt đối là âm. Vì vậy, bash kết thúc việc đọc sai giá trị khi nó nạp lại bộ đệm của nó, sau khi nó vượt qua mốc 2GB.

Nếu kết luận của tôi là sai và trên thực tế, bash của bạn đang chạy trên nền tảng 64 bit hoặc được biên dịch với sự hỗ trợ lớn, đó chắc chắn là một lỗi. Vui lòng báo cáo để phân phối hoặc ngược dòng của bạn .

Shell không phải là công cụ phù hợp để xử lý các tệp lớn như vậy. Nó sẽ chậm thôi. Sử dụng sed nếu có thể, nếu không awk.


1
Merci Gilles. Câu trả lời tuyệt vời: đầy đủ, có đủ thông tin để hiểu vấn đề ngay cả với những người không có nền tảng CS mạnh mẽ (32 bit ...). (larsks cũng giúp đặt câu hỏi về số dòng và cần phải thừa nhận.) Sau đó, tôi cũng gặp vấn đề 32 bit và tải xuống nguồn, nhưng chưa đến mức phân tích này. Merci encore, et bonne journée.
jfg956

4

Tôi không biết về sai, nhưng nó chắc chắn đã bị xáo trộn. Nếu dòng đầu vào của bạn trông như thế này:

YYYY-MM-DD some text ...

Sau đó, thực sự không có lý do cho việc này:

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

Bạn đang thực hiện rất nhiều chuỗi con để kết thúc với một cái gì đó trông ... chính xác như cách nó đã nhìn trong tệp. Còn cái này thì sao?

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

Điều đó chỉ cần lấy 10 ký tự đầu tiên từ dòng. Bạn cũng có thể phân phối với bashtoàn bộ và chỉ sử dụng awk:

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

Cái này lấy ngày trong $1(cột được phân tách bằng khoảng trắng đầu tiên trong mỗi dòng) và sử dụng nó để tạo tên tệp.

Lưu ý rằng có thể có một số dòng nhật ký không có thật trong các tệp của bạn. Đó là, vấn đề có thể là với đầu vào, không phải kịch bản của bạn. Bạn có thể mở rộng awktập lệnh để gắn cờ các dòng không có thật như thế này:

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

Dòng ghi này khớp YYYY-MM-DDvới tệp nhật ký của bạn và gắn cờ các dòng không bắt đầu bằng dấu thời gian trên thiết bị xuất chuẩn.


Không có dòng không có thật trong tập tin của tôi: cut -c 1-10 file.log | uniq -cmang lại cho tôi kết quả mong đợi. Tôi đang sử dụng ${line:0:4}-${line:5:2}-${line:8:2}vì tôi sẽ đặt tệp vào một thư mục ${line:0:4}/${line:5:2}/${line:8:2}và tôi đã đơn giản hóa vấn đề (tôi sẽ cập nhật báo cáo vấn đề). Tôi biết awkcó thể giúp tôi ở đây, nhưng tôi gặp vấn đề khác khi sử dụng nó. Điều tôi muốn là hiểu vấn đề với bash, không tìm giải pháp thay thế.
jfg956

Như bạn đã nói ... nếu bạn "đơn giản hóa" vấn đề trong câu hỏi, có lẽ bạn sẽ không nhận được câu trả lời mình muốn. Tôi vẫn nghĩ rằng giải quyết vấn đề này bằng bash không thực sự là cách đúng đắn để xử lý loại dữ liệu này, nhưng không có lý do gì nó không hoạt động.
larsks

Vấn đề đơn giản hóa mang lại kết quả bất ngờ mà tôi đã trình bày trong câu hỏi, vì vậy tôi không nghĩ rằng đó là một sự đơn giản hóa. Hơn nữa, vấn đề đơn giản hóa cho kết quả tương tự như cutcâu lệnh hoạt động. Vì tôi muốn so sánh táo với táo chứ không phải cam, tôi cần làm mọi thứ giống nhau nhất có thể.
jfg956

1
Tôi đã để lại cho bạn một câu hỏi có thể giúp tìm ra nơi mọi thứ đang diễn ra ...
larsks

2

Âm thanh như những gì bạn muốn làm là:

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

Việc closegiữ cho bảng tập tin mở không đầy.


Cảm ơn giải pháp awk. Tôi đã đi kèm với một cái gì đó tương tự. Câu hỏi của tôi là để hiểu giới hạn bash, không tìm thấy một giải pháp thay thế.
jfg956
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.