Redis永続化の急所:`appendfsync` の限界と、I/Oサブシステムの暗部
データベースのアーキテクチャにおいて、永続化とスループットは常にトレードオフの関係にある。Redisはそのインメモリの圧倒的な速度と引き換えに、データの消失リスクとどう向き合うかという古典的かつ本質的な課題を抱えてきた。
AOF(Append Only File)モードにおける `appendfsync` ディレクティブは、そのトレードオフの境界線を引く最もクリティカルな設定である。`always`、`everysec`、`no`。この3つの選択肢は、単なる「設定値」ではない。LinuxカーネルのI/Oサブシステム、VFS(仮想ファイルシステム)、そしてストレージデバイスの物理特性をどこまで理解しているかを問う踏み絵なのだ。
本稿では、教科書的な解説は一切省く。Redisのイベントループ、OSのページキャッシュ、そしてハードウェアの限界点に至るまでの低レイヤの挙動を解体し、真にプロダクションで耐えうる選択基準を提示する。
—
1. `appendfsync` の三相:低レイヤで何が起きているか
AOFファイルへの書き込みは、通常 `write(2)` システムコールを通じて行われる。これは即座にディスクに書き込まれるわけではなく、カーネルのページキャッシュに載るだけだ。このキャッシュを物理メディア(SSD/HDD)に強制フラッシュ(同期)させるタイミングを制御するのが `appendfsync` である。
① `always`:厳格な整合性の代償としてのレイテンシ地獄
トランザクションごとに `fsync(2)`(または `fdatasync(2)`)を発行する。
- 内部挙動:
1. クライアントからの書き込みコマンド受領。
2. メモリ上のデータ構造(Dict等)を更新。
3. AOFバッファにコマンドをアペンド。
4. `write(2)` でOSのページキャッシュへ転送。
5. 即座に `fsync(2)` をコール。
- アーキテクチャ上の問題:
`fsync` は同期的なシステムコールであり、ストレージコントローラがプラッタへの書き込み完了(あるいはSSDのフラッシュコントローラがNANDへの書き込みを確定)を通知するまで、呼び出しスレッドはブロックされる。
さらに最悪なのは、Redisのシングルスレッドイベントループ(Main Event Loop)のメインスレッド自体がこの `fsync` を直叩きする場合、ストレージのレイテンシがそのままRedisのスループット(QPS)の天井になるという点だ。NVMeであっても、高負荷時にはミリ秒単位のスタックが発生し、Redisの本懐であるサブミリ秒の応答速度は完全に崩壊する。
② `everysec`:アーキテクトが選ぶ現実解とそのアキレス腱
バックグラウンドスレッド(別スレッド)が、1秒に1回 `fsync(2)` を実行する。
- 内部挙動:
メインスレッドは `write(2)` のみを行い、非同期でAOFバッファをカーネルに渡すため、レイテンシは最小限に抑えられる。別スレッド(Bio thread: Background I/O thread)が `fsync` を担当する。
- 隠された罠(I/O Stall):
「1秒に1回なら安全かつ高速」と思われがちだが、ここに大きな罠がある。前回の `fsync` から今回の `fsync`までの間に大量の書き込みが発生した場合、カーネルのページキャッシュに溜まったダーティページの量が膨れ上がる。
いざバックグラウンドスレッドが `fsync` を実行する際、OSのカーネル(特にLinuxの `pdflush` や `jbd2` などのサブシステム)は、蓄積された膨大なダーティページをフラッシュするためにストレージへ強烈なI/Oバーストを浴びせる。
これが原因で、メインスレッド側からの `write(2)` すらもカーネル内でブロックされる現象(I/O Stall)が発生する。結果として、`everysec` であっても突発的なレイテンシのスパイク(数千ミリ秒の応答遅延)を完全に排除することはできない。
③ `no`:カーネル任せのギャンブル
`fsync` を明示的に呼ばず、書き込み(`write`)のみを行う。フラッシュのタイミングは完全にOSのカーネルパラメータ(`vm.dirty_background_ratio` や `vm.dirty_ratio`)に委ねられる。
- 内部挙動:
極限までI/O起因のボトルネックを排除できるため、スループットは理論値の限界近くまで引き出される。
- リスク:
OSがクラッシュ、あるいは電源断(Power Outage)が発生した場合、最大でカーネルがフラッシュしていなかった数秒分のデータが不可逆的に消失する。キャッシュやセッションストアのレイヤであれば許容できるケースもあるが、永続データストアとしての信頼性は完全に破綻する。
—
2. AOF Rewrite と `no-appendfsync-on-rewrite` の深い闇
RedisにはAOFファイルの肥大化を防ぐための `BGREWRITEAOF`(AOFリライト)機能が存在する。ここで `appendfsync` の設定と絡む重要なパラメータが `no-appendfsync-on-rewrite` である。
デフォルトは no
no-appendfsync-on-rewrite no
この設定を理解していないエンジニアは非常に多い。
アーキテクチャの衝突:AOF Rewrite vs `fsync`
AOFリライト中、親プロセス(メイン)はデータをメモリ上に維持しつつ、子プロセスがメモリのスナップショットを新しいAOFファイルとしてディスクに吐き出す(Copy-on-Write機構を利用)。
同時に、親プロセスには新しいクライアントからの書き込みが継続し、古いAOFファイルへの追記(`write` と `fsync`)も走り続ける。
もし `appendfsync` が `always` または `everysec` に設定されている場合、以下のリソース競合が発生する。
- 子プロセス:新しいAOFファイルを生成するために大量のシーケンシャルI/Oを消費。
- バックグラウンドI/Oスレッド(またはメインスレッド):古いAOFファイルに対して頻繁な `fsync` を実行。
結果として、ディスクのI/O帯域が飽和し、リライト処理自体が極端に遅延するか、最悪の場合はRedis全体が数秒〜数十秒間フリーズする。
`no-appendfsync-on-rewrite yes` の是非
このパラメータを `yes` にすると、AOFリライト(あるいはRDBの保存)が実行されている間、Redisは `fsync` の実行を一時的に停止する(実質的に `no` の状態にする)。
これによりI/O競合は回避され、リライト中のレイテンシ悪化を防ぐことができる。
- トレードオフ:
リライト中にサーバーがクラッシュした場合、データロスのウィンドウが広がるリスクがある。大規模なデータセット(数十GB〜数百GB)を扱うアーキテクチャでは、リライトそのものが数分単位で完了しないため、この設定の有無は障害時のRPO(目標復旧時点)に直結する。
—
3. チーフアーキテクトが推奨するプロダクション設定の極意
机上の空論を排し、現場の戦場で培った実践的な選択基準を提示する。
パターンA:極限のレイテンシ重視(キャッシュ、セッション、リーダーボード)
データが揮発しても上流のDB(RDBやNoSQL)から再構築が可能、あるいはビジネス上数秒のデータロスのリスクが許容される場合。
appendonly yes
appendfsync everysec
no-appendfsync-on-rewrite yes
- 解説: 限界までレイテンシの安定性を高めつつ、最悪のロスの範囲を1秒に抑え込む。`no-appendfsync-on-rewrite yes` を有効化し、大規模データセットにおけるリライト時のI/Oストールを防ぐことが肝要である。
パターンB:金融・決済・ステート管理(データロス絶対許容不可)
コンシステンシーが最優先であり、データの巻き戻しや喪失が許されない場合。
appendonly yes
appendfsync always
no-appendfsync-on-rewrite no
- 解説: この構成を選択する場合、ソフトウェア側の最適化だけでは不十分である。以下のハードウェア要件を強制しなければならない。
1. BBU(Battery Backup Unit)付きRAIDコントローラ または PLP(Power Loss Protection)を搭載したエンタープライズ向けNVMe SSDを使用すること。
2. ストレージ側で `fsync` の実体をキャッシュせず、不揮発性領域への書き込み確定を保証するハードウェアでなければ、`always` のオーバーヘッドを払う意味が失われる(単に遅いだけのシステムになる)。
—
4. 結び:監視すべきメトリクス
`appendfsync` の挙動をチューニングする際、感覚で設定を行ってはならない。以下のRedisメトリクスを必ずモニタリング基盤(Prometheus + Grafana等)で監視し続けろ。
- `instantaneous_ops_sec`: 現在のQPS
- `aof_pending_fsync`: ディスクへのフラッシュを待っているAOFバッファの数(これが常に数千を超えるようであれば、ストレージのI/O性能が限界を迎えているか、`always`/`everysec` の負荷に耐えられていない)
- `delayed_fsync`: `fsync` の完了待ちが原因で書き込みが遅延した累積回数。この値が右肩上がりに増加している場合、即座にストレージのI/O拡張、あるいは設定の再考が必要となる。
低レイヤの物理制約を無視したデータベース設計は、いつか必ず本番環境のピークタイムに牙を剥く。Redisのメモリ上の美しさを支えるのは、泥臭いディスクI/Oの制御そのものなのだ。
コメント