memcached の max_item_size 変更前に知るべき Slab Allocator の罠
業務で memcached を運用していると、「Value for ... over max size」という警告に出くわすことがある。解決策は max_item_size の引き上げだが、このパラメータ変更は、一見単純に見えて奥が深い。下手に変更すると、メモリ効率の悪化や、意図しない Eviction の頻発を招く可能性がある。
この記事は、max_item_size の変更を検討しているエンジニアが、memcached のメモリ管理の仕組みを理解し、安全な変更計画を立てるための備忘録である。
1. memcached の心臓部: Slab Allocator
memcached のパフォーマンスの秘訣は、そのメモリ管理方式 Slab Allocator にある。一般的な動的メモリアロケータ (malloc など) が抱える外部フラグメンテーションの問題を解決するために設計された。
動的アロケータの問題点: メモリの確保と解放を繰り返すと、空き領域が歯抜け状態になり、合計の空きは十分でも大きな連続領域を確保できなくなる。
Slab Allocator の解決策: 起動時に、あらかじめ決まったサイズの「箱 (Chunk)」を階層的に大量に確保しておく。データを保存する際は、そのデータが収まる最小の箱に入れるだけである。これにより、確保・解放の処理が極めて高速になり、外部フラグメンテーションも発生しない。
この「固定長の箱を事前確保する」という特性が、以降で述べる全ての事象の根源となる。
2. 巨大アイテムが引き起こす副作用
max_item_size を引き上げるということは、この「箱」の最大サイズを引き上げるということだ。これが二つの副作用を引き起こす。
副作用1: 内部フラグメンテーションによるメモリの無駄
Slab Allocator は外部フラグメンテーションを解決するが、代わりに内部フラグメンテーションを発生させる。
例えば、1.1MB のデータを保存する場合を考える。memcached は、そのデータが収まる最小の箱を探す。もしその箱のサイズが 1.25MB であれば、差分の 150KB は純粋な無駄領域となる。max_item_size を大きくすればするほど、箱のサイズ階層の間隔も広がり、この無駄が顕著になる傾向がある。
副作用2: Eviction の誘発
最も注意すべき副作用がこれである。巨大なアイテムは、他の小さなアイテムの Eviction (追い出し) を誘発する。
memcached は、特定のサイズの箱が足りなくなると、共有のメモリプールから新たなメモリ領域 (Page) を確保し、そのサイズの箱で埋め尽くして拡張する。
ここに巨大なアイテム (例: 4MB) が保存されると、それだけでメモリ Page をごっそりと消費する。結果として、共有メモリプールが急速に枯渇する。
そうなると、セッション情報のような頻繁にアクセスされる小さなアイテム (例: 1KB) の箱が足りなくなった際に、拡張するための Page がプールに残っていない、という事態に陥る。他のサイズの箱のメモリを奪うことはできないため、その小さなアイテムの箱の中で LRU (Least Recently Used) に基づく Eviction が始まるのだ。
つまり、犯人は巨大アイテムなのに、被害者は小さなアイテム、という状況が発生する。
3. 調整弁: Growth Factor
このメモリの無駄をコントロールするための調整弁が Growth Factor (-f オプション) である。これは、Slab の箱のサイズがどれくらいの倍率で大きくなっていくかを決める値だ (デフォルト: 1.25)。
-f 1.1 のように値を小さくする:
箱のサイズが細かく刻まれるため、内部フラグメンテーションは減る (メリット)。しかし、管理する箱の種類が増えるため、オーバーヘッドが若干増える (デメリット)。
-f 1.5 のように値を大きくする:
箱の種類は減るが、内部フラグメンテーションは増大する。
データサイズの分布に合わせてこの値を調整することで、メモリ効率を最適化できる。
4. 実践的なアクションプラン
以上の特性を踏まえ、安全に max_item_size を変更するためのプランを立てる。
Step 1: データ分布の分析
何よりもまず、どのようなサイズのデータが、どれくらいの頻度で上限を超えているのかを定量的に分析する。CloudWatch Logs Insights などを使えば、以下のようなクエリで簡単に可視化できる。
fields xxx
| filter log_level = 'warn' and message like /over max size/
| parse message /over max size: \d+ <= (?<actual_size_str>\d+)/
| stats count(*) as warningCount by floor(actual_size_str / 100000) * 100000 as sizeBucket
| sort sizeBucket asc
この結果を見て、上限をいくつにすべきか、またデータが特定のサイズ範囲に集中していないか(Growth Factor 調整のヒント)を把握する。
Step 2: 二段階での変更適用
いきなり全てのパラメータを変更するのはリスクが高い。
第一段階: まずは max_item_size のみを引き上げる (-I 5m など)。これで当面の問題は解決するはずである。その後、サーバのメモリ使用率を注意深く監視する。
第二段階: もしメモリ使用率が想定以上に高い場合、第一段階で分析したデータ分布に基づき、Growth Factor の調整を検討する (-f 1.1 など)。
まとめ
memcached の max_item_size 変更は、単なる上限緩和ではない。Slab Allocator の仕組みを理解し、それが内部フラグメンテーションや Eviction にどう影響するかを把握することが重要である。
データに基づいた分析を行い、段階的なアプローチを取ることで、リスクを最小限に抑えつつ、システムの安定性を保つことができる。
Discussion