Linux 单机 Redis 实战运维:maxmemory 内存预算、淘汰策略选择与 OOM 被杀的完整排查

为什么单机 Redis 才是个人站长最该先搞定的一步

很多个人站长的服务器上其实早就装了 Redis,但它的角色往往停留在「装了个缓存插件,PHP 里能用」这个层次。真正把 Redis 当一台需要运维的中间件来对待的人并不多。结果是内存一直缓慢上涨、某天突然 OOM 被内核杀掉、页面白屏半小时,查日志却发现 Redis 什么错误都没打出来——因为它是被 SIGKILL 干掉的,来不及写日志。

这篇文章不讲 Redis 的数据结构八股,只讲单机 Redis 在生产环境里真实会咬人的几件事:内存上限怎么设、淘汰策略怎么选、持久化怎么配才不至于既慢又不安全、以及出问题的时候该看哪几个数字。适用于 1 核 2G 到 4 核 8G 这种典型的个人站长配置。

一、先摸清现状:info 里真正该看的字段

动手改配置之前,先要知道现在到底是什么状态。很多人只看 used_memory_human,这远远不够。一条命令把关键指标拉出来:

redis-cli info memory | grep -E "used_memory_human|used_memory_rss_human|used_memory_peak_human|mem_fragmentation_ratio|maxmemory_human|maxmemory_policy"
redis-cli info stats | grep -E "evicted_keys|expired_keys|keyspace_hits|keyspace_misses|rejected_connections"
redis-cli info clients | grep -E "connected_clients|blocked_clients"
redis-cli info persistence | grep -E "rdb_last_bgsave_status|aof_last_bgrewrite_status|rdb_changes_since_last_save"
redis-cli info keyspace

逐个解释为什么这几个值重要:

  • used_memory_rss 与 used_memory 的比值就是 mem_fragmentation_ratio。小于 1 说明部分内存被换到 swap 上了,这是性能灾难的开始;大于 1.5 说明碎片严重,通常发生在大量 key 大小不一、频繁增删的场景。个人站常见的 WordPress 对象缓存正好符合这个特征。
  • evicted_keys 是淘汰计数。如果这个数在持续增长,说明你的 maxmemory 设得太紧,缓存正在被反复挤掉,命中率虚高但实际一直在重建。
  • keyspace_hits / (hits + misses) 才是真实命中率。低于 80% 就要怀疑:是缓存 TTL 太短,还是 key 设计有问题,或者是缓存穿透。
  • rdb_changes_since_last_save 表示自上次落盘以来改了多少次。如果这个数字一直很大而 rdb_last_bgsave_status 不是 ok,说明持久化实际上一直在失败。

二、maxmemory 到底设多少:留给系统的余量比你想的多

最大内存不应该按「内存减去其他服务」来简单相减,因为 Redis 在 fork 做 RDB 或 AOF rewrite 的时候,会额外占用接近一半的物理内存(写时复制,COW)。最坏情况下的总占用接近 used_memory 的 2 倍。

所以对于 4G 内存的机器,跑着 Nginx + PHP-FPM + MySQL,比较稳妥的算法是:

系统基础占用        约 300MB
MySQL buffer pool   约 800MB~1GB
PHP-FPM 常驻        约 400MB
留给页缓存与突发    约 500MB
-----------------------
Redis 可用          4G - 2.2G = 约 1.8G
考虑到 COW 峰值,maxmemory 设为 1G 更安全

换算成比例,经验值是maxmemory 不超过物理内存的 25%~30%,且要保证空闲内存大于 maxmemory 的一半。如果机器只有 2G 内存又要跑 MySQL,那 Redis 就别超过 300MB,或者干脆改用文件缓存,别硬上。

配置写法(在 redis.conf 里):

maxmemory 1gb
maxmemory-policy allkeys-lru
maxmemory-samples 10

maxmemory-samples 默认是 5,LRU 是近似算法,采样越多越接近真 LRU,但 CPU 开销也越大。10 是一个不错的折中,能明显减少「刚放进去就被淘汰」的抖动。

三、淘汰策略:很多人选错了 allkeys-lru

Redis 提供了 8 种淘汰策略,个人站长真正需要区分的是这 4 种:

  • noeviction:内存满了直接报错(写命令返回 OOM error)。适合把 Redis 当数据库用的场景,但个人站基本不是。
  • allkeys-lru:所有 key 里按最近最少使用淘汰。适合纯缓存,绝大多数站点该用这个。
  • volatile-lru:只在设置了过期时间的 key 里淘汰。听着更「保守」,实际上是陷阱——如果大部分 key 没设 TTL,内存满了照样 OOM。
  • allkeys-lfu:按访问频率淘汰。适合热点极度集中的场景(比如首页文章列表被反复读),比 LRU 更能保住真正的热数据。

这里有个真实踩坑:很多 WordPress 站装了 Redis Object Cache 插件,插件默认会给一部分 key 设 TTL、一部分不设。如果运维手工配了 volatile-lru,当无 TTL 的 key 占满内存后,Redis 会开始拒绝写入,插件那边表现为「缓存随机失效」,然后数据库压力暴涨。所以纯缓存场景一律用 allkeys-lru,别用 volatile。

如果确认是用来缓存文章页、且热点明显,可以切 allkeys-lfu:

maxmemory-policy allkeys-lfu

切换策略不需要重启,redis-cli config set maxmemory-policy allkeys-lfu 即时生效,但要注意 config set 改的东西重启后会丢,一定要同时写进配置文件。

四、持久化:RDB 还是 AOF,个人站怎么选

先说结论:个人站的 Redis 若只做缓存,可以关掉持久化;如果存了会话(session)或者队列,必须开 AOF。

如果存 session,丢了会导致所有用户被登出——这个体验很糟糕。这时候建议:

appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

everysec 是性能与安全的平衡点,最坏丢 1 秒数据。always 会拖慢写入,个人站没必要。

如果只做页面缓存/对象缓存,直接:

save ""
appendonly no

关掉持久化之后还要注意一件事:重启后缓存全空,数据库会瞬间承受全量请求。这在流量稍大的站上足以把 MySQL 打挂。缓解办法是预热,或者让缓存过期时间错开。可以在重启后立刻跑一个小脚本,把最热的几十个 URL 抓一遍:

cat /var/www/sitemap-hot.txt | head -50 | while read u; do
  curl -s -o /dev/null -A "warmup" "$u"
  sleep 0.2
done

五、慢查询与阻塞:Redis 是单线程,一个慢命令拖垮全部

Redis 命令是串行执行的。一条 keys * 或者对 10 万成员的 zset 做 zrange,就能让所有请求排队几百毫秒。所以必须开慢日志:

slowlog-log-slower-than 10000
slowlog-max-len 128

单位是微秒,10000 表示 10ms。查看方式:

redis-cli slowlog get 10

常见的高危命令清单,个人站长务必在代码里避开:

  • KEYS pattern — 全库扫描,O(N)。用 SCAN 游标迭代代替。
  • FLUSHALL — 生产环境应该在配置里直接禁用或改名成别的命令。
  • SMEMBERS 大集合 — 用 SSCAN。
  • DEL 大 key — 删一个百万成员的 hash 会阻塞。用 UNLINK(异步删除)。
  • HGETALL 巨大 hash — 用 HSCAN。

禁用危险命令的方法,在配置文件里:

rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command CONFIG "CONFIG_a8f3c9"

CONFIG 改名而不是禁用,是因为 config set 是运维刚需,但要防止被攻击者用它落地持久化后门(这是真实存在的 Redis 未授权攻击手法:写 crontab、写 SSH key)。

六、安全:6379 千万不要暴露在公网

这不是危言耸听。全世界扫描 6379 端口的机器人每天都是海量的,一旦无密码且可公网访问,几个小时之内服务器就可能被植入挖矿程序。三层防护:

# 1. 只监听本机或内网
bind 127.0.0.1 -::1

# 2. 设置强密码(requirepass 后面必须是长随机串)
requirepass 换成32位以上的随机密码

# 3. 保护模式(默认开启,不要关)
protected-mode yes

如果 PHP 和 Redis 不在同一台机器,就绑内网 IP 并用防火墙限制来源:

bind 127.0.0.1 10.0.0.5
# nftables 只放行内网
nft add rule inet filter input tcp dport 6379 ip saddr 10.0.0.0/24 accept
nft add rule inet filter input tcp dport 6379 drop

注意 Docker 场景:docker run -p 6379:6379 会绕过 ufw 的 INPUT 链(Docker 直接操作 nat 表和 DOCKER-USER 链),必须用 -p 127.0.0.1:6379:6379 绑定到回环地址,或者在同一 docker network 内用服务名访问而不发布端口。

七、监控:用一条命令做每日巡检

个人站没必要上 Prometheus + Grafana,写个 cron 脚本,每天把关键指标记进文件,出问题时能回溯:

#!/bin/bash
# /usr/local/bin/redis-daily-check.sh
D=$(date +%F-%H%M)
OUT=/var/log/redis-check.log
{
  echo "===== $D ====="
  redis-cli -a "$REDIS_PASS" --no-auth-warning info memory \
    | grep -E "used_memory_human|maxmemory_human|mem_fragmentation_ratio"
  redis-cli -a "$REDIS_PASS" --no-auth-warning info stats \
    | grep -E "evicted_keys|keyspace_hits|keyspace_misses|rejected_connections"
  redis-cli -a "$REDIS_PASS" --no-auth-warning info clients | grep connected_clients
  redis-cli -a "$REDIS_PASS" --no-auth-warning dbsize
  redis-cli -a "$REDIS_PASS" --no-auth-warning --bigkeys 2>/dev/null | tail -20
} >> $OUT

# 超过 1G 就告警
RSS=$(redis-cli -a "$REDIS_PASS" --no-auth-warning info memory | grep used_memory_rss: | cut -d: -f2 | tr -d '\r')
if [ "$RSS" -gt 1073741824 ]; then
  echo "Redis RSS 超过 1G: $((RSS/1024/1024))MB" | mail -s "Redis 告警 $(hostname)" you@example.com
fi

加进 crontab:0 8 * * * /usr/local/bin/redis-daily-check.sh。日志会自动轮转前先看趋势,一般内存曲线是锯齿状缓慢上升属于正常;如果呈直线上升且从不回落,说明有 key 没设 TTL 在漏。

找漏 TTL 的 key 没有官方直接命令,但可以用 scan 统计:

redis-cli --scan --pattern '*' | head -100000 | while read k; do
  ttl=$(redis-cli ttl "$k")
  [ "$ttl" -eq -1 ] && echo "$k"
done | head -50

注意这脚本会打很多命令,只在低峰期跑,并且加个 head 限制数量。

八、一个真实的故障复盘

某次站点在凌晨三点开始变慢,早上一看:PHP 报「Cannot connect to Redis」,页面全部回源查数据库,MySQL 连接数打满,整站 502。

排查顺序是:systemctl status redis 显示 status=9/KILL——不是自己退出的,是被杀的。查 dmesg -T | grep -i oom,果然在 03:02 有 OOM Killer 记录,杀的正是 redis-server。但 maxmemory 明明设了 800MB,机器 4G 内存,怎么会 OOM?

原因是 cron 在 03:00 跑了全站备份,tar czf 压缩时吃了大量页缓存和内存,同时 Redis 正在做 RDB fork(当时 save 900 1 没关),COW 复制让 Redis 实际占用翻倍到 1.6G。三者叠加,内存峰值突破 4G,内核挑了个 RSS 最大的进程杀掉。

修复做了三件事:

  1. 关掉 RDB 自动保存(缓存不需要持久化),改用 save ""。
  2. 把备份时间从 03:00 挪到 04:30,并给 tar 加 nice -n 19 ionice -c3。
  3. 把 maxmemory 从 800MB 降到 600MB,给突发留余量。

之后半年没再复发。这个案例的教训是:Redis 的 OOM 不一定是 Redis 自己的问题,要把它放进整机内存账本里看。dmesg 的 OOM 记录永远是排查这种「无缘无故挂掉」的第一站。

九、一份可以直接抄的配置基线

最后给一份适合个人站的 redis.conf 关键片段,按需改密码和内存值:

bind 127.0.0.1 -::1
protected-mode yes
port 6379
requirepass 你的32位随机密码

# 内存
maxmemory 1gb
maxmemory-policy allkeys-lru
maxmemory-samples 10

# 持久化:纯缓存关掉;存 session 则改为 appendonly yes
save ""
appendonly no

# 慢日志
slowlog-log-slower-than 10000
slowlog-max-len 128

# 连接
timeout 300
tcp-keepalive 300
tcp-backlog 511

# 危险命令
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""

# 日志
loglevel notice
logfile /var/log/redis/redis-server.log

# 客户端输出缓冲(防止慢客户端吃内存)
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit pubsub 32mb 8mb 60

改完 systemctl restart redis-server,然后用 redis-cli -a 密码 info memory 确认 maxmemory 生效,再压一小会儿流量观察 evicted_keys 是否稳定(小幅增长是正常的,持续暴涨说明内存设小了)。

Redis 这件事的本质是:它不是「装完就不用管」的东西,而是一台需要内存预算、需要淘汰策略、需要监控曲线的服务。把上面这几项配好,一台 2G 内存的小机器完全能扛住日均几万 PV 的站点缓存需求。

Last modification:October 17th, 2026 at 10:26 pm

Leave a Comment