やあ、現場で戦うエンジニア諸君。今日もRedisのシングルスレッドの限界に挑んでいることだろう。
今回は、Redisの永続化において、多くのエンジニアが「なんとなく」で済ませ、そして本番環境の大規模負荷時に手痛いしっぺ返しを食らう箇所——AOFリライトバッファ(AOF Rewrite Buffer)について深く切り込む。
「AOFリライト中に書き込まれたデータを一時的に保持する場所でしょ?」という程度の理解なら、今すぐその認識をアップデートしてほしい。その「一時的な保持」が、メモリを食いつぶし、レイテンシを跳ね上げ、最悪のケースではインスタンスを死に至らしめるからだ。
アーキテクトの視点から、このブラックボックスを解剖していこう。
—
1. なぜ「リライトバッファ」という概念が必要なのか
RedisのAOF(Append Only File)は、すべての書き込み操作をログとして記録する。当然、運用時間が長くなればファイルサイズは肥大化する。これを再構築(Rewrite)して、現在のデータ状態を表現する最小限のコマンド群に圧縮するのが `BGREWRITEAOF` だ。
ここでエンジニアが直面する論理的矛盾がある。
「バックグラウンドでリライトを実行している最中も、クライアントからの書き込みは止まらない」ということだ。
1. 子プロセスが「その時点の」スナップショットをディスクに書き出す。
2. その間、親プロセス(メインスレッド)には「新しい」書き込みが届き続ける。
3. リライトが終わった瞬間、子プロセスが作ったファイルには「リライト中の書き込み」が欠落している。
この欠落を埋めるためのピースこそが、AOFリライトバッファだ。
—
2. 【深淵】Redis 7.0以前の「メモリ増幅」の罠
まずは歴史を知る必要がある。Redis 6.2以前、このリライトバッファは純粋にメモリ上の領域として実装されていた。これが悪名高き「メモリ使用量の急増」を招く原因だ。
リライト中、親プロセスは「通常のAOFバッファ」と「AOFリライトバッファ」の両方に同じ書き込み内容をコピーする。
/ Redis 6.x 以前の概念的な動き /
void feedAppendOnlyFile(struct redisCommand cmd, int dictid, robj objv, int objc) {
// 1. 通常のAOFバッファへ書き込み
if (server.aof_state == AOF_ON)
aofRewriteBufferAppend((unsigned char)buf,sdslen(buf));
// 2. リライト中なら、リライトバッファへも書き込み(二重管理!)
if (server.aof_child_pid != -1)
aofRewriteBufferAppend((unsigned char)buf,sdslen(buf));
}
ここに潜むリスク:
- メモリ二重消費: 高負荷な書き込みが続く中でリライトが走ると、リライトバッファが肥大化し、OSのCopy-on-Write(CoW)と相まって一気にメモリ不足(OOM)に陥る。
- CPUバースト: リライト終了の最終段階で、親プロセスはこの巨大なバッファを子プロセスにパイプ経由で流し込み、同期する必要がある。この間、Redisはブロックされ、レイテンシがスパイクする。
—
3. 【革命】Redis 7.0:Multi-Part AOFによるパラダイムシフト
もし君のプロジェクトがまだ古いRedisを使っているなら、今すぐバージョンアップを検討すべき理由がこれだ。Redis 7.0から導入された Multi-Part AOF は、この「リライトバッファ」という概念を劇的に変えた。
もはや、リライト中の書き込みをメモリ上のバッファに溜め込む必要はなくなったのだ。
仕組みの変遷:
- Base File: 子プロセスが書き出す、ある時点の全データ。
- Incr File (Incremental): リライト開始「後」の書き込みを記録する新しいAOFファイル。
リライトが開始されると、Redisは新しい「インクリメンタルAOFファイル」を生成し、以降の書き込みを直接そこへ流し込む。リライトが完了すると、古いBaseファイルと古いIncrファイルを破棄し、新しいBaseと新しいIncrをマニフェストファイルで管理する。
「リライト完了後にバッファを追記する」という重い同期処理そのものが消滅したのだ。 これはアーキテクチャ上の大きな勝利と言える。
—
4. 実務での設計・設定指針
理屈がわかったところで、我々エンジニアが設定ファイル(`redis.conf`)をどう攻めるべきか解説する。
AOFリライトのトリガー設定
リライトバッファ(あるいは新しいIncrファイル)への負荷を抑えるには、リライトの頻度を最適化する必要がある。
前回の完了時サイズから何%増えたら実行するか
auto-aof-rewrite-percentage 100
そもそもファイルが小さいうちはリライトしない(無駄なI/Oを避ける)
auto-aof-rewrite-min-size 64mb
リードのアドバイス:
大規模環境では `64mb` は小さすぎる。数GB単位(例: `4gb`)に設定し、ディスクI/Oの頻度を下げ、リライトが「本当に必要な時だけ」走るように設計せよ。
書き込み時の一貫性とパフォーマンス
リライト中にfsyncを強制するか?
noにするとパフォーマンスは上がるが、クラッシュ時にリライト中のデータが飛ぶリスクがある
no-appendfsync-on-rewrite no
高負荷な書き込みシステムで「ディスクI/OがボトルネックでRedisのレスポンスが遅い」という報告を受けたら、まずここを疑え。`yes` に設定することで、リライト中の `fsync` を抑制し、スループットを稼ぐことができる。ただし、これは「背中を預ける勇気」が必要な設定だ。
—
5. 運用監視の「極意」
リライトバッファの状態を監視せずして、安定運用は語れない。`INFO persistence` コマンドの結果を凝視してほしい。
Redis 7.x 以降の例
127.0.0.1:6379> INFO persistence
…
aof_rewrite_in_progress:0 # 1なら現在リライト中。要注意。
aof_last_rewrite_time_sec:15 # リライトにかかった時間。長すぎるならI/O不足。
aof_current_size:5368709120 # 現在のAOFサイズ。
aof_base_size:4294967296 # リライト後のベースサイズ。
もし `aof_rewrite_in_progress` が 1 の時間が異常に長いなら、その間、システムは常に「リライトに伴う追加負荷(CoWやディスクI/O)」に晒されていることを意味する。
—
結論:チーフアーキテクトからの伝言
AOFリライトバッファは、Redisの「一貫性」と「可用性」が激突する最前線だ。
1. Redis 7.0以降を強く推奨する。 メモリ上のリライトバッファという爆弾を抱える時代は終わった。Multi-Part AOFによる恩恵を享受せよ。
2. メモリ設計には「遊び」を持たせろ。 CoW(Copy-on-Write)の発生を考慮し、物理メモリの 50〜70% 程度を上限として運用するのが鉄則だ。
3. ディスクI/Oを甘く見るな。 Redisはメモリ内データベースだが、永続化を有効にした瞬間、その命運はストレージのIOPSに握られる。
「ただ動く設定」ではなく、「牙を剥いた負荷に耐えうる設定」を。
それが、我々エンジニアがコードとインフラに込めるべき魂だ。
健闘を祈る。
コメント