株式会社PR TIMES 執行役員CTOの@catatsuyこと金子です。今回は先日私が作ったGo製のCLIを社内で利用した話を紹介します。
旧ストレージサーバー廃止失敗
現在のPR TIMESの主要なシステムはデータセンター上にあり、ストレージサーバーはアプライアンスのシステムを使用し、アプリケーションサーバーからはNFSでマウントされています。
PR TIMESは日々様々なプレスリリースが配信されており、当然それに伴い画像などのストレージに保存されるファイルが日々増えています。そのためいつかストレージサーバーのディスク容量が枯渇してしまいます。ディスク容量が枯渇すれば当然新しいストレージサーバーへの移行が必要です。
弊社も先日旧ストレージサーバーの容量が枯渇してしまい、新ストレージサーバーに移行する必要がありました。しかしここで問題が発覚します。それは
1つのディレクトリにものすごい量のファイルが入っている!!
ということです。ある程度インフラを触っている人は知っていると思いますが、1つのディレクトリにファイルを大量に保存すれば、そのディレクトリはlsすら実行できなくなってしまいます。ファイルのリストを取得できない場合、rsyncなども最初にファイルリストを作成するため実行できなくなります。
この問題に対して解決する時間がなかったため、先日のメンテナンスでは問題のあるディレクトリ以外を新しいストレージサーバーに移行しました、そして新旧両方のストレージサーバーが併存することになりました。しかしこの構成は運用コストが上がっていますし、そのうち旧ストレージサーバーの容量も枯渇します。必ずいつか崩壊してしまいます。
そこでこの状況を打開するためにアプリケーションの改修を行い、旧ストレージサーバーに保存されていたファイルパスの規則を変更して単一のディレクトリではなく、複数のディレクトリに分けて保存されるようにしました。保存先は新しいストレージサーバーに保存するようにしたので、これにより旧ストレージサーバーへのファイルのアップロードは停止しました。あとは新しいファイルパスの規則に従って旧ストレージサーバーのファイルを新ストレージサーバーに移行すれば旧ストレージサーバーを廃止できるはずです。
ということで旧ストレージサーバーのファイルリストをどうやって取得するのかが今回の記事の内容です。
大量のファイルを取得する試み
問題のディレクトリですが、中には2900万以上のファイルが保存されています。ファイルリストを取得する試みをいくつかやってみました。
ls -U1
lsのソートを止めるls -U1
を実行しても、ばらつきはありますが最大でも600万ファイル程度を出力して停止してしまいました。なぜ停止してしまうのかはちゃんと調べていませんが、もしかしたらストレージサーバーが応答を返さなくなってしまうのかもしれません。
findコマンド
findならlsよりも大量に出力されるのではということでやってみましたが、こちらもばらつきはありますが最大でも1200万行ほどを出力して応答がなくなってしまいました。
ここまででほぼ諦めムードになり、少しずつファイルのリストを出して削除を複数回繰り返して何とかしようとしていました。
getdentsシステムコール
そんな中、以下の記事を見かけました。
You can list a directory containing 8 million files! But not with ls.
800万ファイルが保存されていたディレクトリのファイルリストを取得した方法が紹介された記事です。記事によれば大きなバッファを用意して、それをgetdentsシステムコールに直接渡すことでファイルのリストが取得できたとのことです。
これは是非試したい!と思ったのですが、残念ながら記事にはC言語による実装方法は公開されていますが、実装自体は公開されていませんでした。
Web業界だとシステムコールを直接呼び出すコードを書くことはない気がしますが、幸い私が大好きなGo言語ではsyscallパッケージを使えばシステムコールは直接触れる(その代わりマルチプラットフォームにはできなくなる)ので、Goで作ればいいかということで作ってみました。
実装についてはZennにも軽く書きました。
ということで実装も手に入ったので早速実行してみます。
謎のエラーと実行結果
実行する前にディレクトリのサイズを確認します。ls -dl
で確認できます。ディレクトリ名などは変更してありますが、以下の情報が出力されました。
$ ls -dl target_dir
drwxrwxrwx 3 nfsnobody nfsnobody 2173181952 ** target_dir
まさかの2GB超えです。元記事は800万ファイルで513MB程度だったそうなので、相当なサイズだと思います。ファイル数も4倍弱程度あるので当然ではあります。
llsはデフォルトでls -dl
で返ってくるディレクトリのサイズをbufferのサイズに指定してくれます。ということで早速実行してみたらエラーになりました。
$ lls target_dir > /var/tmp/output.txt
invalid argument
このエラーはシステムコールからEINVALのエラーが返されているようです。
EINVAL Result buffer is too small.
bufferが小さすぎるというエラーらしいですが、bufferが小さくてもすべてのファイルリストが出力できないだけで一部は取得できるはずです。このエラーは最初「どういうこと??」となりました。
llsには-buf-size
というオプションがあり、こちらのオプションを使用することでbufferのサイズを調整することができます。小さすぎるということで適当に大きくしてみましたが、エラー内容は変わりませんでした。
そこで試行錯誤していくとあることに気付きます。
-buf-size 2147483647
は実行できる-buf-size 2147483648
は実行できない
どうやらシステムコールの制約で2GB以上は渡せないようです。こちらが何の制約に引っかかっているのか分かりませんでした。詳しい方がいれば教えてもらえたらうれしいです。ちなみにNFSではないディレクトリで実行しても同様の結果でした。
(2021/09/15追記:getdentsの返り値がintなのでオーバーフローしているのではという指摘をもらいました。確かにそれなら小さすぎるというエラーの理由も説明できるので正解な気がします)
仕方がないのでlls -buf-size 2147483647
を実行してみました。メモリは4GB以上を消費しましたが、数時間程度待つと2938万ファイルのリストが取得できました🎉
このファイルリストは果たして全体の何%くらいが取得できたのでしょうか。まずは-debug
で状況を見ます。
$ lls -buf-size 2147483647 -debug target_dir
bufSize: 2147483647; getdents ret: 2147483632
bufSize
が実際にシステムコールに渡したバッファのサイズ、getdents ret
がgetdents
システムコールの返り値で、実際にバッファ内で利用したバイト数です。この2つの数値の差が非常に小さいのが分かるでしょうか? zennにも書いた通り、大体ファイル1つ辺り (20+ファイル名) byte
を消費します。今回のケースではファイル1つ分以下の差しかないので、まだ他にもファイルはあるのに、全部出せていない状況であることが推測できます。
しかしディレクトリのサイズ (2173181952 byte) を考えれば98%以上は出力されたと推定できます。取得できたファイルリストから移動したファイルを削除して、必要なバッファのサイズを2GB以下にできれば、すべてのファイルリストが取得できるはずです。
現在取得したファイルリストを使用して少しずつファイルを新ストレージにコピーしています。完了して問題なければ旧ストレージからファイルを削除して、もう一度同じことを行う予定です。
(追記:2021/09/20)getdentsシステムコールを複数回呼び出すことで残りのファイルも出力することができます。lls v0.0.2でデフォルト5MBのバッファサイズでgetdentsシステムコールを繰り返し呼び出すように変更しました。このllsを使用することで3312万ファイル全てを出力することができました。v0.0.2によりlsの代替として使えるようになったと思います。
最後に
一時はいつ終わるのか見えなかった旧ストレージサーバー廃止ですが、廃止できる目処が立ち、ホッとしています。
Web企業にいるとあまりこういったシステムプログラミングにはなじみがない人が多いと思います。しかし何か問題になった時にこういった知識が自分を助けてくれた経験が私には何回もあります。こういった機会があればこれからもやっていきたいと思います。
Linuxのシステムプログラミングについて学んでみたい人は個人的に以下の本を薦めています。普段Webプログラミングしかしない人でもあやふやだった知識が整理される良本だと思うのでおすすめします。
