![Bashスクリプトと大容量ファイル(バグ):リダイレクトに組み込まれた読み取り機能を使用して入力すると、予期しない結果が発生する](https://linux33.com/image/21310/Bash%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%A8%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%EF%BC%88%E3%83%90%E3%82%B0%EF%BC%89%EF%BC%9A%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E3%81%AB%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BE%E3%82%8C%E3%81%9F%E8%AA%AD%E3%81%BF%E5%8F%96%E3%82%8A%E6%A9%9F%E8%83%BD%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E5%85%A5%E5%8A%9B%E3%81%99%E3%82%8B%E3%81%A8%E3%80%81%E4%BA%88%E6%9C%9F%E3%81%97%E3%81%AA%E3%81%84%E7%B5%90%E6%9E%9C%E3%81%8C%E7%99%BA%E7%94%9F%E3%81%99%E3%82%8B.png)
私は大容量ファイルを扱っています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
次の結果を提供します(データを非公開にするためにesと入力し、x
他の文字は本当です)。注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
4474604
uniq countの後、初期スクリプトが失敗したことがわかります。
bashで私が認識しない限界に達しましたか? bashでバグを見つけましたか(可能性が低い)。それとも何か間違っていますか?
修正する:
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にバグが届きました。影響を受けるバージョンはbash
6.0.4のDebian Squeeze 6.0.2と4.1.5です。
echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu
アップデート3:
私のバグレポートに迅速に対応したAndreas Schwabに感謝します。このパッチはこれらの不適切な動作を解決します。影響を受けるファイルは次のとおりです。lib/sh/zread.c
Gilesが先に指摘したように:
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
。 as はlseek
ファイルの先頭からオフセットを返します。オフセットは2 GBを超えると負であるため、成功する必要がある場所int
でテストが失敗します。if (r >= 0)
答え1
Bashで何らかのバグを見つけました。これは既知のバグであり、修正されました。
プログラムは、ファイルのオフセットを有限サイズの整数型変数として表します。以前はint
ほとんどすべての人が使用していましたが、int
その種類が符号ビットを含めて32ビットに制限されていたため、-2147483648から2147483647までの値を格納できました。今は違うさまざまなアイテムの名前を入力してください。、off_t
ファイルのオフセットを含みます。
デフォルトでは、off_t
32ビットプラットフォームでは32ビットタイプ(最大許容2GB)、64ビットプラットフォームでは64ビットタイプ(最大許容8EB)です。ただし、型をoff_t
64ビット幅に切り替えてプログラムに適切な関数実装を呼び出すようにするLARGEFILEオプションを使用してプログラムをコンパイルするのが一般的です。lseek
。
32ビットプラットフォームでbashを実行していて、bashバイナリが大容量ファイルサポートでコンパイルされていないようです。通常のファイルから1行を読み取ると、bashは内部バッファを使用して文字を一括して読み取ってパフォーマンスを向上させます。 (詳細はソースコード参照)builtins/read.def
)。行が完了すると、bash呼び出しはlseek
他のプログラムがファイルの場所に興味を持っている場合に備えて、ファイルオフセットを行末に巻き戻します。関数lseek
で呼び出しが発生します。zsyncfc
lib/sh/zread.c
。
ソースコードを詳しく読むことはできませんでしたが、絶対オフセットが負数のときに切り替え点で何かスムーズに起こらないのではないかと推測しています。したがって、bashが2 GBの表示を通過した後にバッファを再充填すると、最終的に誤ったオフセットが読み取られます。
私の結論が間違っていて、あなたのbashが実際に64ビットプラットフォームで実行されているか、大容量ファイルサポートでコンパイルされている場合、これは間違いなくバグです。この事実をディストリビューションに報告するか、上流。
とにかく、シェルはこれらの大容量ファイルを処理するのに適したツールではありません。非常に遅いでしょう。可能であれば sed を使用し、そうでない場合は awk を使用します。
答え2
何が問題なのかわかりませんが、本当に複雑です。入力ラインが次の場合:
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
、ログファイルと一致する行を作成し、標準出力でタイムスタンプで始まらない行を示します。
答え3
あなたがしたいことは次のとおりです。
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
開かれたファイルテーブルがいっぱいになるのを防ぎます。