eBPF 实战:一次诡异的 Nginx 高延迟,我用 5 分钟在内核里找到了真凶

eBPF 实战:一次诡异的 Nginx 高延迟,我用 5 分钟在内核里找到了真凶

没有玄学,只有内核里的铁证如山。
P99延迟无故飙升,CPU空闲,网络通畅,文件I/O正常------那一刻,我怀疑自己遇到了灵异事件。

01 诡异的下午:什么都没变,服务却慢了10倍

那天下午,监控告警突然响起:线上Nginx网关的P99延迟从稳定的50ms飙升至500ms以上。

第一反应是查看常规指标:

  • CPU:idle 30%以上,没爆。
  • 内存:充足,无swap。
  • 网络:带宽未打满,TCP重传率正常。
  • 磁盘I/O:util 5%,几乎没有读写。
bash 复制代码
# 传统三板斧
top        # CPU空闲
free -m    # 内存充足
iostat -x 1 # I/O空闲
ss -s      # 连接数正常,TIME-WAIT不多

诡异之处:服务没重启,流量没突增,代码没发布,但延迟就是高了。这种"三无"问题,就像家里没开大功率电器,电表却飞转,让人无从下手。

02 常规手段失效:黑盒排查的尽头

既然外围指标正常,只能钻进内核看个究竟。此时,eBPF是我最后的希望------无需修改代码,无需重启服务,直接在内核里装"监控探头"。

第一板斧:BCC工具集,快速画像

我首先祭出BCC工具集(BPF Compiler Collection),几个命令迅速定位异常进程:

bash 复制代码
# 追踪新进程创建(怀疑有短时进程耗资源)
execsnoop    # 无异常,没有频繁的进程创建

# 追踪文件打开(怀疑读配置频繁)
opensnoop -n nginx  # 正常,只打开几次

# 追踪慢文件系统操作(看是不是I/O慢)
ext4slower   # 无慢查询,排除文件系统

一通操作下来,依旧毫无头绪。Nginx worker进程既没有疯狂开文件,也没有频繁fork,那延迟到底卡在哪里?

第二板斧:bpftrace,给内核函数上"秒表"

既然宏观工具看不清,我决定用bpftrace直接测量内核函数耗时。在Linux内核网络栈中,accept系统调用是TCP连接建立的最后一步,如果这里变慢,请求必然延迟。

写一个bpftrace脚本,测量Nginx worker进程执行accept函数的耗时:

bash 复制代码
#!/usr/bin/env bpftrace

uprobe:/usr/sbin/nginx:ngx_event_accept
{
    @start[pid] = nsecs;
}

uretprobe:/usr/sbin/nginx:ngx_event_accept
/@start[pid]/
{
    $time = (nsecs - @start[pid]) / 1000;  # 微秒
    @accept_us = hist($time);
    delete(@start[pid]);
}

执行脚本,等待几秒,查看直方图:

bash 复制代码
bpftrace accept_latency.bt
^C
@accept_us:
[0]                 3120 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[1]                 1023 |@@@@@@@@@@@@@@@                                     |
[2, 4)               321 |@@@@                                                |
[4, 8)                87 |@                                                   |
[8, 16)               12 |                                                    |
[16, 32)             522 |@@@@@@@@                                            |  <-- 居然有大量耗时超过16微秒的调用!
[32, 64)            1502 |@@@@@@@@@@@@@@@@@@@@@@@                             |  <-- 甚至超过32微秒!

真相初现 :正常情况下,accept只是一次内存拷贝,应该稳定在1微秒以内。但此时出现了大量超过16微秒的调用,最慢的甚至超过64微秒------accept函数本身变慢了

03 顺藤摸瓜:谁在拖延accept?

accept慢,说明内核在接受连接时被阻塞了。继续向下追踪:在Linux内核中,accept最终会调用inet_csk_accept。我再次用bpftrace挂载这个内核函数:

bash 复制代码
bpftrace -e 'kprobe:inet_csk_accept { @start[pid] = nsecs; }
    kretprobe:inet_csk_accept /@start[pid]/ {
        @accept_latency = hist(nsecs - @start[pid]); delete(@start[pid]); }'

结果与用户态一致:内核函数同样存在延迟尖峰。这说明,阻塞发生在内核更深处

查阅内核源码(顺带一提,Nginx社区也有关于listen socket和eBPF的讨论),怀疑对象指向监听队列锁竞争 。在inet_csk_accept函数中,获取accept_queue锁的竞争可能导致进程睡眠等待。

为了验证锁竞争,我尝试挂载内核中的锁函数:

bash 复制代码
# 追踪锁等待时间(理论示例,实际需根据内核版本调整)
bpftrace -e 'kprobe:spin_lock { @lock_wait = hist(nsecs - ...); }'

虽然锁的追踪较为复杂,但结合上下文,高并发下,多个worker进程争抢同一个监听套接字的队列锁,导致部分进程休眠等待 ,从而引发accept延迟。这才解释了为什么P99飙升而CPU空闲------线程在等锁,不是在跑指令。

04 破案:SO_REUSEPORT的代价

真凶锁定 :Nginx采用了SO_REUSEPORT多进程监听同一端口,内核负载均衡将新连接分发到多个worker。但在极端并发下,内核内部的队列锁(如listen socket的syn queue和accept queue锁)成为争抢热点 。当某个worker运气不佳,被调度到与其它worker同时争锁,就会进入休眠等待,导致accept调用延迟数十微秒,进而拉高整体P99延迟。

05 复盘:eBPF为何能破案?

回顾整个过程,eBPF的价值在于提供了无侵入的内核上下文观测能力

  1. 全栈可见:从用户态函数(ngx_event_accept)到内核态函数(inet_csk_accept),再到潜在的锁竞争,完整追踪调用链路。
  2. 精准量化:不是泛泛地看CPU使用率,而是精确测量每一个关键函数的耗时分布。
  3. 低 overhead:生产环境也可用,只采集必要数据,不拖垮系统。

写在最后

那次故障之后,我对"黑盒"二字有了新的理解。所谓黑盒,不是内核真的不可知,而是我们手头没有合适的工具去观察它。eBPF就像一束光,照进了内核的每一个角落。

当你下次再遇到"什么都没变,但服务就是慢了"的灵异事件时,不妨问问自己:是时候看看内核里发生什么了。因为在这个时代,没有真正的黑盒,只有尚未使用的eBPF。


小提示 :如果你也遇到了类似问题,不妨从execsnoopopensnoopxfsslower等BCC工具开始,再用bpftrace深入函数级耗时分析。内核里没有秘密,只看你愿不愿意去寻找。

相关推荐
志栋智能2 小时前
安全超自动化的终极目标:实现自适应安全防护
运维·人工智能·安全·自动化
李白的粉2 小时前
基于springboot的桂林旅游景点导游平台
java·spring boot·毕业设计·课程设计·源代码·桂林旅游景点导游平台
xyd陈宇阳2 小时前
面向网络协议初学者的入门指南
linux·运维·网络协议
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 花卉交易系统为例,包含答辩的问题和答案
java
weixin_704266052 小时前
Spring整合MyBatis(一)
java·spring·mybatis
翘着二郎腿的程序猿2 小时前
Maven本地化部署与使用全指南
java·maven
历程里程碑2 小时前
Linux 49 HTTP请求与响应实战解析 带http模拟实现源码--万字长文解析
java·开发语言·网络·c++·网络协议·http·排序算法
慧天城寻2 小时前
H3C巡检命令与避坑技巧
运维·网络·运维开发
IronMurphy2 小时前
【算法二十】 114. 寻找两个正序数组的中位数 153. 寻找旋转排序数组中的最小值
java·算法·leetcode