【RDB脳の罠】本番環境で `KEYS` を叩くエンジニアをクビにすべきこれだけの理由
テックリードの私だ。
コードレビューをしていると、時々信じられない光景に出くわす。
本番環境のパフォーマンスチューニング中、あるいはアプリケーションのデバッグ中に、誰かが平然と `KEYS` コマンドを発行しているログを見つけた時の絶望感をわかるだろうか。
「え?だって特定のプレフィックスを持つキーを一発で引きたかったんです」
「開発環境では一瞬で返ってきましたよ?」
――今すぐそのキーボードから手を離せ。
Redisはミリ秒以下の超高速インメモリデータベースとして設計されている。しかし、その圧倒的なパフォーマンスは「適切なデータ構造とコマンドの選択」という厳格な契約の上に成り立っている。`KEYS` コマンドはその契約を真っ向から踏みにじる、本番環境における時限爆弾だ。
今回は、なぜ `KEYS` が「悪」なのか、その内部構造から紐解き、プロのエンジニアが採用すべき真の設計パターンを授けよう。
—
1. なぜ `KEYS` は「死刑宣告」なのか?(内部アーキテクチャの真実)
まず、Redisの根本的な思想を思い出してほしい。
「Redisはシングルスレッドで動作する」
この設計により、複雑なロック機構のオーバーヘッドや競合(Race Condition)を排除し、驚異的なスループットを実現している。しかし、これは裏を返すと「1つのコマンドが実行されている間、他のすべてのクライアントのリクエストは完全にブロックされる(止まる)」ことを意味する。
ここで `KEYS pattern` の挙動を見てみよう。
例: ユーザーセッションのキーをすべて探す
KEYS user:session:
お気付きだろうか?
`KEYS` コマンドは、インデックス化されていない。つまり、Redisの全キーを格納する内部ハッシュテーブルをすべて走査(Full Table ScanならぬFull Keyspace Scan)する。
- 時間計算量(Time Complexity): $O(N)$ ($N$ はデータベース内のキーの総数)
もし、あなたの本番Redisに1,000万件のキーが格納されていたらどうなるか。
Redisは単一スレッドで1,000万回のパターンマッチング(Glob-styleパターンの評価)を愚直に実行し始める。この間、他のすべてのAPIリクエスト、キャッシュの取得、セッションの検証は完全に沈黙(フリーズ)する。
結果として、フロントエンドのアプリケーションサーバーでコネクションタイムアウトが雪崩式に発生し、サービス全体が数秒〜数十秒にわたってダウンすることになる。
開発環境で「一瞬」だったのは、キーが数十件しかなかったからに過ぎない。本番のデータ量を舐めてはならない。
—
2. 唯一の救済措置:`SCAN` コマンドによる安全なイテレーション
「じゃあ、キーの一覧や特定のパターンに合致するキーをどうやって取得すればいいんだ?」という話になる。
ここで登場するのが `SCAN` ファミリー だ。
`SCAN`, `SSCAN`, `HSCAN`, `ZSCAN` は、カーソルベースのイテレーションを提供する。これを使えば、データベースをブロックすることなく、安全にキーを列挙できる。
`SCAN` の基本戦略
`SCAN` は一度に全キーをスキャンするのではなく、小さな単位(チャンク)に分割してスキャンを繰り返す。
カーソル 0 からスタートし、マッチパターンを指定してスキャン(COUNTはヒント)
SCAN 0 MATCH user:session: COUNT 100
実行結果のイメージ:
1) “14” # 次回使用する新しいカーソル位置
2) 1) “user:session:1001” # マッチしたキーのリスト
2) “user:session:1042”
返ってきたカーソルが `0` になるまでループを回すことで、サーバーをブロックせずに安全に全件を舐めることができる。
とはいえ、これもあくまで「バッチ処理や管理ツールでのメンテナンス用」だ。通常のアプリケーションのライフサイクル内で、リアルタイムに `SCAN` を回すような設計にしている時点で、データモデリングの敗北を疑ったほうがいい。
—
3. 実務で採用すべき「堅牢な設計パターン」
そもそも、なぜキーのパターンマッチングが必要になるのか?
大抵の場合それは「リレーショナルデータベース(RDB)的なクエリをRedisに持ち込もうとしている」ことが原因だ。
RedisはKVS(キーバリューストア)であり、RDBの `WHERE col LIKE ‘%abc%’` のような柔軟な部分一致検索を行う場所ではない。プロのエンジニアは、以下のいずれかの設計パターンでこの問題を華麗に回避する。
パターンA: 適切なネームスペース設計とハッシュ(Hash)の活用
散らばったキーを `KEYS` でかき集める必要があるなら、最初から一つのデータ構造にまとめ上げるか、プレフィックスを構造化せよ。
例えば、ユーザーごとのメタデータをバラバラのキーで持たず、Redisの Hash構造 にまとめる。
アンチパターン: 散らばったキー
user:100:name = “Alice”
user:100:age = 30
user:101:name = “Bob”
user:101:age = 25
推奨パターン: Hash構造による集約
HSET user:100 name “Alice” age 30
HSET user:101 name “Bob” age 25
これなら `HGETALL user:100` で一撃で取得できるし、ユーザーIDさえわかれば `KEYS` を使う必要など一切ない。
パターンB: セカンダリインデックス(Sets / Sorted Sets)の自前構築
「特定の条件に一致するキーのリスト」がどうしても欲しい場合は、検索用のインデックスを `Set` や `Sorted Set (ZSET)` で明示的に管理する。
例えば、「アクティブなセッションを持つユーザーIDのリスト」を管理する場合:
ユーザーがログインした時、アクティブセットに追加
SADD active:sessions “user:1001”
SADD active:sessions “user:1042”
アクティブなセッションのキー一覧を安全にO(1)で取得
SMEMBERS active:sessions
このように、「検索に必要なインデックスをデータ書き込み時に同時に更新する」のがRedisを使いこなす上での絶対の鉄則だ。
—
4. チーフアーキテクトからの戒め
本番環境において、`KEYS` コマンドの実行は「銃の安全装置を外し、ロシアンルーレットを始める行為」と同義だ。
あなたがもしジュニアエンジニアから「キーを確認したいので `KEYS` を叩いてもいいですか?」と聞かれたら、こう答えてほしい。
> 「開発環境ならな。だが本番でそれをやったら、お前の給料からインシデントの補填を引くぞ」 と。
実務におけるRedisの運用は、ミリ秒のレイテンシと99.999%の可用性との戦いだ。コマンド一発の気まぐれでシステムを全滅させないために、背後のメカニズムを常に意識した高潔なコードを書き続けろ。
設計を見直し、正しいデータ構造を選べ。Redisは裏切らない。裏切るのはいつだって、設計をサボった人間だ。
コメント