【テクニカル・上級編】 AOF(Append Only File) – Redis

Redis AOFの極限:カーネルをハックし、データロスとレイテンシのジレンマを断つ

こんにちは。大規模分散システムのアーキテクチャの最前線に立ち続けていると、インメモリデータベースであるRedisの「永続化」という古典的かつ本質的なテーマに幾度となく直面する。

世の中の入門書や一般的なドキュメントは、「`appendonly yes` に設定せよ」「`appendfsync everysec` がバランスが良い」といった薄っぺらい表面的な設定論に終始している。だが、高負荷なプロダクション環境において、本当にそれだけで耐えられるか?

断言しよう。カーネルのI/Oサブシステム、ページキャッシュの挙動、そしてRedisのシングルスレッドイベントループとバックグラウンドプロセスの暗闘を理解していなければ、高ス負荷時に突如発生するレイテンシのスパイクや、万が一の障害時のデータ崩壊を防ぐことはできない。

今回は、RedisのAOF(Append Only File)メカニズムの深部へとメスを入れ、実務の限界を突破するための極限の知見を共有する。

—

1. AOFのライフサイクルとI/Oパイプラインの深層

AOFの本質は、クライアントから受け取った書き込みコマンドを、Redis独自のプロトコル(RESP)の形式で順次ファイル末尾に追記していく直截的なログ構造ストレージ(Log-Structured Storage)である。

だが、この「単純な追記」が、OSのカーネル空間とユーザー空間の境界で複雑なドラマを生む。

[Client] —> (Write Command) —> [Redis Event Loop]
|
v
(AOF Buffer in Memory)
|
+—> [write(2)] —> [OS Page Cache]
|
+—> [fdatasync(2)] —> [Disk (NVM/SSD)]

書き込みの三段階ステップ

Redisがコマンドを受け取ってからストレージに永続化されるまでには、明確な3つのフェーズが存在する。

1. コマンドの実行とAOFバッファへのアペンド
コマンドがメモリ上で実行され、成功すると、そのRESP表現が `aof_buf` と呼ばれるインメモリのバッファにバッファリングされる。この時点ではまだシステムコールは発行されていない。
2. `write(2)` システムコールによるカーネル空間への引き渡し
イベントループの毎イテレーションの終わり (`beforeSleep`) に、`aof_buf` の内容が `write(2)` によってカーネルのページキャッシュに書き込まれる。
3. `fsync(2)` / `fdatasync(2)` による物理デバイスへのフラッシュ
ページキャッシュに乗っただけでは、カーネルパニックや電源断(Power Outage)時にデータは消失する。これをハードウェアの不揮発領域へと確実につき落とすのが `fsync` の役割である。

—

2. `appendfsync` の三つの選択肢と、その「隠されたコスト」

設定ファイルに並ぶ三つのオプション。チーフアーキテクトとして、それぞれの実務上の挙動とリスクを残酷なまでの正確さで暴いておこう。

① `appendfsync always` —— 「安全性の幻影」

すべてのコマンドの `write` の後に、同期的かつ厳密に `fsync` を呼ぶ。

  • メリット: 理論上のデータロストはゼロ。
  • 致命的デメリット: ディスクのI/O性能(特にIOPSとレイテンシ)に完全にスループットが支配される。一般的なNVMeであっても、`fsync` の完了を待つたびにコンテキストスイッチとディスクのフラッシュ待ちが発生し、Redisのシングルスレッド性能は数千QPSへと地に落ちる。プロダクション環境でこれを採用していいのは、金融取引のログなど、1バイトの消失も許されない極限のユースケースかつ、専用の超高速PCIeストレージを用意できる場合のみだ。

② `appendfsync everysec` —— 「エンジニアリングの妥協点」

バックグラウンドの別スレッド(POSIXスレッド)が、1秒に1回 `fsync` を実行する。

  • メリット: メインのイベントループをブロックせず、高いスループットを維持しながら、最悪でも過去1秒間のデータ損失にとどめる。
  • 潜むリスク(知られざる真実): ディスクが極度にヘビーなI/O負荷に晒されている場合、バックグラウンドの `fsync` が追いつかなくなる。Redisはレイテンシを守るため、`write(2)` 自体をブロックして `fsync` の完了を待つ挙動をとる。結果として、1秒どころか数秒間、Redis全体がフリーズしたかのようなレイテンシのスパイク(Latency Spike)を引き起こす。

③ `appendfsync no` —— 「カーネルへの完全委任」

Redisは `fsync` を一切呼ばず、OSのカーネルがよしなにページキャッシュをフラッシュするのに任せる。

  • メリット: I/O起因のレイテンシは最小限になる。
  • デメリット: カーネルのデフォルト設定(通常30秒おき等)に依存するため、電源断時のデータ損失リスクが跳ね上がる。通常のWebアプリケーション層のキャッシュとしては許容範囲だが、データストアとして扱うべきではない。

—

3. AOF重力崩壊:AOFリライト(BGREWRITEAOF)の内部メカニズム

AOFを運用する上で避けて通れないのが、ファイルサイズの肥大化だ。同じキーに対して `SET` を100万回繰り返せば、AOFには100万行の履歴が残る。これでは再起動時のリカバリに何時間もかかる。

ここで発動するのが `BGREWRITEAOF` である。内部で何が起きているのか、そのアルゴリズムの妙を紐解こう。

[Parent Process (Redis Main)] [Child Process (Fork)]
| |
|— fork() ———————————>| (Copy-on-Write Memory)
| |
|— AOF Rewrite Buffer (新着書込を蓄積) ——>| (メモリ上の全キーをスキャンし、
| | 最小限の SET/RPUSH コマンド列を生成)
| |
|<-- IPC (書き換え完了通知) -------------------| | | |--- (AOF Rewrite Bufferの内容を追記) ------->| (一時ファイルをアトミックにリネーム)

1. `fork(2)` による子プロセスの生成
親プロセス(メインスレッド)は `fork` を実行し、子プロセスを爆誕させる。この瞬間、Copy-on-Write (CoW) により物理メモリのコピーは発生せず、ページテーブルのみが複製される。
2. メモリ上のデータ構造からの「逆算生成」
子プロセスは、現在のデータベースの全キー空間をイテレートし、「今このデータを再構築するために必要な最小限のコマンド群」を新しいAOFファイルへ書き込んでいく。元の肥大化したAOFファイルは一切見ない。これがミソだ。
3. AOFリライトバッファ(AOF Rewrite Buffer)の同期
`fork` 中および子プロセスが作業している間も、親プロセスは通常のクライアントからの書き込みを受け付け続ける。この「差分」を取りこぼさないため、Redisは「AOFリライトバッファ」を用意し、新しい書き込みをここに蓄積し続ける。
4. 差分のマージとアトミックリネーム
子プロセスがリライトを完了すると、親プロセスにシグナルが送られる。親プロセスは短時間メインループをブロックし、AOFリライトバッファに溜まった差分を新しいAOFファイルの末尾に吐き出し、最後に `rename(2)` システムコールで古いAOFファイルを新しいものへとアトミック(不可分)に置き換える。

この設計により、巨大なデータセットであっても、ダウンタイムゼロでAOFのコンパクト化が完結する。

—

4. 実務で遭遇する「AOF地獄」と限界突破のチューニング指針

現場で数テラバイト級のRedisクラスターを運用してきた中で得た、血と汗の結晶である実践的知見をここに記す。

① `no-appendfsync-on-rewrite` の罠と設定

AOFリライト中(子プロセスがディスクに大量の書き込みを行っている最中)に、親プロセスが `fsync` を実行すると、ストレージのI/O帯域が飽和し、メインスレッドがブロックされてレイテンシが跳ね上がる。
これを防ぐため、Redisには以下の設定が存在する。

AOFリライト中やRDBスナップショット中にfsyncを抑制する
no-appendfsync-on-rewrite yes

  • チーフアーキテクトの警告: `yes` に設定した場合、リライト中の数分間は実質的に `fsync` が行われない状態になる。言い換えれば、この瞬間にサーバーがクラッシュすると、データロストのリスクが急増する。I/O性能の安定(レイテンシの保護)をとるか、堅牢性をとるかのトレードオフをビジネス要件と照らし合わせて判断せよ。

② ディスク容量の枯渇とスタック

AOFが肥大化し、ディスク容量が100%に達した瞬間、Redisは `write(2)` システムコールで `ENOSPC` (No space left on device) エラーを受け取る。
この状態に陥ると、Redisは安全のためにすべての書き込みコマンドの受け付けを拒否し(Read-Onlyモードのようになる)、ログに絶叫を記録し続ける。
対策として、監視ツールで `aof_current_size` とディスクの空地容量を常時監視し、オートスケーリングやアラートの閾値を厳格に設定することは言うまでもないが、あらかじめ `auto-aof-rewrite-percentage` や `auto-aof-rewrite-min-size` を適切にチューニングし、ファイルが肥大化し続ける余地を断つことだ。

最後のAOFサイズから100%増加し、かつ64MB以上になったら自動リライト
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

③ 最新の潮流:RDB-AOFハイブリッド永続化

Redis 4.0以降、私たちは究極の選択を迫られなくなった。それが RDB-AOFハイブリッド永続化(RDB-AOF Hybrid Persistence) である。

aof-use-rdb-preamble yes

この機能を有効にすると、AOFリライトの際に、ファイルの前半部分を「バイナリ形式のRDBスナップショット」として書き込み、後半部分を「それ以降の差分コマンド(AOF)」として追記する形をとる。

  • 再起動速度: RDBの高速なメモリロードの恩恵を受け、数百万件のキーがあっても数秒で起動する。
  • データ安全性: AOFの粒度で差分を保持するため、データロストのリスクを最小限に抑える。

現代のプロダクション環境において、特段の理由がない限り、このハイブリッドモードを有効化しない選択肢は存在しない。

—

5. 結びにかえて

RedisのAOFは、単なる「テキストログの追記」ではない。それは、OSのメモリ管理機構、ファイルシステム、ディスクの物理特性、そしてRedis自身のイベント駆動アーキテクチャの限界点に配慮して精巧に組み上げられた、システムエンジニアリングの芸術品だ。

設定ファイルをコピペして「動いた」と満足するフェーズは終わった。
カーネルが吐き出すI/Oの悲鳴を聞き、メモリとストレージの境界線で何が起きているのかを想像し、自らの手でアーキテクチャを最適化し続けること――それこそが、真のインフラストラクチャ・エンジニアの姿である。

コメント

タイトルとURLをコピーしました