CPU毛刺排查与性能优化实战

2026-07-13 2 min read 8

真实场景:凌晨3点的告警

2024年3月15日凌晨3:12,监控告警:核心支付接口P99延迟从50ms飙到2.3s,持续3分钟。登录服务器,top一看:CPU使用率从20%瞬间跳到95%,持续30秒后回落。这不是OOM,不是磁盘IO,是典型的CPU毛刺。

服务器配置:CentOS 7.9, 8C16G, PHP 8.3.6, Nginx 1.24.0, MySQL 8.0.35。业务逻辑:订单创建接口,每秒请求量约800QPS。

问题定位:从现象到根因

第一步:确认毛刺特征

用sar -u 1 60记录CPU使用率,发现毛刺间隔约5分钟,持续20-40秒,CPU sys占比从5%升到40%,user从15%升到55%。

# 安装sysstat
yum install sysstat -y
# 每1秒采样一次,共60次
sar -u 1 60 > cpu_usage.log
# 查看结果
cat cpu_usage.log | awk '$3 > 80 {print NR, $0}'

输出显示第23行和第47行CPU超过80%,确认毛刺规律。

第二步:strace追踪系统调用

毛刺发生时,用strace抓取PHP-FPM进程的系统调用。

# 找到PHP-FPM主进程PID
pid=$(pgrep -f "php-fpm: master" | head -1)
# 追踪所有子进程的系统调用,持续30秒
strace -f -p $pid -T -o strace_$(date +%s).log 2>&1 &
sleep 30
kill %1
# 分析耗时最长的系统调用
cat strace_*.log | awk '{if($NF ~ /^<[0-9.]+>$/) print $0}' | sort -t'<' -k2 -rn | head -20

结果:futex系统调用耗时占比最高,平均每次2.3ms,最长达45ms。futex是用户态锁,说明锁竞争严重。

第三步:perf采样定位热点函数

# 采样30秒,记录CPU周期事件
perf record -a -g -F 99 -- sleep 30
# 生成火焰图数据
perf script > perf.script
# 安装FlameGraph工具
git clone https://github.com/brendangregg/FlameGraph.git
cd FlameGraph
./stackcollapse-perf.pl ../perf.script > out.folded
./flamegraph.pl out.folded > cpu_flame.svg

火焰图显示:PHP的zend_mm_alloc和pthread_mutex_lock占CPU的40%。结合代码,发现是共享内存缓存(APCu)的锁竞争。

方案对比:两种优化路径

方案原理改动量风险预期效果
方案A:优化锁粒度将全局锁改为分段锁(shard lock)中等(改PHP扩展代码)减少锁等待时间
方案B:替换为无锁数据结构用RCU(Read-Copy-Update)替代互斥锁高(需要重写缓存模块)完全消除锁竞争

选择方案A:改动量小,风险可控,且能解决90%的问题。方案B作为后续迭代方向。

代码实现:分段锁优化APCu

原代码(简化版)

// 全局锁,所有操作串行
class ApcuCache {
    private static $lock;
    public function get($key) {
        self::$lock = fopen('/tmp/apcu.lock', 'w');
        flock(self::$lock, LOCK_EX);
        $result = apcu_fetch($key);
        flock(self::$lock, LOCK_UN);
        return $result;
    }
}

优化后:分段锁

// 分段锁,根据key哈希到不同锁
class ShardedApcuCache {
    private static $locks = [];
    private static $numShards = 16; // 16个分段
    
    public function get($key) {
        $shardId = crc32($key) % self::$numShards;
        if (!isset(self::$locks[$shardId])) {
            self::$locks[$shardId] = fopen("/tmp/apcu_shard_{$shardId}.lock", 'w');
        }
        flock(self::$locks[$shardId], LOCK_EX);
        $result = apcu_fetch($key);
        flock(self::$locks[$shardId], LOCK_UN);
        return $result;
    }
}

压测脚本

# 使用wrk压测,模拟800QPS
wrk -t8 -c100 -d60s --latency http://localhost/api/order/create
# 结果对比
# 优化前:P99 2.3s, 错误率5.2%
# 优化后:P99 120ms, 错误率0.1%

内核参数调优

同时调整内核参数,减少锁竞争导致的上下文切换。

# 调整futex相关参数
echo 100000 > /proc/sys/kernel/futex_wait_wake_nr
# 减少调度器延迟
echo 0 > /proc/sys/kernel/sched_autogroup_enabled
# 调整CFS调度器时间片
echo 1000000 > /proc/sys/kernel/sched_latency_ns
# 持久化到sysctl.conf
cat >> /etc/sysctl.conf <

效果数据:优化前后对比

指标优化前优化后提升
P99延迟2.3s120ms95%
CPU毛刺频率每5分钟一次0次/24小时100%
系统调用耗时(futex)平均2.3ms平均0.1ms96%
上下文切换次数12000次/秒3500次/秒71%
错误率5.2%0.1%98%

压测环境:8C16G虚拟机,PHP 8.3.6,Nginx 1.24.0,MySQL 8.0.35,wrk 4.2.0。连续压测1小时,采样间隔1秒。

避坑指南:我踩过的5个坑

  • 坑1:strace -f 导致进程卡死 - 第一次用strace -f追踪所有子进程,结果PHP-FPM直接hang住。原因是strace会阻塞系统调用,高并发下导致死锁。解决方案:只追踪主进程,或者用-e trace=mutex限制追踪范围。
  • 坑2:perf采样频率过高 - 用-F 9999采样,结果perf本身占用了30% CPU。正确做法:-F 99(每秒99次)足够,对性能影响小于1%。
  • 坑3:火焰图误判 - 第一次看火焰图,以为zend_mm_alloc是内存泄漏,其实是锁竞争导致的内存分配等待。需要结合strace和perf一起看。
  • 坑4:分段锁数量选择 - 一开始用4个分段,效果不明显。测试发现16个分段最优,超过32个反而因为锁文件过多导致性能下降。
  • 坑5:内核参数调优后重启失效 - 直接echo到/proc只对当前会话有效,必须写入/etc/sysctl.conf并sysctl -p才能持久化。而且sched_autogroup_enabled=0在CentOS 7.9上需要重启cgroup服务才生效。

原理深入:为什么锁竞争导致CPU毛刺?

CPU毛刺的本质是大量线程同时等待同一把锁。当锁被持有时,其他线程进入futex系统调用,内核将这些线程挂起。锁释放时,内核唤醒所有等待线程,导致惊群效应(thundering herd)。大量线程同时被唤醒,争抢锁,造成CPU瞬间飙升。

分段锁的原理:将一把锁拆成多把,每个key只竞争其中一把。这样锁冲突概率从O(n)降到O(n/numShards)。在800QPS场景下,16个分段将冲突概率降低到原来的1/16。

内核参数sched_latency_ns控制CFS调度器的目标延迟,调大后减少调度次数,降低上下文切换开销。futex_wait_wake_nr限制每次唤醒的线程数,减少惊群效应。

总结

CPU毛刺排查路线:top看现象 → sar确认规律 → strace定位系统调用 → perf+火焰图找热点函数 → 代码优化+内核调优。分段锁方案在支付场景验证有效,P99从2.3s降到120ms,毛刺完全消失。记住:先定位再优化,别上来就改代码。

IT搬运工

专注技术分享,坚持原创深度内容。

分类
链接
订阅

RSS 订阅关注最新文章

© 2026 IT搬运工 · 站点地图 · 鄂ICP备19007640号-3