中学理科1626207 views
いろは2986023 views
LaTeX957300 views
高校生物549842 views
ヒストリア284143 views
世界の国560595 views
Computer365120 views
中学数学621382 views
りんご192546 views
高校化学2913383 views
Help
Tools

English

gunicorn でメモリ使用量が肥大化する原因と対策

gunicorn を長期間運用していると、メモリ使用量が徐々に増加し、最終的に OOM Killer に殺されるという報告が後を絶たない。GitHub の IssueFastAPI のディスカッションでも繰り返し議論されている問題だ。worker クラスを変えれば解決すると思われがちだが、実際にはそう単純ではない。

メモリが解放されない根本原因

gunicorn 自体にメモリリークがあるわけではない。問題の多くは Python と glibc のメモリ管理の仕組みに起因する。

Python のメモリ管理

Python はオブジェクトを解放しても、そのメモリを OS に返却しない。Python のメモリプールに保持され、次の割り当てに再利用される

glibc の arena 機構

Linux の glibc malloc はスレッドごとに arena というメモリプールを作成する。arena のメモリも簡単には OS に返却されない

つまり、一度大きなメモリを確保すると、Python レベルでも glibc レベルでも「OS から見た使用量」は減らないのだ。

glibc arena の問題

glibc の malloc 実装はマルチスレッド環境での性能向上のため、複数の arena を作成する。64 ビット環境ではデフォルトで CPU コア数 × 8 個の arena が上限となる。

arena が増える条件

スレッドがメモリを確保しようとして既存の arena がロックされていると、新しい arena が作成される。gunicorn の gthread worker やスレッドプールを使うアプリケーションで発生しやすい。

arena が解放されない理由

スレッドが終了しても arena は「空き」とマークされるだけで、OS には返却されない。arena 内に 1 バイトでも使用中のメモリがあると、その arena 全体が保持され続ける。

40 コアのサーバーなら最大 320 個の arena が作成される可能性があり、それぞれが数十 MB を占有すると、それだけで数 GB のメモリが「使用中」に見えてしまう。

worker クラスを変えても解決しない理由

sync、gthread、gevent、uvicorn など worker クラスを変更しても問題が解消しないケースが多い。

sync worker でも Python 内部でスレッドが使われる

gevent/eventlet は内部で独自のメモリ管理を持つ

uvicorn worker でもイベントループ内でメモリ断片化が発生

いずれも glibc arena の影響を受ける

worker クラスの選択は並行処理モデルの違いであり、メモリ管理の根本的な解決にはならない。

現実的な対策

完全な解決は難しいが、以下の方法で影響を軽減できる。

max-requests で定期再起動

一定リクエスト数で worker を再起動し、メモリをリセットする。jitter を付けて一斉再起動を避ける。

MALLOC_ARENA_MAX の設定

glibc の arena 数を制限する。Heroku は MALLOC_ARENA_MAX=2 をデフォルトにしている。

preload オプション

アプリケーションコードを fork 前にロードし、Copy-on-Write でメモリを共有する。ただし Python の参照カウントにより効果は限定的。

jemalloc への切り替え

glibc malloc の代わりに jemalloc を使う。メモリ断片化が少なく、積極的に OS にメモリを返却する。

設定例

# gunicorn 起動時の設定例
gunicorn app:application \
  --workers 4 \
  --max-requests 10000 \
  --max-requests-jitter 1000 \
  --preload

環境変数で glibc の挙動を制御する場合は以下のように設定する。

# arena 数を制限
export MALLOC_ARENA_MAX=2

# または jemalloc を使用
export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2

根本的には「仕様」である

リクエスト処理後にメモリが減らないのは、多くの場合バグではなく仕様だ。メモリを OS に返却しないことで、次の割り当てを高速化している。ただしコンテナ環境のように厳密なメモリ制限がある場合、この「最適化」が裏目に出る。

長期運用を前提とするなら、max-requests による定期再起動は事実上必須と考えたほうがよい。メモリリークがなくても、メモリ使用量は単調増加する傾向があるためだ。