Redis 永続化:RDB、AOF とデータ損失
1. 概要
Redis はすべてのデータを RAM に保持します。プロセスが終了した場合、Redis が事前にディスクに書き込んでいた場合にのみデータが保持されます。この記事では、RDB(スナップショット)と AOF(append-only ファイル)という 2 つの永続化メカニズム、それらの選択と設定方法について説明します。また、永続化を有効にしてもデータが失われる状況もリストアップしています。記事の最後には、永続化ステータスを確認する Spring Boot コードと、Testcontainers で実行するテストがあります。
2. RDB:定期的なスナップショット
RDB はデータセット全体を特定の時点で、デフォルトは dump.rdb という名前のバイナリファイルに書き込みます。redis.conf での設定:
# 3600秒後に最低1つの変更、
# 300秒後に最低100の変更、または60秒後に最低10000の変更がある場合にスナップショット
save 3600 1 300 100 60 10000
dbfilename dump.rdb
dir /data
# 最後の BGSAVE が失敗した場合、書き込みコマンドを拒否
stop-writes-on-bgsave-error yes
rdbcompression yes
save 条件が満たされると、Redis は fork() を呼び出します。子プロセスはスナップショットを一時ファイルに書き込み、その後 dump.rdb に名前変更します。親プロセスは通常どおりクライアントにサービスを提供し続けます。オペレーティングシステムのコピーオンライト機構のおかげで、両プロセスはメモリページを共有します。親プロセスがページを変更したときだけ、そのページがコピーされます。
スナップショットを手動で取得することもできます:
redis-cli BGSAVE
redis-cli LASTSAVE
Background saving started
(integer) 1760169600
BGSAVE はバックグラウンドで実行されます。LASTSAVE は最後に成功したスナップショットの Unix タイムスタンプを返します。本番環境では SAVE を使用しないでください。このコマンドは書き込みが完了するまでサーバー全体をブロックします。
RDB の利点:
- ファイルが小さく、バックアップやその他の環境へのコピーに非常に適しています。
- 再起動が速い。Redis はバイナリファイル 1 つをロードするだけです。
- スナップショット間のスループットへの影響はほぼありません。
欠点: 最後のスナップショット後の変更はすべて、Redis がクラッシュした場合に失われます。上記の設定では、最悪の場合、数分間のデータを失う可能性があります。
注意: 大規模なデータセット(数十 GB)での
fork()は、Redis を数百ミリ秒の間ハング状態にする可能性があります。書き込みが多い場合、コピーオンライトによって RAM がほぼ 2 倍になる可能性があります。メモリに余裕を持たせ、Linux でvm.overcommit_memory = 1を有効にしてください。
3. AOF:各書き込みコマンドを記録
AOF はデータを変更するすべてのコマンド(SET、INCR、HSET など)をログファイルの末尾に書き込みます。起動時、Redis はこのログを実行してデータセットを再構築します。
appendonly yes
appenddirname "appendonlydir"
appendfilename "appendonly.aof"
# always | everysec | no
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes
3.1. appendfsync ポリシー
ファイルへの書き込みは、データがディスク上にあることを意味しません。データは fsync() コマンドが実行されるまで、カーネルのページキャッシュに残る可能性があります。ここがあなたがクラッシュ時に失うデータの量を決定する重要な点です:
| 値 | fsync タイミング | クラッシュ時の最大損失 | パフォーマンス |
|---|---|---|---|
always |
各書き込みコマンド後 | ほぼ損失なし | 最も遅い |
everysec |
毎秒、バックグラウンドスレッドで | 約 1 秒(最大 2 秒) | 良好、デフォルト |
no |
OS が決定、通常約 30 秒 | 数十秒 | 最も速い |
everysec では、前の fsync がディスクが遅いため 2 秒後も完了していない場合、Redis は書き込みコマンドをブロックして、さらなる遅延を避けます。したがって、実際のデータ損失ウィンドウは 1 秒ではなく 2 秒である可能性があります。
また、2 つのタイプの障害を区別する必要があります。Redis プロセスだけがクラッシュする場合、write() されたデータはまだページキャッシュに残っており、カーネルが後でディスクに書き込みます。マシン全体が停電したりカーネルがパニックになった場合、fsync されていない部分は失われます。
3.2. AOF rewrite とマルチパート AOF
AOF はファイルサイズが時間とともに増加します。例えば、100 万個の INCR counter コマンドは単一の SET counter 1000000 コマンドに置き換えることができます。Redis はファイルが前回の rewrite のサイズの 2 倍になった場合(auto-aof-rewrite-percentage 100)および最小 64 MB に達したときに、AOF を自動的に rewrite します。
Redis 7 以降、AOF は appendonlydir ディレクトリ内にあり、複数のファイルで構成されます:
appendonlydir/
├── appendonly.aof.1.base.rdb # base、aof-use-rdb-preamble のおかげで RDB 形式
├── appendonly.aof.1.incr.aof # base 後に生成されたコマンド
└── appendonly.aof.manifest # 現在の AOF に属するファイルを説明
aof-use-rdb-preamble yes を有効にすると、base ファイルは RDB 形式で書き込まれるため、ロードが高速です。incr 部分はまだコマンドログなので、データ損失が少なくなります。これは「ハイブリッド」モードであり、Redis 4.0 以降のデフォルトです。
4. どのモードを選択すべきか?
| 要件 | 推奨設定 |
|---|---|
| 純粋なキャッシュ、DB から再構築可能なデータ | 両方無効:save ""、appendonly no |
| 数分のデータ損失を許容、コンパクトなバックアップが必要 | RDB のみ |
| セッション、ショッピングカート、レート制限に信頼性が必要 | AOF everysec + RDB(バックアップ用) |
| 1 つのコマンドも失わない(非常にまれ) | AOF always、ただし別の DB の使用を検討してください |
ほとんどのアプリケーションに合理的な選択は、両方を有効にすることです:データ損失を減らすための AOF everysec と、定期的なバックアップファイル用の RDB。両方が有効な場合、Redis は起動時に AOF を優先してロードします。これは AOF がより完全であるためです。
注意: Redis をキャッシュとしてのみ使用している場合は、永続化を無効にしてください。キャッシュに対して RDB を有効にすると、
fork()、ディスク I/O、RAM を追加して消費するだけで、利点はありません。
5. Spring Boot アプリケーションから永続化を確認
依存関係を追加します:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
次のクラスは永続化ステータスを読み取り、スナップショットを要求し、レプリカの確認を待ちます:
@Component
public class PersistenceInspector {
private final StringRedisTemplate template;
public PersistenceInspector(StringRedisTemplate template) {
this.template = template;
}
public Properties persistenceInfo() {
return template.execute((RedisCallback<Properties>) conn ->
conn.serverCommands().info("persistence"));
}
public void triggerSnapshot() {
template.execute((RedisCallback<Object>) conn ->
conn.execute("BGSAVE", "SCHEDULE".getBytes()));
}
public long waitForReplicas(int replicas, long timeoutMs) {
Long acked = template.execute((RedisCallback<Long>) conn ->
(Long) conn.execute("WAIT",
String.valueOf(replicas).getBytes(),
String.valueOf(timeoutMs).getBytes()));
return acked == null ? 0 : acked;
}
}
各メソッドの説明:
persistenceInfo()はINFO persistenceを呼び出し、aof_enabled、rdb_last_bgsave_statusなどのフィールドを含むPropertiesを返します。triggerSnapshot()はBGSAVE SCHEDULEを使用します。AOF rewrite が実行中の場合、Redis はエラーを返す代わりにスナップショットをスケジュールします。waitForReplicas()はWAITを呼び出し、以前の書き込みコマンドを受け取ったレプリカの数を返します。セクション 6 がこのコマンドが必要な理由を説明します。
INFO persistence の出力の一部(AOF が有効な場合):
loading:0
rdb_changes_since_last_save:42
rdb_bgsave_in_progress:0
rdb_last_save_time:1760169600
rdb_last_bgsave_status:ok
aof_enabled:1
aof_rewrite_in_progress:0
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
監視に含めてアラートを設定すべき 3 つのフィールド(値が ok 以外の場合):rdb_last_bgsave_status、aof_last_write_status、aof_last_bgrewrite_status。フィールド rdb_changes_since_last_save は、RDB だけに依存している場合に失われるデータ量を示します。
5.1. Testcontainers でのテスト
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class RedisPersistenceTest {
@Container
@ServiceConnection(name = "redis")
static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.4"))
.withExposedPorts(6379)
.withCommand("redis-server", "--appendonly", "yes", "--appendfsync", "everysec");
@Autowired
StringRedisTemplate template;
@Autowired
PersistenceInspector inspector;
@Test
void aofShouldBeEnabledWithEverySecPolicy() {
template.opsForValue().set("order:1001", "PAID");
Properties info = inspector.persistenceInfo();
List<String> fsync = template.execute((RedisCallback<List<String>>) conn ->
conn.serverCommands().getConfig("appendfsync")
.values().stream().map(Object::toString).toList());
assertThat(info.getProperty("aof_enabled")).isEqualTo("1");
assertThat(info.getProperty("aof_last_write_status")).isEqualTo("ok");
assertThat(fsync).containsExactly("everysec");
}
@Test
void backgroundSnapshotShouldSucceed() throws InterruptedException {
template.opsForValue().set("customer:42", "Lan");
long before = Long.parseLong(inspector.persistenceInfo().getProperty("rdb_last_save_time"));
Thread.sleep(1100); // タイムスタンプが新しく異なることを確認
inspector.triggerSnapshot();
Properties info = inspector.persistenceInfo();
for (int i = 0; i < 50 && Long.parseLong(info.getProperty("rdb_last_save_time")) <= before; i++) {
Thread.sleep(100);
info = inspector.persistenceInfo();
}
assertThat(info.getProperty("rdb_last_bgsave_status")).isEqualTo("ok");
assertThat(Long.parseLong(info.getProperty("rdb_last_save_time"))).isGreaterThan(before);
}
}
最初のテストは、AOF everysec で Redis 7.4 を起動し、設定が有効であることを確認します。@ServiceConnection は自動的に spring.data.redis.host/port をコンテナにポイントするため、追加のプロパティ設定は不要です。2 番目のテストは rdb_last_save_time が増加するまで最大 5 秒間待機し、スナップショットが成功したことを確認します。
6. いつデータを失いますか?
永続化を有効にするだけでは不十分です。本番環境で最も一般的な状況は以下の通りです。
1. 2 つのスナップショット間のクラッシュ。 RDB のみを使用している場合、rdb_last_save_time 後に書き込まれたすべての内容を失います。
2. fsync ウィンドウ。 AOF everysec では、マシンがダウンした場合、約 1~2 秒を失います。no では、この数値は数十秒まで上昇する可能性があります。
3. 非同期レプリケーションでのフェイルオーバー。 マスターはレプリカがコマンドを受け取る前に、クライアントに OK を返します。マスターが die し、Sentinel またはクラスターがレプリカをマスターに昇格させる場合、複製されなかったコマンドはすべて失われます。これは、マスターが AOF always を持っている場合でも発生します。WAIT はこのリスクを軽減するのに役立ちます:
template.opsForValue().set("payment:7788", "CAPTURED");
long acked = inspector.waitForReplicas(1, 100);
if (acked < 1) {
log.warn("payment:7788 はレプリカによって確認されていません");
}
WAIT 1 100 は、最大 100ms の間、少なくとも 1 つのレプリカが確認されるまでブロックします。WAIT はまだ Redis を strongly consistent なシステムにはしません:レプリカはコマンドを受け取ることができますが、fsync されていない可能性があります。これはデータ損失の確率を減らす方法と考えてください。保証ではありません。
4. マスターが空のデータセットで再起動。 マスターが永続化を無効にしていると仮定し、自動的に再起動し、空のデータで起動します。レプリカはマスターと同期し、独自のデータをクリアします。マスターが永続化を有効にしていない場合は、自動再起動を許可しないでください。
5. コンテナが volume をマウントしていない。 Redis は引き続き dump.rdb と AOF を書き込みますが、コンテナのファイルシステムにです。コンテナが削除されて再作成されると、これらのファイルは失われます。
services:
redis:
image: redis:7.4
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
volumes:
redis-data:
Kubernetes では、同等の状況は PersistentVolumeClaim の代わりに emptyDir を使用することです。
6. ディスク満杯または fork() 失敗。 BGSAVE が失敗し、stop-writes-on-bgsave-error yes が有効になっている場合、Redis はエラー MISCONF ですべての書き込みコマンドを拒否します。この動作はデータを保護しますが、アプリケーションをエラーにします。多くの人々がこのオプションを無効にして応答し、その後 Redis は永続化なしで実行を続けます。
7. FLUSHALL を実行してしまった。 AOF では、FLUSHALL コマンドはファイルの末尾にある 1 行です。AOF がまだ rewrite されていない場合、Redis を停止し、その行を削除して、再起動できます。rewrite が実行されている場合は、回復方法がありません。このため、本番環境では ACL を使用してこのコマンドの名前を変更またはブロックしてください。
8. Eviction。 これは永続化の欠陥ではありませんが、結果は同じです。maxmemory-policy allkeys-lru では、Redis は RAM が満杯の場合、キーを削除します。失われてはいけないデータには noeviction が必要です。
注意: Redis を金銭やオーダーのデータの唯一のストレージとして使用しないでください。「OK を返したら失われない」ことを保証する必要がある場合は、関係データベースまたはコンセンサスを持つシステムを使用してください。Redis はキャッシュまたは高速化層として機能すべきです。
7. まとめ
RDB はコンパクトなバックアップファイルと高速な起動を提供しますが、最大数分間のデータを失う可能性があります。everysec 付きの AOF はこの損失を約 1~2 秒に減らしますが、追加の I/O がかかります。ハイブリッドモード(aof-use-rdb-preamble)は両方の利点を組み合わせています。実際のリスクの大部分は、これら 2 つのメカニズムの外にあります:非同期レプリケーション、マウントされていない volume、ディスク満杯、FLUSHALL と eviction。INFO persistence のフィールドを監視し、重要な書き込みコマンドに WAIT を使用し、セクション 5 のような Testcontainers で設定をテストしてください。