Redis持久化时fork触发COW页被内核误判为“冷页”,主因是vm.swappiness=60、启用THP、NUMA本地内存耗尽及overcommit_memory=0未调优四者协同导致Swap异常升高。

Redis fork()触发COW页被内核误判为“冷页”
Redis执行RDB快照或AOF重写时,会调用fork()创建子进程。此时子进程与主线程共享物理内存页,但页表标记为写时复制(COW)。一旦主线程或子进程尝试修改任意页,内核就需拷贝该页——而这个过程本身就会让大量匿名页(如Redis堆内存)瞬间变成“可换出”状态。
关键点在于:vm.swappiness=60(Linux默认值)会让内核在空闲内存低于总内存40%时,主动将这些刚被标记的COW页换入Swap,哪怕free -h显示还有2GB以上available内存。
- 现象:top里
%si持续 >1%,iostat -x 1中so突增,但free -h不报警 - 验证命令:
cat /proc/sys/vm/swappiness输出是否为60或更高 - 别信“内存还够”——这是内核回收策略和页类型(匿名页 vs 文件页)共同导致的误伤
透明大页(THP)让fork耗时暴涨并加剧Swap倾向
启用THP后,Redis内存常被组织成2MB大页。fork时需复制整页页表,耗时从毫秒级跳到数百毫秒;更糟的是,内核更容易把这种未频繁访问的大页判定为“冷页”,优先塞进Swap。
即使你已把vm.swappiness设为1,THP未关,调优也基本白做。
- 临时禁用:
echo never > /sys/kernel/mm/transparent_hugepage/enabled - 验证是否真生效:
grep -i "mmupage\|mmupf" /proc/$(pgrep redis-server)/smaps | head -5—— 输出中不能出现2048 kB,全是4 kB才算成功 -
cat /sys/kernel/mm/transparent_hugepage/enabled显示[never]≠运行时关闭,必须检查/proc/PID/smaps
NUMA架构下本地内存耗尽却不跨节点分配
双路Xeon服务器常见NUMA拓扑。Redis主进程若被调度到node 0,而node 0本地内存已满,内核宁愿把COW页换出到Swap,也不愿跨node分配内存——哪怕node 1还空着10GB。
这和swappiness、THP都无关,是NUMA内存策略的默认行为。
- 强制均衡分配:
numactl --interleave=all redis-server /etc/redis/redis.conf - 检查当前绑定:
numastat -p $(pgrep redis-server),看各node的Heap和Stack分布是否严重倾斜 - 如果用systemd管理,需在
ExecStart=前加ExecStartPre=/usr/bin/numactl --interleave=all true,否则启动失败
overcommit_memory=2缺失导致fork失败或退化
当Redis内存接近上限时,fork()需要预留等量虚拟地址空间(即使不立即分配物理页)。若/proc/sys/vm/overcommit_memory=0(默认),内核会保守估算物理内存+Swap是否足够,结果常拒绝fork,转而触发AOF重写失败或RDB超时。
更隐蔽的问题是:估算失败后,内核可能降级使用低效分配路径,间接推高Swap压力。
- 必须设为2:
echo 2 > /proc/sys/vm/overcommit_memory - 对应含义:允许分配,只要总申请量不超过
swap + RAM * overcommit_ratio(默认overcommit_ratio=50) - 配合
vm.swappiness=1,才能让内核在真正OOM前才动Swap
Redis持久化引发Swap占用,从来不是单一配置问题。四个参数——vm.swappiness、THP开关、overcommit_memory、NUMA调度策略——必须协同生效。漏掉任意一个,都可能让其他三项努力归零。尤其要注意/proc/PID/smaps里的真实页大小和numastat的节点分布,它们比free和top更能说明问题。











