【テクニカル・上級編】 ディスクI/O負荷の影響 – Redis

Redis永続化の暗黒面:ディスクI/O競合という名のシグナルと、限界突破のアーキテクチャ

Redisの真の恐ろしさは、その圧倒的なスループットとミリ秒未満のレイテンシにある。しかし、大規模なデータセットを扱い、数万QPSを超える高負荷環境へ足を踏み入れた途端、エンジニアは避けて通れない壁に直面する。それが「永続化(RDB/AOF)に起因するディスクI/O競合」だ。

「メモリデータベースなのだから、ディスクI/Oなど大した問題ではない」と考えているうちは、ジュニアクラスの域を出ない。本稿では、チーフアーキテクトの視点から、Redisがディスクへデータを書き出す瞬間に何が起きているのか、その低レイヤのメカニズムを解剖し、I/O負荷を極限までコントロールするための実践的アプローチを提示する。

—

1. 内部メカニズムの解剖:なぜ `fork()` と AOF rewrite がシステムを殺すのか

Redisはシングルスレッドモデルでコマンドを処理する。この設計思想が極めて高いCPU効率とロックフリーなメモリ操作を実現しているが、永続化においてはこれが諸刃の剣となる。

COW (Copy-On-Write) とページフォルトの悪夢

RDBの保存(`BGSAVE`)やAOFの重構造化(`BGREWRITEAOF`)を行う際、Redisは親プロセスから `fork()` を呼び出し、子プロセスを生成する。
ここで発生するのが COW (Copy-On-Write) によるメモリオーバーヘッドである。

[Redis Main Process] (Memory: 50GB)
│
├─ fork() ──> [Child Process] (仮想メモリ空間を共有)
│ │
│ └─ 順次ディスクへフラッシュ (RDB/AOF)
│
[Client Write] ──> 該当ページを変更しようとする
│
▼
【OS Kernel】
ページ単位 (4KB / HugePage時は2MB) のコピーが発生
= メモリコピーの間、親プロセスは停止(レイテンシ急増)

書き込みが頻発する(Write-heavyな)ワークロードにおいて、親プロセスが既存のメモリページを書き換えるたびに、OSカーネルは物理メモリのコピーを裏で行う。
特に Transparent Huge Pages (THP) が有効な環境では、1ページが `4KB` から `2MB` に跳ね上がるため、たった1つのバイトを変更するだけでも2MBのメモリブロック全体がコピーされ、OSレベルで深刻なCPUスパイクとレイテンシの劣化(レイテンシ・スパイク)を引き起こす。

fsync の同期的ブロックとカーネルバッファキャッシュ

AOF(Append Only File)において最もクリティカルなのは `fsync` のポリシーだ。
`appendfsync everysec` に設定している場合、バックグラウンドスレッドがディスクへの書き込みを行うが、ディスクのI/O帯域が飽和している、あるいはストレージのI/O待ち(iowait)が蓄積しているとどうなるか?

カーネルのページキャッシュから物理デバイスへのフラッシュが追いつかなくなると、OSの書き込みシステムコール(`write(2)` や `fsync(2)`)そのものがブロックされる。結果として、メインスレッドからのAOF追記処理や、ネットワークI/Oを処理するイベントループ全体が引きずられて停止する。

—

2. 観測:I/Oボトルネックの検知とメトリクスの読み方

「Redisが遅い」と感じたとき、単にCPU使用率を見ていても真の原因にはたどり着けない。以下のメトリクスを監視し、I/O競合の兆候をミリ秒単位で捉える必要がある。

注目すべき `INFO` 出力のキー

永続化関連
rdb_last_bgsave_status:ok
rdb_current_bgsave_time_sec:45 # BGSAVEにかかった時間
aof_enabled:1
aof_rewrite_in_progress:0
aof_delayed_fsync:128 # ← これが「0」以外ならレッドゾーン

統計関連
instantaneous_ops_per_sec:25400
instantaneous_input_kbps:1200.5
instantaneous_output_kbps:4500.2

特に `aof_delayed_fsync` は極めて重要な指標である。これがインクリメントされている場合、OSの `fsync` が完了するのをRedisのバックグラウンドI/Oスレッドが待たされていることを意味し、ストレージのI/Oパフォーマンスがワークロードに完全に負けている証拠だ。

さらに、OS側で `iostat -xz 1` を実行し、以下の状態になっていないか確認せよ。

  • `%util` が 90% 〜 100% に張り付いている
  • `await`(I/O完了までの平均待ち時間)が数十ミリ秒〜数百ミリ秒に達している

—

3. 限界を突破するためのアーキテクチャ対策

このディスクI/O競合とレイテンシの呪縛から逃れるためには、OSレベルの設定からRedisのトポロジ設計まで、多層的な防御(Defense in Depth)が不可欠だ。

1. カーネルパラメータの鉄則設定(OSレイヤ)

まず、Redisを動かすLinuxカーネルの挙動をインメモリデータベース向けに調律する。

Transparent Huge Pages (THP) の完全無効化
(rc.local や systemd 等で確実に適用する)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

overcommit_memory を適切に設定(fork時のメモリ枯渇を防ぐ)
sysctl -w vm.overcommit_memory=1

2. AOF `no-appendfsync-on-rewrite` の活用

AOFの書き換え(`BGREWRITEAOF`)と通常の `fsync` が同時に走ると、同一ファイル(またはディスク)に対するI/O競合が劇的に悪化する。
`redis.conf` で以下の設定を有効化し、リライト中は `fsync` の強制実行を一時的に抑制する。

no-appendfsync-on-rewrite yes

※注: この設定は、万が一のリライト中にクラッシュした場合、最大で数秒分のデータロストリスクが理論上増加するトレードオフを含んでいる。しかし、高スループット環境でのレイテンシ安定化においては極めて有効な布石となる。

3. ストレージの物理的分離とNVMeの採用

HDDはもちろん、低スペックなSATA SSDをRedisの永続化先に選ぶことはアーキテクチャ上の罪悪である。

  • NVMe SSDの採用: IOPSが高く、かつマルチキュー(MQ)に対応した高速なNVMeストレージを使用する。
  • データディレクトリの分離: OSのルートファイルシステムとRedisのAOF/RDB出力先(`dir` ディレクティブ)を物理的、あるいは論理的に完全に分離し、ログ出力やOSの別プロセスによるI/Oノイズをシャットアウトする。

4. 究極の選択:永続化のオフロードとレプリケーション戦略

もし、あなたのシステムが「1秒のデータロスも許されないプライマリDB」ではなく、キャッシュやセッションストア、あるいは堅牢な上流DBを持つリードレプリカであるならば、「Redis単体で永続化を完結させない」というアーキテクチャの決断が必要だ。

  • マスタースレーブの非対称運用:
  • Master: 永続化(RDB/AOF)を完全に無効化(`save “”` 且つ `appendonly no`)。これにより、ディスクI/Oによるレイテンシスパイクをゼロにし、純粋なインメモリの限界性能を引き出す。
  • Replica: 永続化を有効化し、バックアップや障害時のフェイルオーバー用ノードとして割り切る。
  • 専用の非同期バックアップ: データを安全に保持したい場合は、Masterではなくレプリカに対して `BGSAVE` を実行させ、生成された `.rdb` ファイルをS3などのオブジェクトストレージへ非同期で吸い上げるパイプラインを構築する。

—

結言

Redisの永続化におけるディスクI/O負荷は、単なる「ハードウェアの性能不足」で片付けられる問題ではない。それはメモリ管理(COW)、OSスケジューラ、ファイルシステム、そしてRedisのシングルスレッドモデルが織りなす複雑な物理的制約の現れである。

チーフアーキテクトとしての私の警句を最後に残そう。
「速さとは、無駄な処理を削ぎ落とした先にあるのではなく、ボトルネックがどこで発生するかを完全に予測し、制御し切った状態のことだ」

あなたのシステムのRedisが真のパフォーマンスを発揮しているか、今夜もう一度 `INFO` と `iostat` の数値を確認することを勧める。

コメント

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