性能调优怎么做:P99 毛刺从 1.8s 降到 130ms,cgroup v2 限流与 eBPF 排查步骤

🔑 关键词:性能调优,P99尾延迟,cgroup v2,eBPF,Linux性能排查

📖 摘要:从一次 8C16G Go 服务 P99 毛刺入手,讲清排队论、cgroup v2 限流、CPU steal、中断亲和、网络与磁盘参数,给出可复现命令和参数取舍。

先吐槽一句:我特别烦那种“十大 Linux 性能参数”文章。抄完 net.core.somaxconn=65535、vm.swappiness=0,线上 P99 该毛刺还是毛刺。2023 年我在一个 8 vCPU / 16GB 的 Go 服务上就吃过亏:压测 QPS 从 4200 掉到 1800,P99 从 200ms 蹦到 1.8s,top 里 CPU 总利用率只有 38%。我一开始也去翻网络参数,结果真正的问题是 cgroup v2 的 cpu.max 被写成了 200000 100000,也就是 2 核配额。服务开了 16 个 worker,GC 和网络轮询抢 CPU,cpu.stat 里我每 10 秒采样一次,nr_throttled 增量 3000 多次,throttled_usec 累计 1.2 秒。把配额改成 600000 100000 后,P99 降到 130ms,QPS 回到 4800。

图片

这件事让我形成个不太主流的观点:性能调优的第一目标不是把 CPU、内存、磁盘利用率打满,而是控制排队。M/M/1 排队模型里,利用率 ρ 到 0.5 时,平均等待约等于 1 个服务时间;ρ 到 0.7,等待变成 2.33 倍;ρ 到 0.9,等待直接 9 倍。很多人看着 CPU 90% 觉得“很健康”,其实尾延迟已经在爆炸边缘。所以我更愿意把资源利用率压在 60%-70%,留 30% 给突发流量、GC、页回收、中断。这不是浪费,是给 P99 买保险。

那具体怎么排查?我现在的顺序是:先定 SLO,再看 PSI 和 cgroup,然后才是应用、网络、磁盘参数。SLO 别写“尽量快”,写成 P99 < 200ms、错误率 <0.1%、吞吐 >3000 QPS。Linux 4.20+ 可以看 /proc/pressure/cpu、/proc/pressure/io、/proc/pressure/memory,some avg10 比 load 更早暴露资源争抢。容器里必看 /sys/fs/cgroup/<你的路径>/cpu.stat,重点不是 usage_usec,而是 nr_throttled 和 throttled_usec。如果 10 秒内 throttled_usec 超过 200000(200ms),基本可以判定被限流。云主机再看 top 的 st 或 sar -u 1,steal 超过 5% 就别自己瞎调了,找云厂商。

图片

我常用的命令很土,但够用:

  • pidstat -u -r -w 1:看 CPU、缺页、上下文切换。上下文切换 cswch/s 超过 10 万就要查锁或线程模型。
  • iostat -x 1:SSD 别只看 %util,看 await 和 aqu-sz。4K 随机读 await 超过 5ms 就要注意,NVMe 超过 1ms 已经算高。
  • ss -s 和 ss -ti:看重传、cwnd、rtt。retrans 上升往往比带宽打满更影响 P99。
  • bpftrace:runqlat 看调度排队,biolatency 看块设备延迟,profile 做火焰图。bpftrace -e 'kprobe:tcp_retransmit_skb { @[comm] = count(); }' 能快速抓重传。
  • perf top -g 或 perf record -F 99 -p <pid> -g -- sleep 30,然后 perf script | stackcollapse-perf.pl | flamegraph.pl > out.svg。

图片

这里有个对比:物理机时代,CPU 使用率高往往就是进程真的在算;容器时代,CPU 使用率 40% 也可能被 cgroup 在 100ms 周期内掐死。因为 cpu.max 是按周期重置的,前 20ms 突发把配额花完,后面 80ms 全部排队。你看到的是周期平均,用户感受到的是周期尾部。这也是为什么我优先看 throttled_usec,而不是 top。

再说网络参数。net.core.somaxconn=4096、net.ipv4.tcp_max_syn_backlog=8192、net.core.netdev_max_backlog=16384 这些可以调,但要和应用 backlog 对齐。Nginx 要写 listen 80 backlog=4096;,Go 的 http.Server 也要看 ReadTimeout、WriteTimeout、IdleTimeout。tcp_tw_reuse=1 只在客户端或 NAT 环境谨慎开,tcp_tw_recycle 早被移除了,别抄 2015 年的文章。磁盘那边,NVMe 调度器用 none,SATA SSD 用 mq-deadline,挂载加 noatime,nodiratime。vm.swappiness=10、vm.dirty_ratio=15、vm.dirty_background_ratio=5 比无脑 swappiness=0 更稳。

图片

应用层我最想说的是连接池和线程池。很多人 QPS 一高就把池子从 100 加到 500,结果 P99 更差。算一下:QPS 1200,P95 处理时间 45ms,并发需求 = 1200 × 0.045 = 54。考虑 1.2-1.5 倍冗余,连接池设 64-96 就够。线程池同理,不是越大越好,队列一定要有界,比如长度 100、超时 200ms,否则排队时间会吃掉所有 SLO。JVM 可以看 G1 的 MaxGCPauseMillis=200,但别指望它解决锁竞争。Go 服务注意 GOMAXPROCS,容器里最好显式设成 cgroup 配额核数,比如 6,而不是宿主机 64。

图片

压测方法也影响结论。我一般用 wrk -t4 -c200 -d300s --latency http://target/,至少跑 5 分钟,记录 P50/P95/P99。再做梯度:并发 50、100、200、400。如果 200 到 400 时 QPS 不涨、P99 翻倍,那就是拐点,别再加机器,先找排队点。改参数必须一次只改一个,A/B 至少 30 分钟,否则你根本不知道是哪个参数起效。

最后说取舍。innodb_flush_log_at_trx_commit=2 能把写入吞吐拉高 20%-30%,但可能丢 1 秒数据;sync_binlog=0 类似。金融账务别碰,日志类业务可以谈。THP 对 Redis 有时造成延迟尖刺,官方建议关,但关了对某些大内存应用又有 TLB 开销。没有万能参数。我现在更愿意把性能预算拆开:P99 200ms 里,应用逻辑 60ms、GC 30ms、磁盘 40ms、网络 40ms、排队 30ms。谁超了修谁。性能调优不是玄学,是一笔一笔算账。

图片

如果只记一句话:别追求利用率 100%,那是给 P99 挖坑。把 cgroup 限流、PSI、runqlat、biolatency 这几个指标盯住,先把排队压下去,再谈参数。我承认这套方法不够酷,但比抄参数靠谱。

🏷️ 标签: