news 2026/9/26 5:08:16

用eBPF破解Nginx偶发高延迟:连接跟踪锁竞争排查实录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用eBPF破解Nginx偶发高延迟:连接跟踪锁竞争排查实录

半个月前,我遇到一个印象很深的线上问题:反向代理层Nginx的RT(响应时间)突然从 10ms 左右飙到 300ms,而且不是持续高,是随机偶发跳变。第一反应是后端节点抖动,翻了一圈监控,后端响应没问题;第二反应是网络问题,从客户端到服务器 ping 却稳定在 0.2ms。最后只能登录机器用常规工具排查,结果什么都看不出来。折腾到后来我意识到,这类“偶发、高延迟、系统资源看不出异常”的问题,大概率不在应用层,而在内核态。于是我搬出 eBPF,花了几分钟就在内核里锁定了真凶:连接跟踪模块的锁竞争。

这篇文章会把完整的排查思路、实操命令和根因讲清楚。Nginx、高延迟、内核,这三个词放在一起,最容易让人陷入“怀疑配置、怀疑网络、怀疑代码”的死循环,而 eBPF 的价值恰恰是把隐藏在系统调用之下、TCP 协议栈内部、甚至网络过滤钩子里的耗时热点原样暴露出来。适合正在折腾性能问题、想了解可观测性的后端和运维同学看。

1. 现象复现:诡异的 Nginx 高延迟从哪来

1.1 业务背景与最初判断

当时架构很简单:一台 Nginx 服务器作为反向代理,接收外部网关流量,转发给几个内网后端服务。流量不算大,每秒大概两千到三千请求,连接数也就几千,CPU 负载常年在 20% 以下。Nginx 配置也中规中矩,worker_processes auto,keepalive开了,后端连接池也做了,怎么看都不像会出问题的样子。

但监控曲线非常难看:平均 RT 只有 15ms 左右,P99 却时不时冲到 300ms 甚至 500ms。我把时间线和多个周期的数据叠在一起看,发现高延迟不是固定某个时段,而是随机出现在白天和晚上的任意时间,持续几秒到几十秒又自己恢复。这种“毛刺型”延迟最烦人,因为 reducers 和告警经常被触发,值班同事被叫起来熬夜,可等我们登录机器,问题往往已经消失了。

第一轮排查集中在应用层。检查后端服务:耗时不正常吗?没有,后端日志显示它们的处理时间很平稳。检查Nginx日志:$request_time高的时候$upstream_response_time却很正常,说明耗时不在上游。检查网络:客户端到 Nginx 的ping和mtr都没有丢包和跳变。于是怀疑是 Nginx worker 本身阻塞,重启 Nginx 能好转几分钟,但问题照旧。

1.2 常规排查手段为什么失效

在拿到 eBPF 之前,我用了几乎所有常规套路,这里直接说结论:

  • top/htop:CPU 占用完全正常,没有软中断飙高,没有 worker 100% 跑满。
  • ss -tn:查看 TCP 连接状态,没看到大量 TIME_WAIT、SYN_RECV 堆积,accept 队列也没有溢出。
  • netstat -s:TCP 层没有明显的丢包、重传计数器暴涨,ListenOverflows偶尔几个,但不是大量增长。
  • strace -p <nginx_pid>:跟踪 worker 的系统调用,看到的无非是accept、epoll_wait、read、write,没有任何一个阻塞超过几十毫秒的系统调用。
  • perf top:抓内核热点,函数排行里根本没看到明显的异常,只有native_write_msr、__x64_sys_epoll_wait之类常见项。

乍一看这服务器“健康得离谱”。但延迟是真实存在的,问题一定在某个我们看不到的地方。

后来我想明白了:strace只能看到用户态进程进入内核的“边界”,看不到系统调用返回之后内核继续跑的某段逻辑;perf top偏采样统计,适合被频繁调用的热点,而这种偶发、短促、由特定数据包路径触发的延迟,很容易被稀释掉。要精确定位,必须找一个能挂在内核函数入口和出口,按请求或者按包去统计耗时的黑科技——这就是 eBPF。

2. eBPF 为什么能解决这类问题

2.1 eBPF 本质:在内核里跑一段“安全探针”

eBPF,扩展 Berkeley Packet Filter,最早源于抓包优化,但现在已经变成一套通用的内核动态追踪框架。你可以把它理解成往内核的关键路径上插微型探针,不用改内核源码、不用重新编译、不需要重启系统。探针由内核验证器检查安全性后,通过 JIT 编译成原生指令运行。

传统内核模块也能做这件事,但需要自己管理内存、锁、生命周期,一个野指针就能把整台服务器搞死。eBPF 探针运行在受限沙箱里,有复杂度限制、有内存访问边界校验、有死循环检测,所以生产环境也可以放心临时挂载。对我这种“只想查个问题、没有勇气写内核模块”的人来说,完全是救命级别的能力。

我这次用到的核心挂载点是kprobe,也就是内核函数探针。kprobe可以在任意非内联内核函数入口插入自定义代码,kretprobe则捕获函数返回。两者配合,可以精确算出某段内核代码的执行耗时。除了 kprobe,还有 tracepoint(内核预置的稳定跟踪点)、fentry/fexit(基于 BPF 的轻量探针)等可选,但 kprobe 的最大好处是无所不贴,找问题时最灵活。

2.2 它到底“看”到了什么

拿这次的问题来说,我需要在 Nginx worker 收包路径上找到真实瓶颈。网络收包从网卡驱动、到协议栈、到 Nginx 之间会经历很多函数:tcp_v4_rcv、ip_rcv、nf_conntrack_in、tcp_v4_syn_recv_sock……任何一个函数出现瓶颈,都会影响整体延迟,但它们在常规监控里都没有独立指标。

eBPF 的另一个强大之处在于可以按 CPU、按进程、按函数聚合分布。比如我写一个探针统计tcp_v4_rcv的耗时直方图,能立刻看到是不是绝大多数请求都很短,只是尾部有一批很长的异常点;再按 CPU 核分开,又能判断是不是某个核的中断处理失衡。这种“分布”比平均值、最大值更能反映问题本质。

bpftrace是我最推荐的上手解析工具,它提供类似 awk 的语法,几行就能写完一个探针脚本。如果你不想手写 bpftrace,还可以用 BCC 里的现成工具,比如funclatency、trace、profile,一行命令就能输出某个内核函数的耗时分布。下面我完整还原 5 分钟定位过程。

3. 五分钟定位真凶:实操全过程

3.1 准备:确认内核支持 eBPF 并安装工具

先说明环境:Linux 内核 5.4,发行版是 CentOS 7(生产环境,不能随便重启)。eBPF 在内核 4.9 以后基本可用,5.x 更加完善;如果内核较老或者没有开启 BTF,可以使用 kprobe 方式作为兜底。

安装 bpftrace:

# CentOS 7 / 8 使用 yum 安装 yum install -y bcc bpftrace # Debian/Ubuntu 使用 apt apt install -y bpfcc-tools bpftrace

如果没有现成软件源,也可以下载安装包或用源码编译,但通常多花点时间。装完后先用bpftrace -l 'kprobe:tcp_v4_rcv'列出目标符号,确认探针可挂载。注意不要在生产环境同时挂太多探针,否则 CPU 开销会放大。

检查权限:eBPF 需要 root 用户,或者至少具备CAP_BPF、CAP_SYS_ADMIN。容器场景下需要给 privileged 或者显式添加 capabilities,否则挂载时会遇到Operation not permitted。

3.2 先确认延迟现象确实存在于内核路径

为了量化耗时到底发生在哪个阶段,我从 Nginx 的收包入口tcp_v4_rcv开始追踪,统计这个函数从进入到返回的耗时分布。命令如下:

bpftrace -e 'kprobe:tcp_v4_rcv { @start[tid] = nsecs; } kretprobe:tcp_v4_rcv /@start[tid]/ { @rcv_usecs = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'

正常情况下,tcp_v4_rcv只是在 TCP 接收路径上做一些状态处理,耗时应该是微秒级。但结果让我精神一振,直方图长这样:

@rcv_usecs: [1, 2) 124766 [2, 4) 31645 [4, 8) 2334 [8, 16) 569 [16, 32) 128 [32, 64) 14 [64, 128) 9 [512, 1024) 4 [4096, 8192) 2

绝大部分请求都在几微秒内处理完,但尾巴上出现了 4 到 8 毫秒的超大值。这就是我们要追的“幽灵延迟”。接着我又按 CPU 拆了一遍,发现异常值集中在同一块 CPU 的softirq处理路径上,说明不是 Nginx 用户态进程主动阻塞,而是内核在处理网络包时偶发卡顿。

3.3 用 bpftrace 聚焦“腐败”嫌疑函数

tcp_v4_rcv只是一个容器,真正耗时要继续往下钻。这台服务器为了满足业务的外网访问,iptables 规则里开了 NAT 和部分转发,所以我第一怀疑对象就是连接跟踪模块nf_conntrack_*。这里有个背景知识:只要加载了nf_conntrack,并且有数据包要判定是否属于已有连接,那么每个首次进入的连接或者每个被 NAT 转发的报文,都会经过nf_conntrack_in和nf_conntrack_confirm这类函数。连接多、表项多、锁竞争严重时,这里会成为灾难。

继续对nf_conntrack_in做耗时统计:

bpftrace -e 'kprobe:nf_conntrack_in { @start[tid] = nsecs; } kretprobe:nf_conntrack_in /@start[tid]/ { @ct_usecs = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'

结果非常有说服力:

@ct_usecs: [1, 2) 18520 [2, 4) 3471 [4, 8) 228 [8, 16) 45 [16, 32) 12 [32, 64) 6 [64, 128) 8 [256, 512) 5 [512, 1024) 3 [4096, 8192) 2

注意,nf_conntrack_in的单次耗时居然能到 8 毫秒,对于每次请求只是查一下表而已,这个数值反常到了什么程度?类比一下,你每天路过小区门口的快递柜,正常时 5 秒就能取走一个件,但现在偶尔要站在柜门前等 8 分钟,因为柜门锁卡住了还排队。

再用 BCC 工具funclatency快速验证一遍,避免是我手写探针的问题:

funclatency -m nf_conntrack_in

输出同样显示大部分调用耗时小于 1ms,但存在极端分布。到这里,问题范围基本收窄:高延迟来自连接跟踪模块的偶发严重耗时。

3.4 找到真凶:连接跟踪表锁竞争

既然怀疑是锁竞争,就需要再抓一层。连接跟踪表里查找的时候会调用__nf_conntrack_find,它需要遍历哈希槽位的链表,并且要持有对应锁。我挂探针统计它的耗时:

bpftrace -e 'kprobe:__nf_conntrack_find { @start[tid] = nsecs; } kretprobe:__nf_conntrack_find /@start[tid]/ { @find_usecs = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'

结果让我直接确认根因:

  • 首先,__nf_conntrack_find的命中率非常高,说明几乎每个包都要走一次查找。
  • 其次,它的耗时分布和nf_conntrack_in高度吻合,同样是几十微秒到几毫秒都有。
  • 最关键的证据:当我用bpf_trace_printk临时记录一次较长耗时的调用栈,看到/sys/module/nf_conntrack/parameters/hashsize只有 65536,但当前活跃的 conntrack 条目数是net.netfilter.nf_conntrack_count,达到了 6 万多,哈希槽的平均冲突链长度接近 1,高峰时某些槽位链长超过 50。

冲突链长了之后,查找需要遍历链表的时间变长;更致命的是并发情况下同一槽位的锁需要被不同 CPU 互相争抢,一旦 CPU 忙不过来,处理网络包的软中断就会被卡住,Nginx 那一个请求的延迟直接就上去了。这个现象在我用mpstat -P ALL 1观察时也有印证:异常的毫秒级延迟期间,某个 CPU 的softirq占比短暂跳到 90% 以上。

顺手验证了dmesg,果然看到过nf_conntrack: table full, dropping packet的日志。虽然只在连接数打满时偶尔出现,但佐证了连接跟踪表确实处在“濒临爆满”的状态。

3.5 处理方案与效果

根因清晰后,处理就不复杂了,三步走:

  1. 确认这台 Nginx 服务器上,真正需要 NAT 的流量只有某几个网段,其他流量完全是内部转发,不需要做连接跟踪。
  2. 在 iptables 的 raw 表里加上NOTRACK规则,让大部分流量绕过nf_conntrack,只保留必要部分的连接跟踪。
  3. 同时调大连接跟踪表参数,避免未来扩展时再次触发瓶颈。

具体命令大致如下:

# 查看当前连接跟踪表状态 sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max # 调大表上限和哈希桶大小(在启动脚本或 sysctl 文件里持久化) sysctl -w net.netfilter.nf_conntrack_max=1048576 echo 262144 > /sys/module/nf_conntrack/parameters/hashsize # 对不需要跟踪的接口流量设置 NOTRACK iptables -t raw -A PREROUTING -i eth0 -p tcp --dport 443 -j NOTRACK iptables -t raw -A PREROUTING -i eth0 -p tcp --dport 80 -j NOTRACK

注意,hashsize修改对已有表不生效,需要先清理或重启服务,所以我建议是在低峰期操作。如果只是临时救火,可以先设置nf_conntrack_max增大容量,再重点观察锁问题是否缓解。

调整后,我再跑了一遍之前的bpftrace探针,__nf_conntrack_find的最大耗时从几毫秒降回了几十微秒,Nginx 的 P99 也从 300ms 回落到 15ms 左右。在这个案例里,Nginx 根本不是问题所在,它只是一个无辜的“受害者”。

4. 常见问题与排查技巧实录

4.1 eBPF 探针使用时的几个坑

把这次操作复盘一下,有几个容易被新手忽视的坑。

第一个是kprobe 的稳定性问题。kprobe 是在内核函数入口动态修改指令,如果函数被标记为noline或被优化内联,探针可能挂不上或者误挂到奇怪的位置。所以生产环境挂探针之前,尽量先用bpftrace -l 'kprobe:foo*'确认符号存在。另外,不要长时间大面积挂 kprobe,它会影响 CPU 指令缓存和流水线,导致性能损耗。像这次排查,单段脚本跑几分钟足够了。

第二个是内核版本符号差异。不同版本里nf_conntrack_in的符号名可能不一样,老版本还可能是nf_conntrack_in,更新内核里可能变成nf_conntrack_in的 inline 展开,或者有新的追踪点。建议直接使用 bpftrace 的通配符:

bpftrace -l 'kprobe:nf_conntrack*'

先把候选符号拉出来,再精确定位。这个动作花不了一分钟,但能省下大量“探针明明挂上却不触发”的疑惑。

第三个是探针脚本里 map 竞争的问题。在多个 CPU 同时命中 kprobe 时,如果使用全局变量共享 start 时间,可能出现互踩。我在上面的例子里用tid(线程 ID)作为 map key,保证同一个 CPU 上嵌套调用不串数据。如果你想更严谨,可以用cpu作为 key,写一个嵌套计数器。不过大多数场景下tid够用。

还有一个容易忽略的点:bpftrace 输出 hist 时,单位前缀要注意。上面我除以 1000 得到微秒,看到直方图里[4096, 8192)就是 4 到 8 毫秒;如果不除以 1000,默认按纳秒统计,你会看到一长串 0 到几百万纳秒,反而不好读。

4.2 没有 eBPF 时的替代排查路径

如果你手上的老内核实在不支持 eBPF,也可以用传统手段缩小范围,但效率会低不少。

  • perf trace:可以跟踪系统调用,但看不到内核内部函数。
  • perf record -g -a:全系统采样加调用栈,适合高频热点,对偶发延迟的命中率较低。
  • ftrace:老牌动态追踪框架,可以开 function graph 跟踪指定函数,但输出量大、上手门槛比 bpftrace 高。
  • netstat -s、dmesg、conntrack -S:用于观察连接跟踪模块的统计和丢包,虽然没有函数级精度,但能给出方向性线索。

如果确认是连接跟踪问题,直接看这几个指标最有价值:

指标正常危险信号
nf_conntrack_count远小于 max 的一半接近nf_conntrack_max
nf_conntrack_max按业务流量合理预留过小导致表满丢包
__nf_conntrack_find耗时分布几乎全部 < 50 微秒存在大量 >1ms 尾部
conntrack -S的insert_failed和drop0 或极低持续增长说明丢连接
单 CPU softirq 占比多核均衡某个核频繁飙高

我这里也分享一个经常被忽视的细节:很多机器上的 conntrack 默认表上限是 65536,看起来不小,但如果你一个 HTTP 请求会经过INPUT、FORWARD、OUTPUT等多个方向,再加上服务主动连后端时创建新的连接表项,几千 QPS 就能很快吃满。解决方向从来不是“把表调大”一条路,最大化收益是绕过它。对无需状态过滤和 NAT 的路径,尽量用 raw 表的NOTRACK剪掉跟踪负担;能不开 NAT 就少开 NAT;能升级到 nftables 就升级,它比 iptables 更轻量、维护更友好。

5. 写在最后的个人体会

这次排查之后,我养成了一个习惯:遇到偶发高延迟,不再只盯着 Nginx 日志和 CPU 平均负载,而是先问一个问题——“应用层和系统资源都正常,那么时间究竟消耗在内核的哪段路上?” eBPF 给我的不只是工具层面的能力提升,更重要的是一种思维转变:系统调用的边界不是排查的终点,内核函数本身也应该被观测。

如果你也想上手这套技能,建议先在测试环境跑跑 bpftrace 的官方示例,重点理解hist、count、kprobe这几个基础概念。后面遇到真正线上问题时,你就能像我这次一样,挂上探针、看直方图、缩小范围、找到真凶,整个过程可能也就一杯咖啡的功夫。最后再分享一个小技巧:如果 bpftrace 脚本里某段逻辑不生效,先用printf("hit\n")打印一下触发点,确认探针确实在被调用,再往下加统计逻辑。排查内核问题,先证明自己看到的是真相,再去找答案,能少走很多弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 5:08:09

黑神话悟空xrnm.dll缺失怎么办?详解运行库修复与DLL报错排查指南

开头“无法启动&#xff0c;因为计算机丢失xrnm.dll”或者“找不到xrnm.dll”这类弹窗&#xff0c;最近在黑神话悟空玩家群里可以说是高频出现。这截图一甩出来&#xff0c;懂行的会说一句“典型的运行库问题”&#xff0c;不懂行的直接慌掉&#xff0c;以为游戏文件坏了要重装…

作者头像 李华
网站建设 2026/9/26 5:08:06

8G显存实战Qwen3 27B:GGUF量化与Ollama调优指南

1. 为什么8G显存跑27B模型这件事值得认真聊先把结论摆在前面&#xff1a;8G显存跑Qwen3 27B这个级别的模型&#xff0c;不是玄学&#xff0c;也不是营销话术&#xff0c;但它确实有明确的前提条件——你得用对量化格式、配对推理框架、并且接受一定的速度妥协。我前后折腾了大概…

作者头像 李华
网站建设 2026/9/26 5:08:04

基于一维非稳态传热的回焊炉炉温曲线建模与优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 5:07:35

浏览器端语义判断实战:OpenJev多模型对比框架设计与性能优化

1. 为什么要在浏览器里做语义判断第一次看到 OpenJev 这个项目标题的时候&#xff0c;我脑子里冒出来的第一个念头是&#xff1a;为什么非得是浏览器&#xff1f;语义判断这件事&#xff0c;放在服务端做不是更省事吗&#xff1f;模型权重不用下载、算力不用愁、版本更新也方便…

作者头像 李华
网站建设 2026/9/26 5:06:58

SpringBoot+Vue贸易行业CRM系统开发实战:从表设计到部署

1. 为什么选SpringBootVue做贸易行业CRM&#xff1a;一次课设到实战的完整复盘如果你正在为毕业设计、课程设计或者单纯想系统学一遍前后端分离开发而发愁&#xff0c;拿“贸易行业CRM系统管理平台”当项目载体&#xff0c;是我比较推荐的路子。原因很简单&#xff1a;CRM这个业…

作者头像 李华
网站建设 2026/9/26 5:06:26

医疗细胞图像分割:UNet-2D实战与部署避坑指南

简介&#xff1a;本资源是一套面向医学图像处理研究者与AI初学者的细胞分割实战项目&#xff0c;聚焦UNet-2D模型在二维显微图像中的精准细胞边界识别任务&#xff0c;适用于病理分析、细胞计数及教学实验等场景。压缩包共15个文件&#xff0c;含4个核心Python脚本&#xff08;…

作者头像 李华