SCRIPT FLUSHの深層:Luaスクリプトキャッシュの内部メカニズムとメモリ管理の限界
Redisを単なる「高速なキーバリューストア」と捉えているうちは、このミドルウェアの真価の半分も見えていない。Redisの本質は、シングルスレッドのイベントループ上でアトミックに動作する、極限までチューニングされたインメモリ・実行エンジンである。
そのエンジンの中核をなすのが、サーバー側で動的なロジックを安全に実行するための Luaスクリプトサブシステム だ。
今回は、このサブシステムが抱えるメモリの闇と、それを制御するための `SCRIPT FLUSH` コマンドについて、Redisの内部アーキテクチャの観点から徹底的に解剖する。
—
1. Luaスクリプトキャッシュの内部構造
Redisに送信されたLuaスクリプトは、単にその場で実行されて消えるわけではない。`EVAL` コマンドや `EVALSHA` コマンドの効率的な実行を担保するため、スクリプトはサーバー内の専用キャッシュに保持される。
SHA1ダイジェストと辞書(Dictionary)の裏側
Redisのソースコード(`script.c` あたり)を覗いたことがある者なら知っているはずだ。Redisは、読み込まれたすべてのLuaスクリプトのSHA1チェックサムをキーとし、パース済みのLuaバイトコード(あるいは関数ポインタ)をバリューとする、内部的なハッシュテーブル(Dictionary)を維持している。
/ 概念的な内部構造のイメージ /
dict lua_scripts = dictCreate(&luaScriptsDictType, NULL);
/
- Key: 40文字のSHA1ヘキサ文字列 (例: “23235571… “)
- Value: 実行可能なLua関数のポインタ (rio構造体やrobjなど)
/
`EVALSHA` が呼び出された際、Redisはこのハッシュテーブルを $O(1)$ でルックアップし、Luaの仮想マシン(Lua VM)上で直接関数をキックする。これにより、ネットワーク帯域の節約と、スクリプトのパースオーバーヘッドの完全な排除を同時に実現している。
—
2. なぜ `SCRIPT FLUSH` が必要なのか?(メモリリークとキャッシュ肥大化)
「キャッシュなんだから、放っておいても勝手に消えるだろう」という甘い認識は、大規模プロダクションでは致命傷になる。
スクリプトキャッシュの寿命は「Redisのライフサイクル」に等しい
デフォルトの状態において、RedisのLuaスクリプトキャッシュには TTL(有効期限)の概念が存在しない。一度読み込まれたスクリプトは、明示的に削除するか、サーバーが再起動されるまで、永遠にメモリを占有し続ける。
これが何を意味するか?
動的に生成されたクエリや、UUIDなどを埋め込んだユニークなスクリプトを誤って大量に `EVAL` し続けた場合、このキャッシュ辞書は無限に肥大化し、最終的に OOM (Out of Memory) キラーの餌食 になる。
ここで登場するのが `SCRIPT FLUSH` である。
キャッシュを強制解放する
127.0.0.1:6379> SCRIPT FLUSH
OK
このコマンドを発行すると、Redisは内部の `lua_scripts` 辞書を走査し、保持されているすべてのLuaスクリプトの参照を断ち切り、メモリを解放する。
—
3. `SCRIPT FLUSH` の実行モード:`ASYNC` と `SYNC` のトレードオフ
Redis 6.2以降、`SCRIPT FLUSH` にはオプションを指定できるようになった。
ここが本稿の最も重要なハイライトの一つである。大規模なデータセットを持つRedisインスタンスにおいて、単なるフラッシュはシステム全体のレイテンシを悪化させる要因になり得る。
同期的なフラッシュ(ブロッキング)
127.0.0.1:6379> SCRIPT FLUSH SYNC
非同期的なフラッシュ(バックグラウンドスレッドで処理)
127.0.0.1:6379> SCRIPT FLUSH ASYNC
1. `SYNC`(デフォルト)
メインスレッドをブロックし、同期的にキャッシュを破棄する。数個のスクリプトであればオーバーヘッドは無視できるレベルだが、数万、数十万のユニークなスクリプトが汚染している場合、このメモリ解放処理(`dictFree`)の間、Redisのシングルスレッドイベントループは完全に凍結する。ミリ秒単位のSLAが求められる環境では、ピークタイムの `SYNC` は自殺行為になり得る。
2. `ASYNC`
バックグラウンドの別スレッド(Bio thread pool)にメモリの解放作業を委譲する。メインスレッドはブロックされず、クライアントからのリクエストを処理し続けることができる。
大規模クラスタや、スクリプトキャッシュが肥大化した環境では、常に `ASYNC` を選択すべきである。
—
4. レプリケーションとスクリプトフラッシュの伝播
アーキテクトとして見落としてならないのが、「このコマンドはレプリカにどう伝播するか」 という点だ。
`SCRIPT FLUSH` は、マスターノードで実行されると、そのままレプリカノードへとレプリケーションストリームを通じて送信される(AOFやReplication Link経由)。
[Master] –(SCRIPT FLUSH)–> [Replica 1]
\-> [Replica 2]
なぜこれが重要か?
Luaスクリプトのキャッシュは各Redisインスタンスのローカルメモリ上に独立して存在している。マスター側でスクリプトが破棄されたということは、レプリカ側でもそれらのスクリプトを参照する `EVALSHA` はすべて `NOSCRIPT` エラーを返すようになる必要がある。
Redisはこの整合性を保つために、`SCRIPT FLUSH` を確実に下流へ伝播させる。
—
5. 実務におけるベストプラクティス:いつ、どう使うべきか
シニアエンジニアとして、私は以下の指針をプロダクション環境の運用ルールとして定めている。
1. 動的なスクリプト生成の禁止
アプリケーション層(Ruby, Python, Node.jsなど)で、文字列結合によって毎回異なるLuaスクリプトを動的に生成し、それをRedisに送りつけるような設計は絶対に避けること。スクリプトは静的に固定し、引数(KEYS と ARGV)でデータの差異を吸収せよ。これだけでキャッシュの肥大化は防げる。
2. デプロイパイプラインへの組み込み
新しいバージョンのアプリケーションがデプロイされ、古いLuaスクリプトが二度と使われなくなることが確実な場合、メンテナンスウィンドウやトラフィックの少ない時間帯に `SCRIPT FLUSH ASYNC` を実行し、メモリの断片化とキャッシュ領域をクリーンに保つ。
3. モニタリングの徹底
`INFO memory` や `INFO scripting` を定期的に監視せよ。`used_memory_scripts` のメトリクスが意図せず右肩上がりに上昇している場合、それはアプリケーション層でのスクリプトキャッシュ汚染(リーク)の明確な兆候である。
スクリプト関連の統計情報の確認例
127.0.0.1:6379> INFO scripting
Scripting
lua_calls:1425609
lua_writes:0
lua_eval_calls:0
lua_evalsha_calls:1425609
lua_connected_clients:0
lua_commands_sent:0
lua_lang:Lua 5.1
—
終わりに
`SCRIPT FLUSH` は、単なる「お掃除コマンド」ではない。
Redisという極限のメモリデータベースの内部構造を理解し、そのリソースを限界まで搾り取るためのコントロールパネルの一部である。
コマンドの仕様を知るだけでなく、その背後にあるメモリ管理、スレッドモデル、レプリケーションの整合性までを俯瞰できて初めて、真のRedisエンジニアと呼べる。
コードを書くときは、常にその背後で動くメモリとCPUの挙動に思いを馳せろ。それが、障害のない堅牢なシステムを構築唯一の道である。
コメント