CPU 使用率过高排查:Redis 到底在忙什么

CPU 使用率过高排查:Redis 到底在忙什么

为什么 CPU 使用率过高需要排查

Redis 是单线程模型,CPU 是它的关键资源。当 CPU 使用率过高时:
– QPS 会达到上限,不能再提升
– 延迟可能显著增加
– 部分请求可能超时
– 如果使用多线程 IO(Redis 6.0+),CPU 使用率可能来自 IO 线程

第一步:确认 CPU 确实过高

# 查看 Redis 进程 CPU 使用率
top -p $(pgrep -x redis-server)

# 输出示例
top - 10:00:00 up 10 days,  2:35,  1 user,  load average: 1.00
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
1234 redis     20   0 2.100g 1.000g  12000 R 98.0 12.5 123:45.67 redis-server

解读:%CPU = 98%,说明 Redis 已经吃满了单核。

# 查看整体 CPU 使用
mpstat -P ALL 2 3

第二步:分类排查 CPU 消耗来源

来源一:正常业务 QPS 过高

redis-cli INFO STATS | grep instantaneous_ops_per_sec
# instantaneous_ops_per_sec:85000

如果是正常业务导致的高 QPS,且 CPU 已到极限,说明需要扩容了。

判断
– QPS 在持续上升(业务增长正常)
– 没有明显慢查询
– 延迟在可接受范围

解决方案
1. 使用 Redis Cluster 分片
2. 使用读写分离(主写从读)
3. 升级更高主频的 CPU

来源二:慢命令消耗 CPU

# 查看慢查询
redis-cli SLOWLOG GET 20

# 查看命令统计(找出 CPU 消耗大户)
redis-cli INFO COMMANDSTATS
# cmdstat_keys:calls=100,usec=5000000,usec_per_call=50000.00
# cmdstat_smembers:calls=200,usec=3000000,usec_per_call=15000.00

CPU 密集型命令排行榜

命令 时间复杂度 CPU 含量 优化方案
KEYS O(N) ⭐⭐⭐⭐⭐ 改用 SCAN
SMEMBERS O(N) ⭐⭐⭐⭐ 改用 SSCAN
SORT O(N+M*log(M)) ⭐⭐⭐⭐ 改用 zset
ZRANGE large O(log(N)+M) ⭐⭐⭐ 限制返回数量
LREM O(N) ⭐⭐⭐ 使用其他结构
DEL bigkey O(N) ⭐⭐⭐ 改用 UNLINK

来源三:频繁 Fork 导致 CPU 飙升

# 查看 fork 耗时
redis-cli INFO STATS | grep latest_fork_usec

# 查看 RDB 和 AOF 重写状态
redis-cli INFO PERSISTENCE

症状
– CPU 周期性飙高
– 飙高时间点与 BGSAVE/AOF rewrite 时间吻合

原因:fork() 创建子进程时需要复制页表,如果内存占用大(数 GB),fork 过程会消耗大量 CPU。

# 检查 fork 对 CPU 的影响
import redis
r = redis.Redis()

def check_fork_cpu(r):
    stats = r.info('stats')
    persistence = r.info('persistence')

    fork_time = stats.get('latest_fork_usec', 0) / 1000000
    print(f"最近 fork 耗时: {fork_time:.2f}s")

    bgsave_time = persistence.get('rdb_last_bgsave_time_sec', 0)
    print(f"最近 BGSAVE 耗时: {bgsave_time}s")

    if fork_time > 0.3:
        print("⚠️ fork 耗时过长,建议关闭 THP")

来源四:过期 key 清理

# 查看过期和淘汰统计
redis-cli INFO STATS | grep -E "expired_keys|evicted_keys"

# 查看有 TTL 的 key 数量
redis-cli INFO KEYSPACE
# db0:keys=1000000,expires=500000,avg_ttl=3600

如果大量 key 在同一时间点过期,Redis 会密集清理过期 key,消耗 CPU。

# 检查是否密集过期
r = redis.Redis()
info = r.info('stats')
expired = info['expired_keys']
# 对比前后差值,如果每秒过期数 > 10000,需要优化

来源五:AOF 重写

# 查看 AOF 重写状态
redis-cli INFO PERSISTENCE
# aof_rewrite_in_progress:1  # 正在重写
# aof_last_rewrite_time_sec:30  # 上次重写耗时

AOF 重写本身由子进程执行,但主进程需要用 CPU 写入重写缓冲区(rewrite buffer)。如果写入量很大,CPU 消耗也会可观。

来源六:主动防御(安全原因)

# 检查是否有安全扫描或攻击
redis-cli CLIENT LIST
# 查看是否有异常 IP 大量连接
redis-cli CLIENT LIST | awk '{print $2}' | sort | uniq -c | sort -rn

工具链:从发现到定位

#!/bin/bash
# cpu_debug.sh - Redis CPU 问题排查脚本

echo "=== 当前 QPS ==="
redis-cli INFO STATS | grep instantaneous_ops

echo "=== 命令耗时 TOP ==="
redis-cli INFO COMMANDSTATS | sort -t: -k3 -rn | head -10

echo "=== 最近慢查询 ==="
redis-cli SLOWLOG GET 10 | grep -A 4 "4) \""

echo "=== Fork 耗时 ==="
redis-cli INFO STATS | grep latest_fork

echo "=== 连接数 ==="
redis-cli INFO CLIENTS | grep connected_clients

echo "=== 过期 key 情况 ==="
redis-cli INFO KEYSPACE
echo "过期/秒:"
redis-cli INFO STATS | grep expired_keys

echo "=== 系统级 ==="
strace -c -p $(pgrep -x redis-server) 2>&1 | head -20

分场景解决方案

场景 原因 解决方案
高 QPS + CPU 满 业务增长 集群分片/读写分离/升级 CPU
QPS 不高但 CPU 满 慢命令 检查 SLOWLOG 和 COMMANDSTATS
周期性 CPU 飙高 Fork/BGSAVE 关闭 THP/调整持久化策略
CPU 突然升高 攻击或异常流量 检查 CLIENT LIST
CPU 缓慢增长 key 持续增加 数据分片/清理过期数据

预防措施

# 监控并设置 CPU 告警
import psutil
import redis

def monitor_cpu(redis_host='localhost', threshold=80):
    """监控 Redis CPU 使用率"""
    r = redis.Redis(host=redis_host)

    while True:
        pid = r.info('server').get('process_id', 0)
        try:
            proc = psutil.Process(pid)
            cpu_pct = proc.cpu_percent()

            if cpu_pct > threshold:
                qps = r.info('stats')['instantaneous_ops_per_sec']
                slowlogs = r.slowlog_get(5)

                alert_message = (
                    f"Redis CPU 告警: {cpu_pct}%\n"
                    f"当前 QPS: {qps}\n"
                    f"慢查询: {[s['command'] for s in slowlogs]}"
                )
                print(alert_message)
                # 发送告警...

        except (psutil.NoSuchProcess, Exception) as e:
            print(f"监控异常: {e}")

        time.sleep(10)

面试要点

  • CPU 高不一定是 Redis 慢——可能是 正常业务负载高
  • SLOWLOGCOMMANDSTATS 是定位 CPU 消耗的首选工具
  • 最高 CPU 消耗的场景往往是 KEYS/SMEMBERS 等 O(N) 命令
  • THP 未关闭 导致 fork 时 CPU 飙升
  • 大量 key 同时过期也会导致 CPU 飙高
  • 在确定要扩容前,先确认是否可以通过优化命令来解决
© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容