vault backup: 2026-05-22 00:25:59

This commit is contained in:
hhs
2026-05-22 00:25:59 +08:00
parent 9cb53bad9c
commit dda0d6927c
3 changed files with 888 additions and 372 deletions
+171 -78
View File
@@ -7,7 +7,12 @@ create time: 2026-05-15 18:13
## 概述
AOF(Append Only File)以**追加写日志**的方式记录每个写操作。相比 RDB 的快照式恢复,AOF 提供更高的数据安全性——你可以根据策略做到**不丢数据、丢 1 秒、或丢 N 条**。代价是文件更大、恢复更慢。
AOF(Append Only File)以**追加写日志**的方式记录每个写操作。如果你用过 MySQL 的 binlog 或 Kafka 的 commit log,这个思想是一样的:**不存最终状态,而是存每一步变化过程**。
相比 [[hhs/Redis/04-RDB持久化|RDB]] 的快照式恢复,AOF 提供更高的数据安全性——你可以根据策略做到**不丢数据、丢 1 秒、或丢 N 条**。代价是文件更大、恢复更慢。
> [!question] RDB 已经能持久化了,为什么还需要 AOF?
> RDB 是「拍照」——每隔 N 秒拍一张快照,两次拍照之间的数据变化全部丢失。AOF 是「录像」——每个写操作都被记录,最多只丢最后一次 fsync 之后的操作。打个比方:RDB 好比每小时存一次文档,AOF 好比开了自动保存。
## 核心机制
@@ -16,19 +21,32 @@ AOF(Append Only File)以**追加写日志**的方式记录每个写操作。
```mermaid
flowchart LR
Client["客户端写入"] --> Server["Redis 主线程<br/>执行命令 + 返回结果"]
Server --> Buffer["aof_buffer<br/>内核/用户态缓冲区"]
Buffer --> FSyncPolicy{"刷新策略?"}
Server --> Buffer["aof_buf<br/>用户态缓冲区"]
Buffer --> FSyncPolicy{"appendfsync 策略?"}
FSyncPolicy -->|everysec| KernelBuf["操作系统页缓存"]
KernelBuf --> Disk["磁盘 aof 文件"]
KernelBuf --> Disk["磁盘 AOF 文件"]
FSyncPolicy -->|always| Disk
FSyncPolicy -->|no| OSPage["交给 OS 自己 flush"]
FSyncPolicy -->|no| OSPage["交给操作系统自行 flush"]
style Server fill:#f9d,stroke:#96c
style Disk fill:#dfd,stroke:#6c6
```
> [!TIP] 一条命令是怎么落到磁盘的?
> 1. 客户端发送命令 → 2. Redis 在主线程执行并返回结果 → 3. 同时将命令追加到 `aof_buffer` → 4. 根据 `appendfsync` 策略最终刷入磁盘。整个过程对主线程几乎是异步的。
> [!tip] 一条命令是怎么落到磁盘的?
> 1. 客户端发送 `SET mykey hello` → 2. Redis 主线程执行命令、更新内存、返回 OK → 3. **同时**将命令以 RESP 协议格式追加到 `aof_buf`(用户态缓冲区) → 4. 根据 `appendfsync` 策略调用 `fsync()` 最终刷入磁盘
>
> 注意步骤 2 和 3 是同步的,但步骤 4 是异步的——这意味着**命令返回成功 ≠ 数据落盘**。
实际落盘的 AOF 文件长这样(RESP 协议格式):
```
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nhello\r\n
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nworld\r\n
*2\r\n$3\r\nDEL\r\n$5\r\nmykey\r\n
```
> [!question] 为什么 AOF 用 RESP 协议而不是人类可读的纯文本?
> RESP 是 Redis 自身的通信协议,选择它的原因很简单:**不需要额外的序列化/反序列化开销**,Redis 直接把发给客户端的响应原样写入 AOF,读取时也直接用 RESP 解析器回放。这比发明一套新格式要高效得多。
### 刷新策略配置
@@ -38,18 +56,53 @@ appendfsync everysec # 每秒 fsync <- 推荐(最佳平衡)
# appendfsync no # 完全依赖 OS -> 性能最好,风险最高
```
| 策略 | 丢失风险 | 吞吐量 (ops/s) | 适用场景 |
|------|---------|---------------|---------|
| `always` | 零丢失 | ~900 | 金融级要求(极少用) |
| `everysec` | 最多丢 1 秒 | ~74k | **生产标准配置** |
| `no` | OS 决定,可能丢大量 | ~93k | 可接受全量丢失的场景 |
在理解这三种策略之前,先搞清楚 `fsync` 到底在做什么:
> [!TIP] everysec 的性能真相
> `everysec` 不会阻塞主线程——它把 fsync 放到单独的后台线程。即使某个 fsync 花了 2s(磁盘卡顿),也只是这一秒的写入被推迟到下一周期,不会影响主线程。
> [!info] fsync 的本质:把"快递柜"里的包裹真正送到你手里
> 当 Redis 调用 `write()` 时,数据只是从用户态拷贝到了**内核页缓存**(类似放进了快递柜),操作系统会择机将其刷到物理磁盘。而 `fsync()` 是强制要求操作系统**立即**把页缓存的数据持久化到磁盘——相当于要求快递柜立即派送。
>
> 如果不调用 `fsync()`,数据在页缓存中时如果发生断电,这些数据就丢了。
理解了 fsync,三种策略的区别就一目了然:
| 策略 | fsync 时机 | 丢失风险 | 吞吐量 (ops/s) | 适用场景 |
|------|-----------|---------|---------------|---------|
| `always` | 每条写命令后 | **零丢失** | ~900 | 金融级要求(极少用) |
| `everysec` | 每秒一次(后台线程) | 最多丢 ~1 秒 | ~74k | **生产标准配置** |
| `no` | 从不主动 fsync,交给 OS | OS 决定,可能丢 30 秒+ | ~93k | 可接受数据丢失的场景 |
> [!warning] everysec "最多丢 1 秒" 是有前提条件的
> 这个说法成立的前提是 `fsync()` 能在 1 秒内完成。如果磁盘 IO 阻塞(比如同一块盘上跑着其他 IO 密集任务),一次 `fsync()` 可能卡住几秒甚至十几秒——此时未 fsync 的数据量会持续累积,丢失窗口也随之扩大。不过好在 Redis 的 `everysec` 模式下,`fsync()` 在**后台线程**执行,不会阻塞主线程处理新请求。
> [!tip] 为什么 always 这么慢?
> `always` 是在**主线程**中同步调用 `fsync()`。一次 `fsync()` 耗时约 0.2~2ms(取决于磁盘),这意味着每条写命令都要等磁盘确认,吞吐量直接从万级跌到百级——这就是"数据安全 vs 性能"的经典权衡。
## AOF 重写(Rewrite)
AOF 文件会不断增长,需要定期重写来去除无效命令(如 key 被 DEL 后又 SET)。
AOF 文件只会**追加**、永远不会自动清理。时间一长,大量"垃圾命令"会让文件体积失控:
```mermaid
flowchart LR
subgraph AOF_LOG["appendonly.aof(实际内容)"]
CMD1["SET counter 0"]
CMD2["SET counter 1"]
CMD3["SET counter 2"]
CMD4["INCR counter"]
CMD5["DEL temp_key"]
CMD6["SET temp_key abc"]
CMD7["DEL temp_key"]
end
subgraph REALITY["内存中的最终状态"]
MEM["counter = 3<br/>temp_key 不存在"]
end
AOF_LOG -.->|"7 条命令 → 真实状态只有 1 条"| REALITY
style AOF_LOG fill:#fdd,stroke:#c66
style REALITY fill:#dfd,stroke:#090
```
> [!question] 为什么不直接"删除"过期命令?
> AOF 是顺序追加的日志文件,就像流水账本——你不能从账本中间撕掉一页而不影响后面所有页码。**只能"重写":把当前内存的真实状态重新写一份新的、精简的 AOF 文件来替换旧文件。**
### 重写触发条件
@@ -71,85 +124,106 @@ auto-aof-rewrite-min-size 64mb # AOF 最小绝对大小(避免刚启动就
### 重写过程(类似 RDB fork)
重写并不是简单地"删减旧 AOF",而是**从内存中的真实数据出发,生成全新的精简 AOF 文件**。整个过程分三步:
```mermaid
sequenceDiagram
participant S as Redis Server
participant P as 主进程
child CG as Rewriter Child
participant C as 重写子进程
participant Log as appendonly.aof
participant NewLog as appendonly.aof_rewrite_tmp
end
participant P as "主进程(继续服务)"
participant C as "重写子进程"
participant Old as "appendonly.aof(旧)"
participant Tmp as "aof_rewrite_tmp(新)"
participant Buf as "aof_rewrite_buf"
S->>P: BGREWRITEAOF
P->>S: OK
P->>C: fork() 子进程
C->>NewLog: fork 时刻起遍历内存 -> 重写为精简命令
Note over P,C: Main process continues serving requests
loop 每个新写命令
P->>Log: 追加原始命令
P->>C: 同时写入 aof_rewrite_buf<br/>(重写期间的新操作)
end
C->>NewLog: 追加 aof_rewrite_buf 中的内容
C->>P: 完成
P->>Log: rename(原 -> 旧.bak, 新 -> 当前)
P->>P: "收到 BGREWRITEAOF 命令"
P->>C: "fork() 创建子进程"
Note over C: "第一步:遍历内存所有 key<br/>生成精简命令写入新文件"
C->>Tmp: "SET key1 val1<br/>SET key2 val2<br/>HSET hash f1 v1 ..."
Note over P: "主进程继续正常处理请求"
P->>Old: "新命令照常追加到旧 AOF"
P->>Buf: "同时复制一份到重写缓冲区"
Note over C: "第二步:内存遍历完成"
C->>Buf: "请求主进程发送缓冲区内容"
Note over C: "第三步:把重写期间的新命令追加到新文件末尾"
Buf->>Tmp: "追加增量命令"
C->>P: "重写完成通知主进程"
P->>P: "原子 rename:旧文件 → .bak,新文件 → appendonly.aof"
```
> [!WARNING] 重写的内存开销
> fork 后重写子进程也使用 COW 机制。如果你的 AOF 正在持续增长且写入量大,可能需要评估可用内存是否足够支撑 fork 的 COW 副本。在 100GB+ 的 Redis 实例上,一次 fork 可能导致额外的几 GB 内存占用。
> [!tip] 关键理解:为什么需要"重写缓冲区"?
> 子进程在遍历内存生成新 AOF 的过程中(可能持续几十秒甚至几分钟),主进程还在不断接收新写命令。这些"重写期间的增量命令"被暂存在 `aof_rewrite_buf` 中,等子进程遍历完内存后,再将这些增量追加到新文件末尾——这样才能保证新 AOF 不丢任何数据。
> [!warning] 重写的内存开销
> fork 后重写子进程使用 COW(Copy-On-Write)机制,和 RDB 的 BGSAVE 一样。如果你的实例内存很大(100GB+)且写入频繁,fork 可能导致额外几 GB 的内存占用。在内存紧张的环境中,建议控制 AOF 文件大小、及时触发重写,避免单次重写数据量过大。
## AOF 开启方式
```conf
appendonly yes # 开启 AOF
appendfilename "appendonly.aof" # AOF 文件名
appendonly yes # 开启 AOF(默认 no)
appendfilename "appendonly.aof" # AOF 文件名(Redis 7 会在此基础上创建子目录)
dir /var/lib/redis # AOF/RDB 文件存放目录
```
> [!tip] 在线开启/关闭 AOF
> 可以通过 `CONFIG SET appendonly yes` 在运行时开启 AOF,但需要注意:这会触发一次阻塞式的 AOF 初始化(类似 BGSAVE),**生产环境建议在低峰期操作**。关闭 AOF 同理:`CONFIG SET appendonly no`。
Redis 7.x 的多文件目录结构:
```bash
# Redis 7.x 目录结构(新版本使用多文件目录而非单文件)
ls /var/lib/redis/appendonlydir/
total 256M
drwxr-x--- 2 redis redis 4.0K May 15 18:00 .
drwxr-x--- 2 redis redis 4.0K May 15 18:00 ..
-rw-r----- 1 redis redis 16K May 15 18:00 manifest.aof
-rw-r----- 1 redis redis 128M May 15 18:00 base.rdb
-rw-r----- 1 redis redis 128M May 15 18:01 incr-0000000000000003.aof
# total 256M
# manifest.aof <- 文件清单(记录哪些文件组成完整 AOF)
# base.rdb <- 最近一次重写生成的 RDB 全量快照
# incr-0000000000000003.aof <- 增量 AOF(记录重写之后的新写操作)
```
### 思考:为什么 AOF 会持续增长?
想象以下操作序列:`SET x 1 -> SET x 2 -> SET x 3 -> DEL x`。如果不做处理,AOF 中会记录这 4 条命令,但 key `x` 已经不存在了——这些写入了磁盘却没有任何价值。这就是为什么需要**重写**。
> [!question] 为什么 Redis 7 要把单文件拆成目录结构?
> 旧版单个 AOF 文件有一个痛点:重写时需要先生成临时文件,再 `rename` 替换。如果 AOF 有 50GB,rename 操作虽然原子但磁盘 IO 开销巨大。拆成多文件后,重写只需替换 `base.rdb` 和新增 `incr-*.aof`,IO 开销大幅降低,也更方便做增量备份。
## 混合持久化(Hybrid Persistence)
混合持久化是 Redis 4.0 引入、Redis 7 默认开启的特性——在 AOF 重写时,先用 RDB 快照全量备份当前内存状态,再用 AOF 追加增量变化。
混合持久化是 Redis 4.0 引入、Redis 7 默认开启的特性,它解决了 AOF 重写的一个尴尬问题:**重写时到底该生成 RDB 还是 AOF?**
> [!question] 纯 AOF 重写的痛点
> 纯 AOF 重写需要把每个 key 的当前状态都转成 SET/HSET 等命令写入文件。当实例有几十万个 key 时,这些命令序列化+解析的过程会显著拖慢恢复速度。而 RDB 是二进制格式,加载速度比逐条执行 AOF 命令快 10~100 倍。
>
> 混合持久化的答案很简单:**两种都用**。重写时先写 RDB 全量快照作为"底子",再追加一小段 AOF 增量命令作为"补丁"。
```conf
# redis.conf
aof-use-rdb-preamble yes # 开启混合持久化(默认值)
```
```mermaid
flowchart LR
BG["BGREWRITEAOF"] --> Fork["fork 子进程"]
Fork --> RDBPart["RDB 全量部分<br/>二进制快照,写入极快"]
Fork --> AOFPart["AOF 增量部分<br/>仅含 fork 后的写操作"]
RDBPart --> Merge["合并为临时文件"]
AOFPart --> Merge
Merge --> Rename["原子 rename 替换"]
重写后生成的文件结构如下:
style RDBPart fill:#bbf,stroke:#66c
style AOFPart fill:#dfd,stroke:#090
```mermaid
flowchart TD
subgraph NEW_AOF["重写后的新 AOF 文件"]
direction LR
RDB["RDB 二进制块<br/>(全量数据快照)"] --> AOF["AOF 增量块<br/>(仅重写期间的新写操作)"]
end
Fork["fork 子进程"] --> RDB
Fork --> AOF
RDB --> Merge["合并写入临时文件"]
AOF --> Merge
Merge --> Rename["原子 rename 替换旧文件"]
style RDB fill:#bbf,stroke:#66c
style AOF fill:#dfd,stroke:#090
style Merge fill:#ffd,stroke:#cc0
```
**好处有两个:**
1. **恢复速度快**:启动时先加载 RDB 全量快照(二进制格式比解析 AOF 命令快 10~100 倍),再回放少量 AOF 增量命令
2. **文件体积小**:相比纯 AOF 记录每条操作指令,混合模式下文件通常缩小 70%~90%
1. **恢复速度快**:启动时先加载 RDB 二进制快照(顺序读、无需解析命令语法),再回放少量 AOF 增量命令。即使 AOF 增量段有几十 MB,相比从头解析几十 GB 的纯 AOF 命令也快得多
2. **文件体积小**:RDB 本身就是压缩的二进制格式,比等量数据的纯 AOF 文本命令小 70%~90%
> [!WARNING] 恢复流程的变化
> 混合模式下,Redis 启动时先读取 `base.rdb` 还原全部数据,再逐条执行增量 `.aof` 段中的命令。如果只保留 AOF 段而删除 `base.rdb`,恢复将退化为纯 AOF 模式,速度大幅下降。
> [!warning] Redis 7 的多文件目录结构
> Redis 7 不再使用单个 AOF 文件,而是采用多文件目录结构:
> - `base.rdb`:AOF 重写时生成的 RDB 全量快照
> - `incr-*.aof`:增量 AOF 文件(记录重写之后的新写操作)
> - `manifest.aof`:文件清单,记录哪些文件属于同一个 AOF 实例
>
> 恢复时 Redis 会按 manifest 加载:先加载 `base.rdb`,再按顺序回放所有 `incr-*.aof`。如果删除了 `base.rdb`,恢复退化为纯 AOF 模式,速度会大幅下降。
## AOF vs RDB 对比
@@ -167,29 +241,48 @@ flowchart LR
## AOF 故障恢复
断电或异常退出时,AOF 文件可能出现**尾部截断**(最后一条命令不完整)。Redis 启动时会自动检测并尝试修复。
### 常见故障场景与修复
```bash
# 如果 AOF 损坏(断电等原因导致截断)
redis-server --repair appendonly.aof
# 或 Redis 7 下修复整个目录
redis-server --fix appendonlydir/aof-manifest
# 场景一:AOF 文件末尾被截断(最常见——断电导致最后一条命令写了一半)
# Redis 启动时会自动丢弃末尾不完整的命令,无需手动干预
# 场景二:AOF 文件损坏严重(如磁盘坏块导致文件中间损坏)
# 需要手动修复:找到最后一个合法命令,截断之后的内容
redis-check-aof --fix appendonly.aof
# 场景三:Redis 7 多文件目录结构
redis-check-aof --fix appendonlydir/appendonly.aof.1.incr.aof
```
> [!tip] 实际修复流程
> 1. 先备份损坏的 AOF 文件:`cp appendonly.aof appendonly.aof.bak`
> 2. 运行 `redis-check-aof --fix`(交互式,会告诉你损坏位置并确认是否截断)
> 3. 重新启动 Redis,检查数据完整性
> 4. 如果数据缺失严重,从 RDB 冷备份恢复 + 回放最近的增量 AOF
### fsync 失败时的行为
```
fsync -> EIO(磁盘错误)
↓
ERROR "I/O error writing to APPEND ONLY FILE..."
↓
立即 SHUTDOWN NOSAVE(停止服务防止数据进一步损坏)
```mermaid
flowchart TD
FS["fsync() 调用"] --> OK{"返回成功?"}
OK -->|"是"| CONT["继续正常服务"]
OK -->|"否:EIO 磁盘错误"| LOG["记录日志<br/>I/O error writing to APPEND ONLY FILE"]
LOG --> SD["执行 SHUTDOWN NOSAVE<br/>立即停止服务"]
style SD fill:#fdd,stroke:#c00
style CONT fill:#dfd,stroke:#090
```
> [!NOTE] Redis 为什么会主动 shutdown?
> 这是一种「fail-fast」设计哲学。AOF 的核心价值就是数据安全,当底层磁盘出现问题时,继续运行只会导致更多数据静默损坏。宁可停机也不能让用户在「不确定」的数据上继续业务。
> [!note] Redis 为什么会主动 shutdown?
> 这是一种「fail-fast」设计哲学。AOF 的核心价值就是数据安全,当底层磁盘出现问题时,继续运行只会导致更多数据静默损坏。**宁可停机也不能让用户在"不确定"的数据上继续业务**——这和数据库遇到 WAL 写入失败时的处理逻辑是一样的。
> [!WARNING] AOF 不是万能的
> - AOF 修复工具只能恢复大部分,极端情况下会有部分命令丢失
> - 定期做冷备份(RDB 迁移到 OSS/S3)仍然是必须的兜底手段
> [!warning] AOF 不是万能的
> - AOF 修复工具只能处理末尾截断,中间损坏只能截断丢失后面的数据
> - 定期做冷备份(RDB 快照传到 OSS/S3)仍然是必须的兜底手段
> - 生产环境建议部署**主从架构**:即使主节点 AOF 出问题,从节点仍有完整数据
## AOF vs RDB 选型决策树