blog.8-p.info

11月からやっていた、daemon/logger: read the length header correctly が無事マージされた。

Docker Engine (プロセスでいうと dockerd) の RSS がどんどん高くなっていって、最終的には Go ランタイムの panic で死んでしまうというバグで、OOM when following “local” logs of high-log-output container として3月には報告され、私は11月から手をつけはじめた。

Go < 1.16 では RSS を信用してはいけない

RSS (Resident Set Size) がどんどん高くなるということは、メモリリーク? と思うけどあんまり怪しいところはない。調べていくと、runtime: default to MADV_DONTNEED on Linux を見つけた。

In Go 1.12, we changed the runtime to use MADV_FREE when available on Linux (falling back to MADV_DONTNEED) in CL 135395 to address issue #23687. While MADV_FREE is somewhat faster than MADV_DONTNEED, it doesn’t affect many of the statistics that MADV_DONTNEED does until the memory is actually reclaimed under OS memory pressure. This generally leads to poor user experience, like confusing stats in top and other monitoring tools; and bad integration with management systems that respond to memory usage.

Go では 1.12 から 1.15 まで MADV_FREE を使っていたのだけど、これは Linux がユーザー空間に露出している各種統計情報には反映されず、色々と混乱を招いていたので、1.16 から MADV_DONTNEED に戻る、らしい。

なので、RSS だけ見てメモリリークっぽい挙動でも、実際にはメモリリークではない、ということがあるらしい。

Docker Engine が中で使っているログフォーマット

Docker Engine が中で使っているログフォーマットは

[メッセージ長][Protocol Buffers でエンコードされたログメッセージ][メッセージ長]

という形になっている。で、どうやらここのメッセージ長が変になってしまって、巨大なメモリを確保しているらしい。ということで、メッセージ長が 10,000 を越えたら、その時点のログを適当なところに保存して、そのメッセージ長を16進数で表示する、という雑な変更を入れて、

% docker logs -f $(docker run --rm -d --log-driver local alpine cat /dev/urandom) > /dev/null

なんてやってみると、panic: too large 95e09737 といって dockerd が死ぬ。保存したログファイルを読んでみると、確かに 0x95 0xe0 0x97 0x37 というバイトの並びは、ログの中の、メッセージ長ではない部分に存在する。

何が悪かったの?

Decode() が諸事情でメッセージ全体を読みきれなかったときに、今まで読んだバイト列をどこかに保存しておいて、次に Decode() が呼ばれた時はそれをつなげる、という処理が抜けていて、メッセージの途中をヘッダと解釈していたのが原因だった。

原因がわかってしまうとどうってことないけれど、Protocol Buffers まじりをバイナリエディタで開いて眺める、なんてあんまりやらないので新鮮なバグでした。