03-数据会不会丢:RDB、AOF 与内存管理的真实边界
数据会不会丢:RDB、AOF 与内存管理的真实边界
一、重启之后,购物车少了一大截
第 02 篇结尾留了个问题:缓存丢了可以重建,那存储层的数据丢了怎么办。我们很快撞上了,丢的不是缓存,是被当成存储的那部分。
购物车没进 MySQL,直接落在 Redis——中小项目常这么干,理由是"反正 Redis 有持久化"。某次凌晨发布重启实例,第二天监控显示购物车条数少了一大截。没有报错,没有告警:启动成功、客户端连上,读不到的 key 返回空,业务把它解释成"用户没加过购物车"。
复盘查到两件事:
- 这台实例
appendonly no,只有 RDB,save用默认档位(默认值,非实测)。两次快照之间的写入全丢。 - 另一台开了 AOF 的实例更隐蔽:断电时 AOF 尾部写了半条命令,
aof-load-truncated yes(默认值)让 Redis 带警告启动、跳过不完整部分。恢复点不是"最后一条命令",而是"最后一个完整命令"。
第二条比第一条危险。丢最后几秒,业务顶多重做;"回到过去"却是读到一个格式完全正确、时间点是过去的值——不报错、不打库,被当正确值用上一周才发现。
所以本篇只回答一个问题:对这台 Redis,宕机时到底丢多少,由哪几个配置决定。
二、丢失窗口:配置说了算
| 配置 | 宕机最多丢 | 代价(定性) | 适用 |
|---|---|---|---|
| 不做持久化 | 全部 | 无 | 纯缓存,可全量重建 |
仅 RDB(默认save) | 上次成功快照之后的全部写入 | 恢复快、文件小 | 丢一段也能重建的数据 |
AOFeverysec | 约 1 秒(官方表述);磁盘阻塞时可能更多 | 每秒一次 fsync | 主流起点 |
AOFalways | 单条命令级 | 每条命令一次 fsync,写延迟被磁盘绑定 | 量小且不能丢 |
AOFno | 交给内核刷脏页,不可控 | 最快 | 不该用于数据 |
混合(rdb-preamble+everysec) | 同everysec,不同 RDB | 恢复快 + 丢得少 | 推荐默认 |
一个常被读错的点:aof-use-rdb-preamble只改变 AOF 文件的开头长什么样,不改变尾部追加和 fsync 节奏。开了混合持久化,丢失窗口仍然是appendfsync决定的那一个。
「丢多少」由appendonly+appendfsync决定;RDB 决定的是「恢复多快」。
三、RDB:fork 复制的是页表,不是数据
BGSAVE调fork():内核复制页表、建立写时复制(COW)映射,主线程同步完成,阻塞时长与页表规模相关——内存越大、页越多,fork 越慢。
fork 返回后,父子共享同一批物理页,任何一方写某页,内核才复制那一页。所以:
- 主线程只在 fork 期间被按住,不是整个快照期间。
- 之后的"内存上涨"是 COW 的结果,不是阻塞。快照期间写得越密,复制的页越多,最坏接近翻倍。
[图:fork 复制页表 → COW 写时复制 → 快照期间内存膨胀]
两个容易漏的细节:
- 透明大页(THP)会放大 fork 开销。4KB 页合成 2MB,页表项变少但单次复制变重,fork 延时会明显拉长,生产建议关掉。
- 复制了多少页有字段可查:
INFO persistence的rdb_last_cow_size。它比"我觉得没涨多少"可靠,也是后面算maxmemory的依据。
SAVE是主线程原地写,期间所有命令排队,只该在停机维护时用;BGSAVE才 fork 子进程。同一时刻只允许一个 bgsave,重复触发会被跳过(看rdb_bgsave_in_progress)。自动触发的场景除了save <seconds> <changes>,还有SHUTDOWN、主从全量同步、DEBUG RELOAD。
关掉 RDB 的后果要说清:save ""只是不再自动快照,不等于"永不 fork"——主从全量同步仍要生成 RDB(落盘或 diskless)。若同时appendonly no,重启就是空库。它不是性能开关,是数据开关。
四、AOF:everysec不是零丢失
4.1 三种 fsync 的边界
appendfsync | 语义 | 最坏丢失 | 定性代价 |
|---|---|---|---|
always | 每条命令 write + fsync 后才回 OK | 单条命令 | 写延迟被磁盘绑定,吞吐显著下降 |
everysec | 后台线程每秒 fsync 一次 | 约 1 秒(官方表述) | 折中,主流选择 |
no | 只 write 到内核缓冲 | 由内核决定,不可控 | 最快,也最不可控 |
"约 1 秒"是上限描述,不是保证。机械盘、大 AOF、快照或重写并发时,这个窗口会比 1 秒长,下面讲原因。
有没有发生过?看INFO persistence的aof_delayed_fsync:大于 0 就说明出现过"因 fsync 未完成而延迟写"。这是最该加监控、却最少人看的字段之一。
再往下看一层:everysec的阻塞其实来自内核。fsync本身是丢给后台线程做的,但"每秒一次"这个承诺要靠主线程自己守。当上一次后台fsync迟迟没返回(源码里以 2 秒上下为界,具体以版本为准),而新命令又需要写 AOF 时,主线程会主动停下来等它完成——不这样的话,继续写在磁盘上就违背了"最多丢 1 秒"。这一等,等的就是磁盘:机械盘坏道、云盘突发 IO 限流,都会把这份等待直接传给业务线程。
这也是aof_delayed_fsync值得单独告警的原因:它记录的正是"主线程为等 fsync 被拖住"的次数。这个数一涨,业务侧就会出现延迟毛刺,而SLOWLOG里可能一条慢命令都没有——因为慢的不是某条命令,是整条主线程被磁盘按住了(第 06 篇会用LATENCY的事件视角把这个区别讲透)。
4.2 重写:fork 又来了,还多一份缓冲
AOF 是命令流水,会越来越长;重写要做的是生成"当前数据集的最小表达"。
- 触发:
auto-aof-rewrite-percentage 100且auto-aof-rewrite-min-size 64mb(默认值,非实测)。 - 期间双写:子进程只看到 fork 那一刻的数据集,此后新到的命令既要写旧 AOF,也要写进
aof_rewrite_buf,重写完成后追加到新文件末尾。 - 风险:重写期间的"内存双杀"。写入量很大时,上面两件事会同时恶化:COW 不断复制物理页,
aof_rewrite_buf不断变长。 - 这个缓冲没有任何配置项能直接给它设上限。
aof_rewrite_buf不是客户端输出缓冲,client-output-buffer-limit管不到它;它的大小约等于"写入速率 × 重写剩余时间",只能间接控制。两份内存叠加,RSS 会迅速逼近甚至超过物理内存上限,直接引发 OOM——而used_memory看上去可能毫无异常。
怎么观察、怎么压?老版本的INFO persistence里有aof_rewrite_buffer_length能看实时大小(7.0 改多文件 AOF 后该字段未必还在,以自己实例为准)。真正有效的动作是三条:错峰——把重写放到低峰期手工BGREWRITEAOF,别让它在流量高峰自己触发;调小重写规模——把auto-aof-rewrite-percentage降下来,让它更早、更小地发生,而不是等 AOF 涨得很夸张再一次性重写;别指望no-appendfsync-on-rewrite——那个参数换的是延迟,不是内存。
[图:AOF 重写期间的双写、aof_rewrite_buf 与 COW 叠加]
Redis 7.0 起 AOF 是多文件结构:appendonlydir/(appenddirname默认值)下是 base、incr 与 manifest。好处是重写不用再复制整个大文件,代价是目录结构变了,降级到 6.x 可能直接读不了。
4.3 混合持久化与版本兼容
aof-use-rdb-preamble yes(5.0 起默认):重写时把当前数据集按 RDB 格式放在文件开头,后续命令按 AOF 追加;恢复时先按 RDB 加载(快),再回放增量(丢得少)。
兼容性要给个明确态度:“能重启"不等于"能降级”。RDB 带版本号,高版本写的低版本读不了,启动直接报错;7.x 的多文件 AOF 也不能被 6.x 直接加载。要降级得在新版本上导出重建,或用redis-cli --rdb搬数据(不同小版本行为有差异,需按实际环境验证)。
4.4 从库要不要开 AOF
- 全量同步的 RDB 从哪来:主库 fork 生成,落盘再传,或不落盘直接写 socket(diskless)。
repl-diskless-sync yes在 7.0 起是默认(6.x 及更早默认no);repl-diskless-sync-delay 5(默认值)会等更多副本聚集再一起传——一次 fork 服务多个副本,代价是切换后多等一下。- 从库开 AOF:它成为可用备份源,主库彻底挂掉还能从副本恢复;代价是每台从库都有磁盘写放大,且两边 AOF 的丢失窗口是两回事。
主库开 AOF 是在守"丢多少"这条线,从库开 AOF 是在守"还有没有第二份"。
五、内存去哪了
5.1 四个指标先分清
| 指标 | 视角 | 看什么 |
|---|---|---|
used_memory | 分配器的逻辑视角 | 数据集 + 服务器开销 |
used_memory_rss | 操作系统视角 | 实际占用的物理页,含碎片 |
mem_fragmentation_ratio | rss / used | > 1 有碎片;< 1 说明用了 swap |
used_memory_overhead | 服务器自身 | 复制 backlog 与缓冲区、客户端输出缓冲、AOF buffer |
used_memory_dataset | used − overhead | 数据本身占了多少 |
排查顺序固定:先看dataset涨没涨 → 再看overhead有没有异常 → 最后看rss − used的差值。
[图:used_memory / rss / overhead / dataset 四层关系]
ratio明显大于 1(经验上 1.5 以上值得查)通常是碎片;小于 1 要立刻处理——说明部分内存被换到 swap,延迟会出现无法预测的毛刺。
5.2 "过期了内存没降"的三种情况
- 逻辑删除 ≠ 物理释放。
DBSIZE走字典大小,key 一删就降;used_memory是分配器视角,jemalloc 回收后未必立刻还给 OS,rss可能不动。 - 从库不主动过期。从库等主库的
DEL传播过来才删;在此之前这些 key 仍占内存,只是读取时返回空。所以"从库内存比主库高"不一定是 bug。 - 大 key 过期本身就是一次删除,释放是 O(N) 的。
lazyfree-lazy-expire默认为no(用CONFIG GET lazyfree-lazy-expire确认自己实例),主线程会亲自回收,表现为"内存降了,但那一刻命令抖了一下"。
清理机制本身是抽样:hz 10(默认值,dynamic-hz默认开启),每轮从带 TTL 的 key 里抽 20 个(源码常量,不是配置项),过期比例超过 25% 就再抽一轮。所以过期 key 的清除速度由采样决定,不是到期即删。
5.3 八种淘汰策略
| 策略 | 候选范围 | 选谁淘汰 |
|---|---|---|
noeviction | — | 不淘汰,写操作直接报 OOM |
allkeys-lru | 所有 key | 最久未使用(采样近似) |
allkeys-lfu | 所有 key | 使用频率最低 |
allkeys-random | 所有 key | 随机 |
volatile-lru | 仅设了 TTL 的 key | 最久未使用 |
volatile-lfu | 仅设了 TTL 的 key | 频率最低 |
volatile-random | 仅设了 TTL 的 key | 随机 |
volatile-ttl | 仅设了 TTL 的 key | 剩余 TTL 最短 |
默认是noeviction,默认maxmemory 0(不限制)——两者一起,意味着默认配置下 Redis 会一直吃到被系统杀掉。
LRU/LFU 都是采样近似。maxmemory-samples 5(默认值)决定采样池大小:从池里挑最久未用/最少使用的那个。它和精确 LRU 的命中率有差距,采样数越大越接近,代价是每次决策更费 CPU。LFU 是 8 位计数器(0–255)加衰减:lfu-log-factor 10控制增长难度,lfu-decay-time 1控制衰减速度(均为默认值),更适合"长期热点 + 突发扫描",因为它记得频率,而不只是最后访问时间。
volatile-*有个陷阱:一个 key 都没 TTL,淘汰就无事可做,写操作直接 OOM。这是"明明配了淘汰策略,为什么还是写不进去"的根因。
5.4maxmemory到底设多少
maxmemory ≤ 物理内存 − 系统与其他进程占用 − 复制缓冲区与客户端输出缓冲的峰值 − fork 期间 COW 的增量上界 − 碎片余量| 项 | 怎么估 |
|---|---|
| 系统与其他进程 | free -m的 used 减去 Redis 的used_memory_rss |
| 复制/输出缓冲 | client-output-buffer-limit各项上限之和(replica 默认硬限 256mb) |
| COW 增量 | 看rdb_last_cow_size的历史峰值,按写入比例外推 |
| 碎片余量 | 按mem_fragmentation_ratio的常态值折算 |
关键:maxmemory约束的是逻辑内存,不包含 fork 时的 COW 副本。设成物理内存 100%,等于把 OOM Killer 的枪口对准自己——快照或重写一来,COW 把物理内存推上去,内核直接杀进程。被杀时没有 OOM 日志,只有dmesg里一行记录,很容易被误判成"Redis 自己挂了"。
更深一层:为什么配了maxmemory,进程还是被 OOM Killer 杀了?
因为maxmemory管的是 Redis 的逻辑内存(used_memory),而内核决定杀不杀你,看的是物理内存(RSS)。这两者之间隔着一个常被忽略的差额——COW 副本、碎片、复制与客户端缓冲区。
举个算术例子(只是推演,不是实测):一台 16G 的机器,maxmemory 12G,看着留了 4G 余量。这时触发一次BGSAVE或 AOF 重写,恰好写入又很密集,COW 把大量共享页复制成私有页,RSS 从 12G 被推到 16G 以上。内核触发 OOM Killer,按oom_score挑一个杀掉,占内存最多的那个通常就是 Redis。全程 Redis 的逻辑内存可能一直没超过 12G,所以它在自己的日志里"什么都没做错"。
结论不是"maxmemory设小点更安全"这么简单,而是:maxmemory要按"物理内存 − COW 峰值 − 缓冲区 − 碎片 − 其他进程"来设,写入越密集,留给 COW 的余量就要越大。顺手还有两件事值得做:给 Redis 单独调oom_score_adj,让它别第一个挨刀;在容器里把maxmemory定在 cgroup 上限之下,而不是照着宿主机内存算。
5.5 单个 key 为什么比你想的贵
- SDS 头:按长度选
sdshdr8/16/32/64。embstr(≤ 44 字节)把 redisObject 与 SDS 一次分配,超过就变raw,两次分配。 - dictEntry + 桶数组:哈希表节点与桶本身,负载因子 1,扩容时翻倍。
- jemalloc 尺寸类:按档向上取整,44 字节会落到 48 或 64 字节档(看 jemalloc 版本与分配路径,需实测)。
- expires 字典:设了 TTL 的 key 要在过期字典里再占一份 dictEntry——加 TTL 是要付内存代价的。
所以 key 名越长、数量越多,overhead 占比越高。用MEMORY USAGE key实测单个再乘数量,别用STRLEN推,那只是下限。
5.6 碎片与 activedefrag
碎片来自反复增删、变长 value(APPEND、RPUSH)、不同尺寸类交替驻留。手段三个:mem_fragmentation_ratio看趋势,MEMORY DOCTOR直接给结论,activedefrag(默认no)开起来后有五个阈值可调(ignore-bytes 100mb、threshold-lower 10、threshold-upper 100、cycle-min 1、cycle-max 25,默认值非实测)。MEMORY PURGE只在 jemalloc 下有效,把空闲页还给 OS。最彻底的仍是重启或主从切换,但要付丢失窗口和恢复时间的代价。
慎用activedefrag yes。碎片整理不是后台线程干的活,它在主线程里切片执行——虽然每次只做一小段,但吃的是本来该用来处理命令的 CPU 时间,搬动数据时还要临时占内存。高并发写入时强行开着,吞吐会出现肉眼可见的下降。
所以它更像救命药,不是日常保健品:只在"碎片确实严重(mem_fragmentation_ratio长期偏高、MEMORY DOCTOR也明确提示)且业务处在低峰"时,临时CONFIG SET activedefrag yes,整理到指标回落再关掉。另外要注意它依赖 jemalloc,某些构建里根本没编进去,开了没反应先确认这一点,别以为是参数写错了。
六、命令与实验
6.1 三套配置 +INFO persistence逐字段
# A. 纯 RDB appendonly no save 3600 1 300 100 60 10000# B. 纯 AOF(everysec) appendonly yes appendfsync everysec auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb save ""# C. 混合(推荐起点) appendonly yes appendfsync everysec aof-use-rdb-preamble yes save 3600 1改完用CONFIG REWRITE落盘,或启动时指定配置文件。别用CONFIG SET appendonly yes长期放着不管——它只改运行时,重启即失效。
| 字段 | 含义 | 什么时候要看 |
|---|---|---|
rdb_bgsave_in_progress | 是否正在快照 | 排查延迟毛刺时 |
rdb_last_bgsave_status | 上次快照成功/失败 | 失败必须告警,否则丢失窗口在悄悄变大 |
rdb_last_cow_size | 上次快照期间复制的字节数 | 算maxmemory的依据 |
aof_last_write_status | 上次 AOF 写盘状态 | 出现err说明磁盘满或权限问题 |
aof_delayed_fsync | 因 fsync 未完成而延迟写的次数 | 大于 0 就要查磁盘 |
aof_rewrite_in_progress | 是否正在重写 | 与内存上涨的时间点对照 |
aof_last_cow_size | 重写期间 COW 字节数 | 判断重写是否把内存顶上去 |
字段在各版本有增减,以实际输出为准(需按实际环境验证)。
6.2DEL与UNLINK的差别怎么观察
# 造一个大 key,沿用第 01 篇 5.4 的做法seq1100000|awk'{print "RPUSH biglist item-"$1}'|redis-cli--piperedis-cli MEMORY USAGE biglist redis-cli CONFIG RESETSTAT redis-cli DEL biglist redis-cli INFO commandstats|grep'cmdstat_del'# 再建一次,改用 UNLINKseq1100000|awk'{print "RPUSH biglist item-"$1}'|redis-cli--piperedis-cli CONFIG RESETSTAT redis-cli UNLINK biglist redis-cli INFO commandstats|grep'cmdstat_unlink'redis-cli INFO stats|greplazyfree_pending_objectscmdstat_*的usec_per_call是命令在主线程花的时间;lazyfree_pending_objects是后台待回收对象数,UNLINK后会先出现非零值再回落到 0。前者包含真正的内存释放,后者只把 key 从字典摘掉、释放交给后台线程,所以DEL的usec_per_call更高。具体差值取决于元素数量与机器,留【本机实测:待补】;多次重复取平均,单次观测噪声很大。
6.3 容量估算脚本
思路是"先量单 key,再乘数量,最后用系数覆盖字典与碎片",而不是背一个系数。
#!/usr/bin/env python3"""Redis 容量估算:算出来的是量级,不是精确值。"""importargparse,jsondefestimate(daily_writes,key_len,val_len,keep_days,overhead,replicas):per_key_floor=key_len+val_len# 只是下限:不含 SDS 头、dictEntry、size class 取整keys=daily_writes*keep_days dataset=keys*per_key_floor extra=dataset*(overhead-1)# 字典、头部、取整、expires 字典total=dataset+extrareturnkeys,total,total*(1+replicas)if__name__=="__main__":p=argparse.ArgumentParser()p.add_argument("--writes",type=int,required=True,help="日均写入 key 数")p.add_argument("--key-len",type=int,required=True)p.add_argument("--val-len",type=int,required=True)p.add_argument("--days",type=int,default=30)p.add_argument("--overhead",type=float,default=1.35,help="经验系数,必须用下面的校准步骤在你自己的实例上量出来")p.add_argument("--replicas",type=int,default=1,help="从库数量")a=p.parse_args()keys,total,with_repl=estimate(a.writes,a.key_len,a.val_len,a.days,a.overhead,a.replicas)print(json.dumps({"keys":keys,"MB_主库":round(total/1024/1024,1),"MB_含从库":round(with_repl/1024/1024,1),"提醒":"overhead 是经验值,未校准前不要拿它做采购决策",},ensure_ascii=False,indent=2))校准比脚本本身重要:
foriin$(seq11000);doprintf'SET sample:%s %s\r\n'"$i""$(printf'x%.0s'$(seq1180))"done|redis-cli--piperedis-cli MEMORY USAGE sample:1# overhead ≈ MEMORY USAGE 结果 / (key 名长度 + value 长度)拿到真实系数再回头跑脚本。key 名长度、是否带 TTL、value 是 JSON 还是短串,系数都不同——别用一个系数算所有业务。
6.4 定位"内存涨了、数据没涨"
redis-cli--bigkeys-i0.1# 走 SCAN;-i 让每 100 条命令歇 0.1 秒,降低对线上的影响redis-cli MEMORY USAGE product:1001 SAMPLES0# 0 表示精确遍历所有元素,慢,慎用redis-cli MEMORY STATS|grep-E"peak.allocated|total.allocated|dataset.bytes|overhead.total"redis-cli INFO memory|grep-E"used_memory:|used_memory_rss:|used_memory_overhead:|used_memory_dataset:|mem_fragmentation_ratio|maxmemory_policy"判定路径:
used_memory_dataset没涨 → 不是数据变多,继续。used_memory_overhead涨 → 查复制缓冲区、客户端输出缓冲(INFO clients的client_recent_max_output_buffer)。rss − used_memory涨 → 碎片,见 5.6。- 都不涨但进程被杀 → 查 fork 期间的 COW 峰值:
rdb_last_cow_size与aof_last_cow_size。
6.5 两个能复现的实验
过期不等于释放:
redis-cli SET k:expvPX1000redis-cli DBSIZEsleep2redis-cli DBSIZE# 通常已经降了redis-cli INFO memory|grepused_memory: redis-cli MEMORY USAGE k:exp# nil单个小 key 的释放对used_memory是噪声级别,看不出变化。要看批量过期,写一万个带 TTL 的 key 再对比前后,差值留【本机实测:待补】,重点看used_memory与rss是否同步下降。
淘汰策略踩空:
redis-cli CONFIG SET maxmemory 100mb redis-cli CONFIG SET maxmemory-policy volatile-lru redis-cli SET nokey:1v# 这些 key 都没设 TTLredis-cli SET nokey:2v# → (error) OOM command not allowed when used memory > 'maxmemory'redis-cli CONFIG SET maxmemory0# 实验结束,还原报错文案会明确告诉你"内存超了但不让淘汰"。实验完记得还原,别把实验配置留在生产上。
七、踩坑清单
- 把
everysec当零丢失—— 我见过 SLA 里写着"Redis 数据零丢失,方案 AOF everysec"的。它是上限描述,磁盘一慢窗口就比 1 秒长,aof_delayed_fsync会替你记着。 - 以为 RDB 快照期间主线程全程阻塞—— 我写错过这句话,被同事纠正。阻塞在 fork 那一瞬,之后是 COW 导致内存上涨,两件事别混。
- 把 Redis 当唯一存储,拿持久化当 WAL—— 持久化是"尽力恢复到最后一个完整命令",不是事务日志:没有事务边界、没有回滚、没有业务语义上的校验。真不能丢的数据,宿主要在数据库。
- 不设
maxmemory,或设成物理内存 100%—— 我见过进程被内核杀掉后,一群人查了半小时代码,最后在dmesg里看到真相。maxmemory不含 COW 副本,这是最常被漏掉的一点。也别以为设到 70% 就安全:写入密集时 COW 照样能把 RSS 顶穿。 - 以为 LRU 是精确 LRU—— 采样近似,
maxmemory-samples 5。命中率与理想值有差距,key 多、访问分散时更明显。 - 以为 key 过期就立刻释放内存—— 抽样清理 + 分配器不归还,
DBSIZE降了rss不动是常态,不是 bug。 - 混用
volatile-*却没给 key 设 TTL—— 淘汰无对象可选,写操作直接 OOM。这条在"只给部分 key 设了 TTL"的系统里最容易出现。
八、小结
- 丢失窗口由
appendonly+appendfsync决定;RDB 决定恢复速度,不决定丢多少。 - fork 只阻塞一瞬,代价在之后的 COW。
rdb_last_cow_size是最直接能验证这件事的字段。 - 内存要分层看:dataset → overhead → 碎片,顺序别乱;
maxmemory必须给 COW、缓冲区、碎片留位置。
下一篇换个维度:我们配好了持久化,算好了maxmemory,以为数据万无一失了。但持久化只能防宕机,不能防机器故障。磁盘坏了、整台机器没了、机房断了,AOF 再完整也读不出来。第 04 篇讲主从、哨兵与 Cluster——三种拓扑各自的代价,以及切换窗口里那些没人告诉你的坑。
自检三问:
- 你的实例
appendfsync是什么?如果现在断电,最多丢多久的写入——这个答案你验证过,还是猜的? INFO persistence里rdb_last_bgsave_status和aof_last_write_status,有没有配告警?上一次失败是什么时候?- 你的
maxmemory是按什么算出来的?扣掉 fork 的 COW 增量之后,还剩多少真正可用?