対象:Redis/Memcached + RDB/DWH(PostgreSQL, BigQuery, DynamoDB, Cassandra 等)でオンライン特徴量を提供しているチーム
TL;DR(まずは結論)
- 推論時に**DBとキャッシュに同時書き込み(ライトスルー)**を行うと、キャッシュミス連発や「古い値が返る」事故を抑えられる。
- 短めTTL + ジッター(ランダム化)で一斉失効スパイクを回避。
- ミス時はDBフォールバック→即キャッシュ補填でE2Eレイテンシを安定化。
- 「ライトバックのみ」「固定TTLのみ」「キャッシュ専用運用」はアンチパターン。
背景/よくある困りごと
オンライン特徴量をRedisやMemcachedのようなKVSに置くとき、
- 推論要求のたびに都度更新(ライトバック)
- 事前の一括投入(バルクロード) のどちらかに偏ると、一貫性の乱れやスパイク時のDB過負荷につながる。特に高トラフィックでは、
- 最新値が取得できずオフライン評価との差が出る
- キャッシュミス多発 → レイテンシ悪化 → さらにミス増加 が連鎖的に起こる。
解決策:ライトスルー戦略(Write‑Through)
オンライン更新のたびにDB更新と同時にキャッシュへ書き込む。これで、
- キャッシュミスを減らす
- DBとキャッシュの一貫性を維持
- バックエンドDBへの**輻輳(ふくそう)**を回避 が可能。
実装の要点
- 同一トランザクション境界を意識(DBコミット成功後にキャッシュ書込)。
- Idempotency(冪等性):同一リクエストの再送でも結果が変わらないようにする。
- 短めTTL + ジッター:
ttl = base + rand(±jitter)
で一斉失効を防ぐ。 - ホットキー対策:キー分散(suffixシャーディング)や局所LRU。
サンプル(Python + Redis + PostgreSQL)
import os, json, random
import redis, psycopg2
r = redis.Redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379/0"))
pg = psycopg2.connect(os.getenv("PG_DSN", "dbname=features user=mlops"))
BASE_TTL = 600 # 10分
JITTER = 120 # ±2分
def _ttl():
return BASE_TTL + random.randint(-JITTER, JITTER)
def update_feature(entity_id: str, features: dict):
"""ライトスルー:DB更新の直後にRedisへ反映(冪等)"""
payload = json.dumps(features, separators=(",", ":"))
with pg:
with pg.cursor() as cur:
# UPSERT(PostgreSQL)
cur.execute(
"""
INSERT INTO features(entity_id, payload, updated_at)
VALUES (%s, %s, now())
ON CONFLICT (entity_id)
DO UPDATE SET payload = EXCLUDED.payload, updated_at = now()
""",
(entity_id, payload),
)
# DBコミット成功後にキャッシュへ(at-least-once)
key = f"feat:{entity_id}"
r.setex(key, _ttl(), payload)
def get_feature(entity_id: str):
key = f"feat:{entity_id}"
val = r.get(key)
if val:
return json.loads(val)
# ミス時:DBフォールバック → 即キャッシュ補填
with pg.cursor() as cur:
cur.execute("SELECT payload FROM features WHERE entity_id = %s", (entity_id,))
row = cur.fetchone()
if row and row[0]:
r.setex(key, _ttl(), row[0])
return json.loads(row[0])
return None
メモ:RDB以外(DynamoDB/Cassandra 等)でも、永続層コミット → キャッシュ反映の順序は同じ。キュー/リトライで一時失敗にも耐性を持たせると良い。
ありがちなアンチパターン
- ライトバックのみ:一時的なキャッシュ更新がDBに反映されず、古い特徴量が返る。
- キャッシュ専用運用:Redis障害で全推論が落ちる(DBフォールバック経路が無い)。
- 固定TTLだけ:同時刻に一斉失効 → ミス雪崩 → スパイク。
関連技術の組み合わせ
- Feast:Redisをオンライン、BigQuery/Bigtable等をオフライン。
materialize
(バッチ)+ストリーミングを併用して鮮度と一貫性を両立。 - DynamoDB:Streams + Lambda でコミットイベント駆動のライトスルーに近い設計が可能。
- Cassandra:
CONSISTENCY QUORUM
を併用しつつ、書込後フックでRedis補填。
観測(Observability)設計のコア
最終的に守るべきはユーザーの体感。LagだけでなくE2E遅延を主指標にする。
- メトリクス
feature_e2e_latency_ms
(p50/p95/p99)cache_hit_ratio
(ヒット率)db_fallback_rate
(ミス→DB参照率)redis_errors_total
,retry_count
- アラート例
- p95 E2E > 200ms が 5分継続
cache_hit_ratio < 0.9
が 10分継続db_fallback_rate
スパイク(平常比 +300%)
ロールアウト戦略(安全に入れる)
- Shadow/Read‑only 段階:ライトスルーはONだが推論は従来経路を参照。
- Canary(5%→25%→50%→100%):メトリクスしきい値で自動ロールバック。
- 手戻り策:feature flag で瞬時に旧経路へ切替。
障害モードと対策
- Redis障害:DBフォールバックが即座に機能する設計か? コネクションプール/サーキットブレーカ必須。
- DB障害:一時的に**ライトバック(キャッシュ優先)**へ切替できる運用Runbookを用意。
- ホットキー:キー分散、レプリカ読み、レイテンシベースのHPA、
randomized TTL
。
1分でわかる比較表
パターン | 一貫性 | レイテンシ | 失敗時の挙動 | 運用難易度 |
---|---|---|---|---|
ライトスルー | 高い | 低い | DB/Redis片系障害でも耐性(設計次第) | 中 |
ライトバックのみ | 中〜低 | 低い(短期) | 乖離・ロストで品質低下 | 低 |
キャッシュ専用 | 低 | 低 | キャッシュ障害=全停止 | 低 |
バルクロードのみ | 中 | 中 | 鮮度が落ちやすい | 低 |
よくある質問(FAQ)
Q. TTLはどれくらいが目安?
A. 5〜15分を起点に、E2E遅延・ヒット率・DB負荷の3点を見て調整。常時更新が走る特徴量は短め、滅多に変わらないIDマッピング系は長め。
Q. 強整合性が絶対に必要な場合は?
A. 書き込み後に読取で必ずキャッシュを経由し、DB側とバージョン番号/更新時刻で突合。必要に応じてRead‑Your‑Writes保証をアプリで担保。
Q. マルチリージョンでは?
A. ソースオブトゥルースは1つに決め、クロスリージョンは非同期複製+バージョン優先ルールで解決。リージョン内は近接キャッシュ。
まとめ
- ライトスルーは一貫性とスケーリングのバランスが良く、現実解になりやすい。
- ただし“入れただけ”では不十分。ジッターTTL、DBフォールバック、観測/アラート、ホットキー対策、ロールバック手段まで揃えて初めて安定運用。
参考実装の拡張ポイント
- 失敗時再試行(キュー化/DLQ)
- 書込後イベントをPub/Sub配信し、他キャッシュ層も非同期更新
If-Match
ヘッダやversion
カラムで競合検出(楽観的ロック)