Chef で cpanm --installdeps . を実行すると大量のメモリを消費する

経緯

Vagrant で立てた VM を provision する際に Chef を用いて cpanm --installdeps . を実行して依存する CPAN モジュールをインストールする Recipe を書いて実行するとたびたび NoMemoryError で Chef を実行するプロセスが殺されてしまう。

まず、デフォルトのログ出力レベルだと不親切すぎるので、`VAGRANT_LOG=debug vagrant provision` などとして詳細なログ出力を確認したほうがよい。 (参考: Vagrant Documentation)

NoMemoryError と言われてプロセスが殺されるのだからまず VM に割り当てるメモリを増やしてみることにする。これまで 512MB だったので増やしてみるが、1024MB でも 2048MB でも NoMemoryError が送出される。

cpanm --installdeps . が 2GB ちかくもメモリを食い潰すのはおかしい。

Mixlib::Shellout

ところで、Chef は execute resource などで実行されるコマンドの終了ステータスが非ゼロの場合、stderr と stdout の内容を出力するようになっている。

IO.select して出力をバッファ *1 に溜めて、必要なときに出力するようになっている。

Module::AutoInstall

Module::AutoInstall は依存するモジュールを自動的に入れようとする Module::Install のプラグインだが、入力を与えないとずっとプロンプトが出て入力を求めてくる (PERL_AUTOINSTALL 環境変数に値を与えてもうまくいかなかった)。

これはタイムアウトするまで行われるので stderr への大量の出力が行われることになる。

(また < /dev/null として標準入力を渡すこともできなかった)

推測

あるプロセスから大量の出力がある場合、かつそのプロセスが (いかなるステータスにせよ) 終了するまでの時間が長い場合、Mixlib::Shellout のバッファがなかなか flush されないのでバッファが肥大化しうる。

cpanm --installdeps . はまさにそのような条件を再現しやすいのではないか。

ruby-prof を用いれば高級なプロファイリングが行えそうではあるが、Chef とどのように組み合わせたものかすこし悩ましいのでひとまず推測にとどめる。

対策

--quiet オプションを cpanm に渡す。こうすることで stderr あるいは stdout への出力が抑制される。

ログ自体は $HOME/.cpanm/build.log に変わらず出力されるのでデバッグが困難になることは少なさそう。

結論

対話的入力つらい。

環境

  • ホストOS: OS X 10.8.3
  • ゲストOS: Ubuntu 12.10
  • Vagrant: 1.1.5
  • Chef: 10.12.0

*1:インスタンス変数、単なる String オブジェクト