perf record 采集 redis 进程时 cpu 占用飙升,主因是 -g 选项触发频繁栈展开中断,尤其高吞吐下开销剧增;应改用 --call-graph dwarf、-f 99 降低采样干扰,并确保 debuginfo 可用以准确定位真实热点。

perf record 采集 Redis 进程时为什么 CPU 占用飙升?
直接对 redis-server 进程运行 perf record -p $(pgrep redis-server) -g -- sleep 30 后,常发现 Redis 响应变慢、CPU 使用率反常拉高——这不是 Redis 本身负载加重,而是 perf 的采样开销在用户态频繁中断所致。尤其当 Redis 处于高吞吐(如每秒数万请求)时,-g(调用图)会触发大量栈展开,显著拖慢目标进程。
实操建议:
- 优先用
perf record -p $(pgrep redis-server) --call-graph dwarf -F 99 -- sleep 30:用dwarf替代默认的fp(frame pointer),避免因 Redis 编译未带-fno-omit-frame-pointer导致栈回溯失败或采样失真 - 采样频率控制在
-F 99(约 100Hz),过高(如-F 999)会使内核采样队列积压,加剧延迟 - 若 Redis 启用了
jemalloc,需确保系统已安装debuginfo包(如jemalloc-debuginfo),否则内存分配相关函数名显示为[unknown]
perf report 看到大量 __libc_malloc 占比高,是内存问题还是误判?
在 perf report 中观察到 __libc_malloc 或 je_mallocx 占比异常高(>30%),不能直接断定是内存泄漏或分配过频——更可能是采样点落在 malloc 内部临界区(如自旋锁等待),而真正调用方(如 processCommand、createStringObject)被截断。
验证方法:
- 用
perf report -g --no-children查看“真正调用者”,避开内联/尾调用干扰 - 对比
perf script | awk '{print $3}' | sort | uniq -c | sort -nr | head -10,检查高频调用链是否集中在特定命令(如zrange+ziplist解码) - 若热点落在
sdsnewlen或ziplistPush,说明是字符串构造或 ziplist 扩容导致,而非 malloc 库本身瓶颈
如何确认热点是否来自 Redis 自身逻辑而非系统调用?
perf report 默认混合显示用户态和内核态符号,容易把 epoll_wait、read 等系统调用误认为 Redis 主要开销。但 Redis 是事件驱动模型,大部分时间本就阻塞在 epoll_wait —— 它占比高恰恰说明 CPU 并未被滥用。
关键区分方式:
- 过滤掉系统调用:
perf report --no-children -s symbol | grep -v '\[k\|sys\|do_syscall\|entry_SYSCALL\]' - 关注 Redis 源码级函数:
processCommand、call、propagate、rdbSaveKeyValuePair,这些才是真正业务逻辑热点 - 若
aeProcessEvents下游大量分支进入beforeSleep→activeExpireCycle,说明过期键清理成为瓶颈,需检查maxmemory-policy和active-expire-effort配置
perf 结果里出现大量 [unknown] 或 [.],怎么补全符号?
[unknown] 表示 perf 无法解析该地址对应函数名,常见于:Redis 动态链接了无调试信息的 jemalloc,或使用了 strip 后的二进制,或内核模块未加载符号表。
补全步骤:
- 确认 Redis 编译时加了
-g(保留 debug info),且未运行strip redis-server - 安装对应版本的
jemalloc-debuginfo(CentOS/RHEL)或libjemalloc-dev(Ubuntu/Debian) - 若用 systemd 启动 Redis,检查是否启用了
ProtectSystem=full或RestrictSUIDSGID,它们会阻止 perf 访问/proc/kallsyms,导致内核符号缺失 - 临时启用符号读取:
echo 0 | sudo tee /proc/sys/kernel/kptr_restrict(仅调试用,生产慎开)
真实场景中,perf 定位 Redis 热点最易忽略的一点是:没区分「采样时刻的执行位置」和「实际耗时归属」。比如 malloc 耗时长,根源可能在上层反复创建小对象(如每个 INCR 都 new 一个 robj),而不是 malloc 实现慢。得顺着调用链往上报,而不是停在库函数这一层。











