GNUのcpを使って大量のファイルをコピーしたところ、cpの設計上の問題で、極めてコピーが遅かったというお話。
My experience with using cp to copy a lot of files (432 millions, 39 TB)
よう。俺は最近、大量のファイルをコピーする必要があったんだが、UNIXは20年もやってきた俺の経験からも、cpの挙動には驚かされたし、俺の意見はコミュニティに共有されるべきだと思う。
環境:古いDellのサーバー(2コア、初期メモリ2GB、追加して10GB、Ubuntu Trusty)と、新しいDellのストレージ格納機(MD 1200)にある、12個の4TBディスクでRAID 6が設定してあって、全体で40TBの要領を持ち、二つのドライブが同時に失敗しても問題ない環境。サーバーは遠隔地バックアップに使われていて、ディスクへの書き込みしかしてない。ほとんどのファイルにrsnapshotをかけてるので、リンク数が多い(30+)
ある朝、俺はディスクが故障している通知を受け取った。まあ問題ない。たまにあることだ。Dellに電話して、次の日に交換ディスクを受け取った。リビルド中に、交換ディスクが故障した。そして、また別のディスクも故障した。さて、Dellのサポートは単に故障ディスクを交換するのは辞めるように支持してきた。というのも、全体があなぬけである可能性があるから。もちろん、俺も分かっていることだが、ディスクというものは十分に多くの不良ブロックがないと故障だと通知されない。運が悪ければ、異なるディスクの三つのブロックが短期間でやられたの可能性があり、RAIDコントローラーは失敗を検知して、パリティからデータを再計算して、どこか別の場所に記録するということができていないかもしれない。そういうわけで、ヤバイディスクは二つだけだが、データが失われた可能性はある。
容量もほぼ使いきっていたことだし、ストレージ格納機をもうひとつ注文して、古いのから新しいのにファイルをコピーして、古いのを直して、全体の容量を増やそうと考えた。普通ならば、ブロックレベルでコピー/ムーブするところだが(ddとかpvmove)、不良ブロックの疑いがあるので、ファイルレベルのコピーをすることにした。そうすることによって、どのファイルが不良ブロック上にあるのか分かるからだ。他人が大量のファイルをコピーする経験をネット上で検索した結果、cpは必要な処理をやってくれるだろうと結論した。ハードリンクを保つには、どのファイルがすでにコピーされたかを把握しなければならないからだ。俺はサーバーに8GBのRAMを増設し、スワップ領域ももっと多く取るように設定した。
新しい機材が届いたので、コピーを始めた。当初は、iotopで計測すると、300-400MB/sぐらいの良好な進み具合だった。しばらくすると、速度が極端に下がり始めた。ほとんどの時間は、ハードリンクを作成するのに費やされ、ファイルシステムが整合性の取れた状態であることを保証するのには時間がかかるからだ。俺はXFSを使っている。たぶん、write barrierを無効にするのを忘れていたためだろう。RAIDコントローラーには信頼できるバッテリーバックアップ付きのwrite cacheがあるので、無効にしても問題なかったはずだ。予想通り、cpのメモリ使用量が上がり始め、すぐにギガバイト単位に達した。
コピーを開始してから数日後、驚き第一段がやってきた。コピーが止まったのだ。straceによると、cpは一切のシステムコールを発行していなかった。ソースコードを読むと、cpは、どのファイルがコピーされたのかを、ハッシュテーブルで管理していて、頻繁な衝突を防ぐために、たまにサイズを増やしている。RAMを使い切った場合、これは極めて遅い操作となる。
ハッシュテーブルのリサイズはいずれ終わり、cpは処理を再開するだろうと考えた。しばらくすると、またコピーを始めた。何度か途中で泊まり、ハッシュテーブルのリサイズが行われた。それぞれ、かなりの時間がかかった。ついに、10日間ものコピーとハッシュテーブルのリサイズの末、新しいファイルシステムは、古い奴と同量のブロックとinodeを持ったことを、dfで確認した。しかし、驚いたことに、cpコマンドは終了しない。またソースを読むと、cpはハッシュテーブルのデータ構造を、コピーが終わった後でご丁寧にも破棄しようとしている(forget_all呼び出し)。この時点でcpプロセスの仮想メモリのサイズは17GB以上あり、サーバーには10GBのRAMしかないため、大量のスワップが発生する。
cpは"-v"オプション付きで実行されていて、出力(stdoutとstderr両方)はteeコマンドにパイプして(巨大な!)ログファイルに格納してある。どうやらどこかでcpの出力はバッファーされていて、ログファイルは行の途中で終わっている。バッファをフラッシュさせて完全なログファイルを得たいがために、cpがハッシュテーブルを破棄し終わるまで一日ほど待ったが、諦めてプロセスをkillした。
これを書いている今、両方のファイルシステムに対して"ls -laR"を実行していて、すべてコピーされたかどうかを確認中だ。ただ、cpの失われた出力の最後の部分にエラーが含まれていたのでもなければ、I/Oエラーを出したのはたったひとつのファイルだけだ(運良く、そのファイルには別のコピーがあった)
こういう状況はめったに起こるものではないが、cpは管理だけの処理で待ちぼうけを食らわせるのではなく、I/Oを待つ間に管理処理を行うべきではないか。それから、まともなメモリ管理が行われていない大昔のシステムをサポートする必要があるのでもなければ、cp.cの最後のforget_all呼び出しは削除すべきしても問題ないのではないか。
今回得た知見をまとめると。
ハードウェアとファイルシステムがまともであると信頼するのであれば、ファイルシステム全体をコピーするにはブロックレベルのコピーを行え。空き容量が大量にあるのでもなければ、その方が速い。また、メモリ使用量も少ない。
多くのファイルがあって、ハードリンクを保持したい場合、ファイルレベルのコピーを行う場合は、十分なメモリを用意しろ。
データ構造をご丁寧に破棄することは、単にプロセスを終了させて後始末を任せるより、相当に時間がかかる。
故障したハードディスクの数と、不良ブロックのあるハードディスクの数は、別物だ。RAID 6においても、運が悪ければ、3つのハードディスク不良を待たずして、データを失うこともある。RAID 5でも同じだ。一つのディスク不良か、ディスク不良なしでも、運が悪ければデータを失うことがある。
これが誰かの役に立つか、興味を引くといいな。
敬具など
Rasmus Borup Hansen
cpで大量のファイルをコピーする際の問題は、たまにハッシュテーブルをリサイズするが、その間にI/Oを一切行わないというのと、終了時にご丁寧にもハッシュテーブルで確保したメモリを破棄しようとするため、恐ろしいほど時間がかかるということだ。近代的なOSはメモリ管理をしっかりとしているので、今すぐ終了するのであれば、細切れにしたヒープの中身(しかも実メモリが足りないためスワップ発声)をわざわざ意味のない状態にしていく必要はない。
なお、MLでは、これを受けてcpを改良する動きが見られる。
ドワンゴ広告
この記事はドワンゴ勤務中に書かれた。
来週末はBoost勉強会大阪で発表する予定で、そのためにConcept Liteのドラフトを読み込んでいる。軽量コンセプトはある程度理解した。
ドワンゴは本物のC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0
gnuのcp.cを見ると・・・
ReplyDelete#ifdef lint
forget_all ();
#endif
return ok ? EXIT_SUCCESS : EXIT_FAILURE;
メモリリーク警告に対応したために遅くなった、ということかな・・
ツールの警告に盲目的に対応するというのは、DebianがValgrindの警告を消すためにOpenSSLに手を加えた結果、乱数が予測可能になってしまったという事件を思い出しますね。
ReplyDeleteやっぱり変だと思ったら、lintでチェックするときだけfreeするのか。うーん。なんだかテストのためのだけ有効にされるコードというのは本末転倒な気が。
ReplyDeleteそもそもlintは静的なチェックツールなので、動的メモリの管理については何も有益なチェックをしないという点で、これはlintシンボルの濫用だし、#if 0以上の意味がないですな。
ReplyDeletes/スワップ発声/スワップ発生
ReplyDeleteハッシュテーブルで管理しなくてもどこにハードリンクがあるかわかるようにファイルシステムの方を改良するというアプローチはないのでしょうか。不自由なOSの不自由なファイルシステムでさえその程度のことはやってるのに。
ReplyDeleteハードリンクの元を管理すると、メタデータが増え性能が落ちる、fsckも時間が長くなる、cpのメモリ削減には効果なさそう。
ReplyDelete終了時に丁寧にメモリ解放しない方法はstd::だとどうすればいいんだろう。
>終了時に丁寧にメモリ解放しない方法はstd::だとどうすればいいんだろう。
ReplyDeletenew std::map()してdeleteしなければええんやで
> 終了時に丁寧にメモリ解放しない方法はstd::だとどうすればいいんだろう。
ReplyDeleteC++11的には、std::_Exitやstd::quick_exitでしょう。自動変数もグローバル変数もデストラクタを実行せず終わります。