memcached は、シンプルで高速な分散インメモリ キー・バリュー キャッシュです。このドキュメントでは、設計・運用・セキュリティ・クライアント実装の各観点からベストプラクティスをまとめます。
目次
- 基本方針:memcached は「キャッシュ」である
- キャッシュ戦略
- キー設計
- 値(データ)の設計
- TTL(有効期限)の設計
- キャッシュの一貫性と無効化
- Thundering Herd / Cache Stampede 対策
- 分散構成とスケーリング
- サーバー設定・チューニング
- メモリ管理(スラブアロケータ)
- クライアント側の実装
- セキュリティ
- 監視・運用
- アンチパターン
- チェックリスト
1. 基本方針:memcached は「キャッシュ」である
- データはいつ消えても良い前提で設計する。 再起動・メモリ不足による eviction・ノード障害でデータは失われる。
- 永続化・信頼できるデータストアとして使わない。 セッションなど消えると困るデータを置く場合は、消失時の挙動(再ログイン等)を許容できるか確認する。消失が許されないなら Redis(永続化あり)や DB を使う。
- キャッシュがなくてもシステムが(遅くとも)動作すること。 memcached 全停止時に DB が耐えられるか、フォールバック経路を事前に検証しておく。
- memcached の得意分野を理解する。 単純な get/set を大量・低レイテンシで捌くこと。リスト操作、ソート、Pub/Sub、トランザクションなどが必要なら Redis 等を検討する。
2. キャッシュ戦略
Cache-Aside(Lazy Loading)— 最も一般的
1. キャッシュを get
2. ヒット → 返す
3. ミス → DB から取得 → キャッシュに set → 返すdef get_user(user_id):
key = f"user:v1:{user_id}"
user = mc.get(key)
if user is None:
user = db.fetch_user(user_id)
if user is not None:
mc.set(key, user, expire=300)
return user- 長所:実装が簡単、必要なデータだけがキャッシュされる。
- 短所:初回アクセスは必ずミス、更新時に古いデータが残る可能性。
Write-Through / Write-Behind
- 書き込み時にキャッシュも更新する。読み取りヒット率は上がるが、使われないデータまでキャッシュされる。
- 一般的には 更新時は「set」より「delete」 の方が安全(後述の競合問題を避けやすい)。
ネガティブキャッシュ
- 「存在しない」という結果もキャッシュする(例:特殊な番兵値を短い TTL で保存)。
- 存在しないキーへの大量アクセスで DB が叩かれる(キャッシュ貫通)のを防ぐ。
3. キー設計
| 項目 | ベストプラクティス |
|---|---|
| 長さ | 最大 250 バイト。短く保つ(メモリとネットワークの節約) |
| 文字 | 空白・制御文字は使用不可(テキストプロトコル)。ユーザー入力を含む場合はハッシュ化やエンコードを行う |
| 名前空間 | アプリ:エンティティ:バージョン:ID のように階層化する(例:shop:user:v2:12345) |
| バージョン | スキーマ変更時にキーのバージョンを上げることで、古い形式のデータとの衝突を避ける |
| 長いキー | 長くなる場合は SHA-1/SHA-256 等でハッシュ化し、プレフィックスを付けて可読性を残す |
| 環境分離 | 本番/ステージングでキャッシュを共有する場合は環境プレフィックスを付ける(できれば物理的に分ける) |
import hashlib
def make_key(prefix: str, raw: str) -> str:
digest = hashlib.sha256(raw.encode()).hexdigest()
return f"{prefix}:{digest}"名前空間による一括無効化
memcached にはプレフィックス削除の機能がない。グループ単位で無効化したい場合は「名前空間バージョン」を使う。
def ns_key(namespace, key):
ver = mc.get(f"ns:{namespace}")
if ver is None:
ver = int(time.time())
mc.add(f"ns:{namespace}", ver) # add で競合を回避
ver = mc.get(f"ns:{namespace}") or ver
return f"{namespace}:{ver}:{key}"
def invalidate_namespace(namespace):
mc.incr(f"ns:{namespace}", 1) # バージョンを上げると旧キーは参照されなくなる旧キーは参照されなくなり、LRU により自然に追い出される。
4. 値(データ)の設計
- アイテムサイズの上限はデフォルト 1MB(
-Iで変更可能)。大きな値は避ける。- 上限を上げるとスラブクラスの分布が変わり、メモリ効率が落ちることがある。
- 大きなデータは分割するか、そもそもキャッシュ対象を見直す(必要な部分だけキャッシュ)。
- シリアライズ形式を統一する。 言語非依存で安全な形式(JSON、MessagePack、Protocol Buffers 等)を推奨。
- 言語固有のシリアライズ(Python の pickle、PHP の unserialize など)は、キャッシュ汚染時に任意コード実行のリスクがあるため、信頼境界を意識すること。
- 圧縮を検討する。 一定サイズ以上(例:数 KB 超)の値のみ圧縮すると、CPU とメモリのバランスが良い。
- 粒度を適切にする。
- 粗すぎる(ページ全体)→ 一部変更で全体が無効化、サイズ大。
- 細かすぎる(1 フィールドずつ)→ ラウンドトリップ増加。
- 「オブジェクト単位」でキャッシュし、multi-get でまとめて取得するのが基本。
5. TTL(有効期限)の設計
- 必ず TTL を設定する。 無期限(0)は古いデータが残り続ける原因になる。
- データの性質に合わせて TTL を決める。
- 変更頻度が低いマスタデータ:長め(数時間〜1日)
- ユーザー固有・頻繁に変わるデータ:短め(数十秒〜数分)
- TTL にジッター(ランダムな揺らぎ)を加える。 同時に大量のキーが期限切れになるのを防ぐ。
import random
ttl = 300 + random.randint(0, 60)- 30 日の境界に注意。 expiration に 30 日(2,592,000 秒)を超える値を指定すると、相対秒ではなく Unix タイムスタンプ(絶対時刻) として解釈される。意図せず即時期限切れになる事故が起きやすい。
6. キャッシュの一貫性と無効化
更新時は「delete」を基本にする
1. DB を更新
2. キャッシュを delete- 「DB 更新 → キャッシュ set」は、並行リクエスト間で古い値が書き戻される競合が起きやすい。
- delete 後の次の読み取りで最新値が読み込まれる。
競合への追加対策
- CAS(Check-And-Set)を使う。
getsで取得した CAS 値を使い、他者が更新していない場合のみ書き込む。 - delete 後の短期間の書き戻し防止。 読み取り側の set には
add(存在しない場合のみ追加)を使うと、競合の影響を減らせる。 - リース(lease)方式。 Facebook の論文 “Scaling Memcache at Facebook” で紹介された手法。キャッシュミス時にトークンを発行し、トークン保持者のみが set できる。memcached のメタコマンドで類似の仕組みを実装可能。
レプリケーション遅延への配慮
- DB のリードレプリカから読んでキャッシュを埋める構成では、レプリケーション遅延中に古い値がキャッシュされる可能性がある。更新直後のキャッシュ再構築はプライマリから読むなどの対策をとる。
7. Thundering Herd / Cache Stampede 対策
人気キーの期限切れ時に、大量のリクエストが同時に DB へ殺到する問題。
| 対策 | 概要 |
|---|---|
| ロック(mutex key) | ミス時に add("lock:key", ..., ttl短) に成功した 1 プロセスだけが再計算。他は待機 or 古い値を返す |
| 早期再計算(確率的) | 期限切れ前に一定確率で再計算する(XFetch アルゴリズム等) |
| Stale-While-Revalidate | 論理的な期限を値の中に持たせ、実 TTL は長めに。論理期限切れ時は古い値を返しつつ 1 プロセスが裏で更新 |
| TTL ジッター | 多数のキーの同時期限切れを防ぐ |
| メタコマンドの活用 | memcached 1.6 系のメタプロトコルは、stale 値の提供や再計算権の付与(win フラグ)をサーバー側でサポートする |
def get_with_lock(key, loader, ttl=300, lock_ttl=10):
val = mc.get(key)
if val is not None:
return val
if mc.add(f"lock:{key}", 1, expire=lock_ttl):
try:
val = loader()
mc.set(key, val, expire=ttl)
finally:
mc.delete(f"lock:{key}")
return val
# ロック取得失敗:少し待ってリトライ、もしくはフォールバック
time.sleep(0.05)
return mc.get(key) or loader()8. 分散構成とスケーリング
- memcached サーバー同士は通信しない。 分散はクライアント側のハッシュで行う。
- コンシステントハッシュ(ketama 等)を使う。 単純な
hash(key) % Nはノード追加・削除時にほぼ全キーが再配置され、キャッシュヒット率が激減する。 - 全クライアントで同じサーバーリスト・同じ順序・同じハッシュ設定を使う。 不一致があると同じキーが別ノードに書かれ、不整合の原因になる。
- ホットキー対策。
- 特定キーへのアクセス集中は 1 ノードに負荷が偏る。
- アプリ内のローカルキャッシュ(L1)を併用する、キーを複数コピー(
key:1〜key:N)してランダムに読む、などで分散する。
- プロキシの活用。 大規模環境では mcrouter や memcached 内蔵プロキシ(1.6 系以降)などで、ルーティング・レプリケーション・フェイルオーバーを集中管理できる。
- ノード数と 1 ノードあたりのメモリのバランス。 少数の巨大ノードは 1 台障害時の影響が大きい。障害時のミス率増加に DB が耐えられる粒度にする。
- マネージドサービスの検討。 Amazon ElastiCache for Memcached、Google Cloud Memorystore for Memcached などで運用負荷を下げられる。
9. サーバー設定・チューニング
主要な起動オプション:
| オプション | 説明 | 推奨 |
|---|---|---|
-m | 最大メモリ(MB) | 物理メモリに余裕を残す(OS・接続バッファ分)。スワップさせない |
-c | 最大同時接続数 | 想定クライアント数 × 接続プール数 + 余裕。ulimit -n も合わせて調整 |
-t | ワーカースレッド数 | CPU コア数程度。過剰に増やしても効果は薄い |
-l | バインドアドレス | 内部 IP や localhost に限定する(0.0.0.0 は避ける) |
-U 0 | UDP 無効化 | UDP は無効化する(1.5.6 以降デフォルトで無効) |
-I | 最大アイテムサイズ | 必要な場合のみ変更 |
-o modern | 推奨の最新機能群を有効化 | 対応バージョンでは検討(多くは既にデフォルト) |
-o lru_crawler 等 | LRU 管理の改善 | 新しいバージョンではデフォルト有効 |
その他:
- スワップを避ける。 スワップが発生するとレイテンシが急悪化する。
-mを控えめにし、必要に応じて-k(メモリロック)を検討。 - Transparent Huge Pages(THP) の影響を確認する。環境によってはレイテンシのばらつきが出る。
- 最新の安定版を使う。 性能改善・セキュリティ修正・機能追加(メタプロトコル、extstore、TLS など)が継続的に行われている。
- extstore(1.5 系以降)で、SSD をバックエンドに使い大容量・低コストなキャッシュを構成できる。値が大きくアクセス頻度に偏りがあるワークロードで有効。
10. メモリ管理(スラブアロケータ)
- memcached はメモリを「スラブクラス」(サイズごとのチャンク)に分けて管理する。
- アイテムは最も近いサイズのチャンクに格納されるため、サイズのばらつきが大きいと内部断片化が起きる。
- スラブカルシフィケーション(slab calcification) — 古いバージョンでは、一度割り当てられたスラブが他クラスへ移動しにくく、ワークロード変化に弱かった。現在のバージョンでは
slab_reassign/slab_automoveがデフォルトで有効になっており改善されている。古いバージョンを使っている場合はアップグレードを検討する。 -f(成長係数、デフォルト 1.25)を調整すると、チャンクサイズの刻みを変えられる。値のサイズ分布が特殊な場合のみ調整する。stats slabs/stats itemsで、スラブクラスごとの使用状況と eviction を確認する。
11. クライアント側の実装
- 接続プールを使う。 リクエストごとの接続確立は避ける。
- タイムアウトを短く設定する。 キャッシュは「速くなければ意味がない」。接続・読み取りタイムアウトはミリ秒〜数百ミリ秒単位に設定し、タイムアウト時はキャッシュミス扱いで DB にフォールバック。
- 障害ノードの扱いを決めておく。
- 一時的に除外(自動フェイルオーバー)する場合、ノードの離脱・復帰でキーの再配置が起き、古いデータが残るリスクがある。
- 多くのケースでは「障害ノードへのリクエストはミスとして扱い、リングから外さない」方が整合性的に安全。
- multi-get(バルク取得)を活用する。 N+1 問題を防ぎ、ラウンドトリップを削減する。ただし大量キーの一括取得は 1 リクエストのレイテンシを悪化させるので、適度に分割する。
- noreply / quiet モードの使いどころ。 結果確認が不要な書き込みで使うとスループットが上がるが、エラーを検知できなくなる点に注意。
- バイナリプロトコルは非推奨化されている。 新規実装ではテキストプロトコル、またはメタプロトコル(1.6 系)を使用する。
- ローカルキャッシュ(L1)との併用。 ホットキーや極端に読み取りの多いデータは、プロセス内キャッシュを短い TTL で併用するとネットワーク負荷を削減できる。
12. セキュリティ
memcached は デフォルトで認証がない ため、ネットワーク設計が最重要。
- インターネットに絶対に公開しない。
- 2018 年には、公開された memcached の UDP ポートを悪用した大規模な DDoS 増幅攻撃が発生した。
-lで内部インターフェースにのみバインドし、ファイアウォール/セキュリティグループで送信元をアプリサーバーに限定する(デフォルトポート 11211)。- UDP を無効化する(
-U 0)。 - 認証が必要な場合は SASL(バイナリプロトコル)や、テキストプロトコル用の簡易認証機能を検討する。
- 通信の暗号化が必要な場合は TLS を有効化する(1.5.13 以降でビルドオプションとしてサポート)。
- 機密データを保存する場合は慎重に。 memcached 内のデータは平文。必要ならアプリ側で暗号化してから保存する。
- マルチテナント環境でキャッシュを共有しない。 他アプリによるキー衝突・情報漏えい・
flush_allによる全消去のリスクがある。 - キャッシュポイズニングへの備え。 外部入力をキーにする場合はハッシュ化・バリデーションを行う。デシリアライズ時の安全性も確保する(前述)。
13. 監視・運用
主要メトリクス(stats コマンド)
| メトリクス | 意味 | 監視ポイント |
|---|---|---|
get_hits / get_misses | ヒット / ミス数 | ヒット率 = hits / (hits + misses)。急低下はアラート対象 |
evictions | 期限前に追い出された件数 | 継続的な増加はメモリ不足のサイン |
curr_items | 現在のアイテム数 | 傾向の把握 |
bytes / limit_maxbytes | 使用メモリ / 上限 | 使用率 |
curr_connections | 現在の接続数 | -c の上限に近づいていないか |
listen_disabled_num | 接続上限で受付停止した回数 | 0 以外なら -c を見直す |
conn_yields | 1 接続のリクエスト処理で譲歩した回数 | 大量の multi-get が多いと増える |
cmd_get / cmd_set | コマンド数 | スループットの把握 |
expired_unfetched / evicted_unfetched | 一度も読まれずに消えた件数 | 無駄なキャッシュ書き込みの指標 |
運用のポイント
- ヒット率・eviction・レイテンシをダッシュボード化し、アラートを設定する。 Prometheus の memcached_exporter などが利用できる。
- アプリ側でもキャッシュの効果を計測する。 キー種別ごとのヒット率、取得レイテンシ、フォールバック発生率など。
- キャッシュウォームアップを計画する。 再起動やノード追加直後はヒット率が低下する。デプロイ・メンテナンスはトラフィックの少ない時間帯に、ノードを 1 台ずつ実施する。
flush_allは慎重に。 全キャッシュ消去は DB に大きな負荷をかける。本番での実行は手順化・承認制にする。- キー・値の分析ツール。
lru_crawler metadumpで格納されているキーのメタ情報をダンプでき、キーの分布調査に役立つ(本番での実行は負荷に注意)。 - 障害試験を行う。 1 ノード停止・全ノード停止・ネットワーク遅延時のアプリと DB の挙動を事前に確認しておく。
14. アンチパターン
| アンチパターン | 問題点 | 代替策 |
|---|---|---|
| memcached を唯一のデータストアにする | 再起動・eviction でデータ消失 | DB を真実の源泉にする |
| TTL なし(0)で保存 | 古いデータが残り続ける | 必ず TTL を設定 |
| 巨大な値を保存 | メモリ効率低下、ネットワーク負荷、上限超過 | 分割・必要部分のみキャッシュ・圧縮 |
hash % N で分散 | ノード増減で大量ミス | コンシステントハッシュ |
| キャッシュを全キー一括削除で無効化 | DB への突発的負荷 | 個別 delete や名前空間バージョン |
| 更新時に古いデータで set | 競合で古い値が残る | delete、CAS、リース |
| インターネットに公開 | 不正アクセス・DDoS 増幅に悪用 | 内部ネットワーク限定・UDP 無効化 |
| タイムアウト未設定 | memcached 障害時にアプリ全体が停止 | 短いタイムアウト + フォールバック |
| キャッシュ前提で DB 容量を設計 | キャッシュ全損時に DB がダウン | キャッシュなしでの最低限の耐性を確保 |
| ループ内で個別 get(N+1) | ラウンドトリップ増大 | multi-get |
15. チェックリスト
設計
- キャッシュが消えてもシステムが動作する設計になっている
- キーに名前空間とバージョンを含めている
- キー長 250 バイト以内、使用不可文字を含まない
- 値のサイズが 1MB(または設定上限)を十分下回る
- 全キーに TTL を設定し、ジッターを加えている
- 更新時の無効化方針(delete / CAS 等)が決まっている
- 人気キーのスタンピード対策がある
- シリアライズ形式が安全で統一されている
インフラ・設定
- 内部ネットワークのみにバインドし、ファイアウォールで制限している
- UDP を無効化している
-
-m,-c,-tを環境に合わせて設定している - スワップが発生しない
- 最新の安定版を使用している
- 必要に応じて TLS / 認証を有効化している
クライアント
- 接続プールを使っている
- 短いタイムアウトと、ミス時のフォールバックを実装している
- コンシステントハッシュを使い、全クライアントで設定が一致している
- multi-get を活用している
運用
- ヒット率・eviction・接続数・レイテンシを監視している
- アラートを設定している
- ノード障害時・全停止時の挙動を試験済み
-
flush_allやノード入れ替えの手順が整備されている
参考資料
- memcached 公式サイト: https://memcached.org/
- memcached 公式 Wiki(GitHub): https://github.com/memcached/memcached/wiki
- プロトコル仕様: https://github.com/memcached/memcached/blob/master/doc/protocol.txt
- R. Nishtala et al., “Scaling Memcache at Facebook”, NSDI 2013