Bashスクリプトおよび大きなファイル(バグ):リダイレクトからの読み取り組み込みを使用した入力は、予期しない結果をもたらします


16

大きなファイルとbash。これはコンテキストです:

  • 私は大きなファイルを持っています:75Gと400,000,000行(これはログファイルです、私の悪い、私はそれを成長させました)。
  • 各行の最初の10文字は、YYYY-MM-DD形式のタイムスタンプです。
  • そのファイルを分割したい:1日1ファイル。

動作しなかった次のスクリプトを試しました。 私の質問は、このスクリプトが機能しないことであり、代替ソリューションではありません

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

結果を以下に示します(xデータを機密に保つためにes を入れましたが、他のcharsは実際のものです)。に注意してください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 countの4474604後、最初のスクリプトが失敗したことがわかります。

私は知らないbashの制限に達しましたか、bashのバグを見つけましたか(継ぎ目はありません)、または何か間違ったことをしましたか?

更新

この問題は、ファイルの2Gを読み取った後に発生します。継ぎ目readとリダイレクトは2Gよりも大きなファイルを好みません。しかし、より正確な説明を探しています。

Update2

間違いなくバグのように見えます。以下で再現できます:

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に報告されました。影響を受けるバージョンはbash、Debian Squeeze 6.0.2および6.0.4上の4.1.5です。

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変数は、の戻り値を保持するために使用されますlseeklseekそれが2GBを超える場合、ファイルの先頭からのオフセットを返し、int値はテストが原因と否定、であるif (r >= 0)、それは成功していなければならない場所を失敗します。


1
入力データのより小さいセットで問題を再現できますか?これらの問題を引き起こすのは常に同じ入力行ですか?
ラースク

@larks:いい質問です。問題は常に行番号13.520.918で始まります(実際に行ったテストでは2回)。この行の前のファイルのサイズは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バイトの大きさです。readbash のステートメントの制限の方向を指すように継ぎ目があります。
jfg956

回答:


13

あなたは、bashにある種のバグを発見しました。これは、既知の修正を含む既知のバグです。

プログラムは、ファイル内のオフセットを、有限サイズの整数型の変数として表します。昔は、誰もが使用するintちょうど約すべてのために、そしてintそれが今日であり異なっている-2147483648から2147483647までの値を格納することができるように、符号ビットを含む、タイプは32ビットに限定されていた別のもののための型名は、含むoff_tためファイル内のオフセット。

デフォルトでoff_tは、32ビットプラットフォームでは32ビットタイプ(最大2GBまで)、64ビットプラットフォームでは64ビットタイプ(最大8EBまで)です。ただし、LARGEFILEオプションを使用してプログラムをコンパイルするのが一般的です。LARGEFILEオプションは、タイプoff_tを64ビット幅に切り替え、プログラムがなどの関数の適切な実装を呼び出すようにしlseekます。

32ビットプラットフォームでbashを実行しており、bashバイナリが大きなファイルをサポートするようにコンパイルされていないようです。現在、通常のファイルから行を読み取る場合、bashは内部バッファーを使用して、パフォーマンスのためにバッチで文字を読み取ります(詳細については、ソースを参照してくださいbuiltins/read.def)。行が完了すると、bashはlseek、他のプログラムがそのファイル内の位置を気にする場合に備えて、ファイルオフセットを行の終わりの位置に巻き戻すために呼び出します。への呼び出しlseekは、次のzsyncfc関数で発生しますlib/sh/zread.c

私はソースを詳しく読みませんでしたが、絶対オフセットが負の遷移点で何かがスムーズに行われていないと思います。そのため、bashは2GBマークを超えた後、バッファーを補充するときに間違ったオフセットで読み取りを行うことになります。

私の結論が間違っていて、bashが実際に64ビットプラットフォームで実行されているか、または大規模ファイルのサポート付きでコンパイルされている場合、それは間違いなくバグです。ディストリビューションまたはアップストリームに報告してください。

とにかく、シェルはそのような大きなファイルを処理するのに適したツールではありません。遅くなります。可能であればsedを使用し、そうでない場合はawkを使用します。


1
メルシー・ジル。偉大な答え:完全な、十分な情報で、強力なCSバックグラウンド(32ビット...)のない人でも問題を理解することができます。(ラスクは行番号についても質問するのに役立ちます。確認する必要があります。)その後、32ビットの問題についてもソースをダウンロードしましたが、このレベルの分析にはまだ達していませんでした。Merci encore、et bonnejourné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で解決することはこの種のデータを処理する正しい方法ではないと思いますが、うまくいかない理由はありません。
ラースク

単純化された問題は、私が質問で提示した予期しない結果をもたらすため、それが過度に単純化されているとは思わない。さらに、単純化された問題は、機能する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.