2020/01/18golangDockergolangで書いたプログラムをDockerで動かしOOMが発生した際になるべく情報を残して殺される方法を紹介します。TL;DRgolang製のプログラムは仮想メモリ(VSZ)の確保に失敗するとgoroutineのダンプを吐いて死ぬDockerのOOMはRSSベースで検出時にSIGKILLを投げてくるDocker利用時にVSZで制限をかけるスクリプトを書いたgolang製のプログラムはlinux-amd64において最低でも101MBのVSZを要求するVSZの制限がそれより小さいと当然起動できない実際のRSSは3MB程度で起動するBackgroundコンテナ内で動いているプロダクション上のgolang製のプログラムが時々OOMに殺されていました。何も言わずに殺されているのでせめてJavaのようにスレッドダンプを出して死ねないかとの当然の要望が。mmap()システムコールが_ENOMEMを返せばgoroutineのダンプとともに死ぬことがわかりました。DockerのOOMはLinuxカーネルのcgroupという機能によってページイン(RSSが増える)のタイミングで検出されます。RSSっていうのは物理メモリのことで、Linuxでは仮想メモリに値が書き込まれた時に初めて割り当てられます。cgroupによるOOMの補足時にはSIGKILLが送られてくるのでプロセス側ではあがく余地は一切ありません。Dockerでは仮想メモリの確保には成功してしまい制限を超えた物理メモリを割り当てた後にSIGKILLが飛んできます。このミスマッチがメモリ制限下のDockerでgolang製のプログラムを動かす際の不幸を生んでいることがわかりました。wrap-limit-vを書きました。-vに渡してVSZを制限してから目的のプログラムをexecで起動します。Dockerfileからの使い方としてはENTRYPOINTの先頭にこのwrap-limit-vを突っ込むだけです。"/opt/bin/my_prog"]メモリ枯渇をハンドルできるプログラムにとってはRSSで制限するよりもVSZで制限するほうが理に適っています。このwrap-limit-vはそういったプログラムにも有用だと考えられます。いままで動いていた別のプログラムがOOMでまったく実行できなくなりました。図らずもwrap-limit-vがちゃんと機能しオペレーションが詳細を把握できることは証明できました。その際のエラー(の代表的なメッセージ)はこんな感じ。use)要約すると「いままでVSZを0バイト使ってて新たに8Kバイトを確保しようとしたら失敗ちゃった(テヘペロ」です。相当の設定になっていました。VSZとしても64MBは割り当て可能なはずです。突っ込みたいところですが、冷静にgolangのソースコードを調べます。するとgolangのランタイムのヒープとして使うメモリは64ビット環境では64MB単位で確保してそれを細かく分割して払い出すことがわかりました。細かく見ていくとlinux-amd64環境ではこのヒープ管理にヒープアリーナとして追加で32MBを必要とします。加えてメタデータなどの管理用に2MB+αを確保するので合計35MB程度が必要です。実はgolang製のプログラムが必要とするVSZはこれだけではありません。さまざまなシステム管理領域として約2MBを確保してました。linux-amd64環境で動くgolang製のプログラムは最低でも101MBの仮想メモリ(VSZ)を確保する必要があるということです。コレは物理メモリに対する要件ではありません。「メモリ情報を表示したのちに1時間time.Sleep()するだけのプログラム」を実行してprocfsから概算した物理メモリ(RSS)は3MB以内でした。これにはプログラム自身を載せる物理メモリやOSとして必須なものも含まれています。加えてgolangのランタイムのヒープとして払い出されたメモリは8KBに過ぎません。cgroupがRSSで制限をかけることにも一定の理解はできます。以上golangのメモリの使い方の調査報告でした。golangのバージョンによっても変わることに留意してください。