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抓取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是用户态锁,说明锁竞争严重。
# 采样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作为后续迭代方向。
// 全局锁,所有操作串行
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.3s | 120ms | 95% |
| CPU毛刺频率 | 每5分钟一次 | 0次/24小时 | 100% |
| 系统调用耗时(futex) | 平均2.3ms | 平均0.1ms | 96% |
| 上下文切换次数 | 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秒。
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,毛刺完全消失。记住:先定位再优化,别上来就改代码。
专注技术分享与实战