我们的 K8s 集群Prometheus Grafana Alertmanager该配的告警都配了。P99 延迟正常、错误率正常、CPU 内存使用率正常。从监控面板看这是一个”健康”的集群。但用户反馈偶尔感觉”卡一下”刷新就好了。我翻遍了监控找不到任何问题。延迟曲线平滑得像一条直线错误率趋近于零。问题出在哪两周后我用 eBPF 给集群做了一次”全身体检”发现了 5 个传统监控根本发现不了的问题。其中第 3 个让我后背发凉。先说说什么是 eBPF一句话eBPF 让你在内核态运行自定义代码观测系统调用、网络包、文件操作开销极低。不需要改应用代码不需要重启服务不需要插桩。你只需要写一个 BPF 程序内核帮你执行。这不是新概念。2014 年 Linux 3.18 就引入了 eBPF但直到近几年工具链成熟后才真正能在生产环境用起来。Cilium 用它做网络策略Falco 用它做安全检测Pixie 用它做可观测性。而我用它来回答一个问题监控都正常为什么用户觉得卡发现一微突发流量Prometheus 采样不到现象用户反馈每天下午午 14 点-15 点之间偶尔感觉”卡一下”。持续时间很短刷新页面就好了。我查了 Prometheus这个时间段 QPS 正常、P99 正常、错误率正常。eBPF 怎么发现的我用 kubectl-trace 在网关 Pod 上跑了一个 BPF 程序统计每秒接收的包数量和字节数kubectl trace run -e kprobe:tcp_sendmsg { [comm] count(); } nginx-pod然后发现了一个规律每隔几分钟会有持续 200-500ms 的流量突增峰值是平时的 8-10 倍。为什么 Prometheus 看不到Prometheus 默认 scrape 间隔是 15 秒或 30 秒。一个持续几百毫秒的流量峰值在 15 秒的采样间隔里被”平均”掉了。就像你用相机拍一辆飞驰而过的车如果快门速度太慢拍出来是模糊的。根因一个定时任务每隔 5 分钟批量调用内部 API没有做限流。瞬时并发太高导致网关层积压。修复给定时任务加了令牌桶限流平滑请求曲线。发现二TCP 零窗口后端处理慢导致前端请求”假死”现象前端反馈某个页面偶尔会“卡住”转圈转很久但最后能加载出来。不是超时就是干等。等的时间还不固定有时候 5 秒有时候 30 秒。后端服务的监控呢一切正常。QPS 正常、P99 正常、错误率接近 0。排查过程我第一反应是前端的问题但前端同学说“网络请求发出去了pending状态就是在等响应。”那就是后端的事了我查了网关 Nginx 的 access log请求确实到了后端而且状态码是 200。再翻后端服务日志没有 error没有 exception一条正常的请求记录安安静静躺在那里。到这里我开始有点懵了请求发出去了后端也“处理”了日志显示正常返回那前端到底在等什么eBPF 怎么发现的我决定绕过应用层直接看 TCP 层到底在干什么。我在那个后端 Pod 所在的节点上用 bpftrace 挂了一个很底层的探测点tcp_send_probe0。这个函数是内核用来发送“零窗口探测包”的。简单说当接收方 TCP 窗口降为 0 时发送方会周期性发探测包来问“你那边腾出空间了吗”如果这个函数被频繁调用说明有人在丢窗口。结果我看到了异常高的零窗口探测频率而且都指向同一个后端 Pod。我接着跟踪了那个 Pod 的 TCP 接收行为发现一个规律每当它发起一次对外部 API 的调用它的 TCP 接收缓冲区就会被迅速打满然后窗口关闭数据“堵”在了内核里。根因原来这个后端服务是一个数据聚合服务。它收到前端请求后会调用一个外部 API 来获取原始数据几十 MB处理后再返回给前端。它的业务逻辑是流式的但内存配置给得很保守512Mi。当外部 API 开始大量回传数据时服务应用层来不及消费TCP 接收缓冲区很快被撑满。此时内核会向外部 API 宣告窗口为 0让对方暂停发送。于是整个请求就进入了一种“假死”状态前端在等后端返回后端在等外部 API 返回外部 API 在等后端的 TCP 窗口重新打开这就解释了前面的诡异现象后端日志显示“正常”因为它确实接收了请求只是后续处理被阻塞在等待数据上日志还没打到“处理完成”前端感觉“卡住”数据在 TCP 层被“冻”住了最终能加载出来应用层缓慢消费完缓冲区后窗口重新打开数据继续传输请求最终完成只是晚了 30 秒为什么传统监控看不到常规的 APM 延迟监控统计的是“请求从进入到离开的时间”。但是当请求卡在这种等待外部数据的内核缓冲区里时请求并没有离开这段时间不会被计入任何 P99 或平均延迟。它就像一个隐身的水下冰柱——TCP 窗口冻结在 0 的那段时间——把船体划破了水面上的监控却以为一切风平浪静。修复把这个服务的内存限制从 512Mi 调大到 2Gi调大操作系统的 TCP 接收缓冲区net.ipv4.tcp_rmem 4096 87380 16777216底层优化数据处理逻辑用更细粒度的流式消费避免一次性读取全部数据到内存修复后这个页面最慢的加载时间从偶尔 30 秒稳定到了 2 秒以内。发现三某个进程在偷偷做 fsync差点让数据库主从切换现象这是让我后背发凉的一个发现。我们的 MySQL 主从架构主库突然出现了几十秒的卡顿。监控显示CPU 正常、内存正常、磁盘 IO 正常。但 SHOW PROCESSLIST 显示大量查询处于”Waiting for table flush”状态。排查过程我怀疑是磁盘问题但 iostat 显示磁盘使用率只有 30%又怀疑是锁竞争但 SHOW ENGINE INNODB STATUS 没有死锁信息。eBPF 怎么发现的用 bcc-tools 的 biosnoop 跟踪磁盘 IObiosnoop-bpfcc输出里发现每隔几分钟有一个 PID 会发出大量 fsync 调用每次 fsync 耗时 200-500ms。追踪这个 PID发现是一个 Java 应用——不是 MySQL 进程。根因这个 Java 应用用了日志框架 Logback配置里开了 immediateFlushtrue。每次写日志都调 fsync把数据刷到磁盘。而它的日志量很大每分钟写几千条。更致命的是这个应用和 MySQL 主库跑在同一个节点上共享同一块磁盘。Java 应用的 fsync 风暴导致 MySQL 的 fsync 被阻塞进而导致”Waiting for table flush”。根源在于文件系统的日志journal是共享的。当Java应用的日志线程频繁提交fsync冲刷文件系统日志时会导致其他提交fsync的进程如MySQL在日志提交这一步被串行化阻塞。虽然内核有优化但在高负载下这种日志层面的争抢会显著放大延迟。为什么监控看不到磁盘 IO 监控看的是吞吐量和 IOPS。fsync 是小 IO数量不多但延迟极高不会体现在吞吐量上。修复把 Java 应用的 immediateFlush 关掉改成异步刷盘把 MySQL 和日志密集型应用分开部署给 MySQL 配置独立的 SSD后怕如果当时没发现这个问题主库卡顿时间过长可能会触发自动故障转移。而故障转移期间数据可能不一致。发现四TCP 重传集中在特定连接不是网络问题现象某个服务的错误率偶尔升高从 0.1% 涨到 2-3%持续几分钟又恢复。错误日志显示connection reset by peer。排查过程我怀疑是网络问题但 ping 和 mtr 都正常。怀疑是对端服务有问题但对端服务的监控也正常。eBPF 怎么发现的用 bpftrace 跟踪 TCP 重传bpftrace -e kprobe:tcp_retransmit_skb { [args-sk-__sk_common.skc_daddr] count(); }结果发现重传都集中在某个特定的后端 Pod IP 上进一步排查这个 Pod发现它的 conntrack 表满了。根因这个 Pod 是一个长连接网关每个连接都要走 conntrack。而节点的 conntrack_max 是默认值 65536不够用。当 conntrack 表满时新的连接会被丢弃表现为 TCP 重传。为什么监控看不到整体 TCP 重传率正常因为只有这个 Pod 有问题被平均掉了。修复调大 conntrack_max给这个 Pod 配置 hostNetwork: true绕过 conntrack考虑用 eBPF 做连接跟踪替代 conntrack发现五容器启动时遍历了 10 万个文件现象某个服务的滚动更新特别慢新 Pod 启动需要 2-3 分钟而其他服务只要 20-30 秒。排查过程我怀疑是镜像太大但 docker images 显示只有 200MB。又怀疑是健康检查配置问题但 readinessProbe 的 initialDelaySeconds 只有 10 秒。eBPF 怎么发现的用 bpftrace 跟踪文件打开bpftrace -e t:syscalls:sys_enter_openat { [comm] count(); } -p $(pgrep -n myapp)结果发现启动过程中这个服务打开了 10 万多个文件。根因这个服务的镜像里有一个 node_modules 目录里面有 10 万多个小文件而它的启动脚本里有一行 chown -R app:app /app递归修改文件所有者每次启动都要遍历 10 万个文件耗时 2-3 分钟。修复在镜像构建阶段就把文件所有者改好不要在启动时改用多阶段构建把 node_modules 放在单独的一层考虑用 fsGroup 替代 chowneBPF 会取代 Prometheus 吗排查完这 5 个问题我有一个感受eBPF 不是来取代 Prometheus 的而是来填补它的盲区。Prometheus 告诉你”集群整体健康”eBPF 告诉你”具体哪里有问题”。维度PrometheuseBPF观测粒度聚合指标P99、QPS、错误率单个请求、单个系统调用时间精度秒级或分钟级毫秒级或微秒级覆盖范围应用层内核层 应用层开销低极低但取决于 BPF 程序复杂度易用性高低需要理解内核但 eBPF 也有代价学习曲线陡你需要理解内核机制、系统调用、网络协议栈。写一个 BPF 程序比写一个 PromQL 查询难得多。工具链不成熟虽然有 kubectl-trace、bpftrace、bcc 等工具但和 Prometheus 生态相比还是太原始了。生产环境有风险BPF 程序跑在内核态如果写错了可能导致内核崩溃。虽然 eBPF 的 Verifier 会检查安全性但逻辑错误还是可能引发问题。数据量太大eBPF 能观测到每个系统调用、每个网络包数据量是 Prometheus 的 1000 倍。怎么存储、怎么分析、怎么告警都是问题。给运维人的建议不需要成为 eBPF 专家但要知道它能解决什么问题。当你遇到这些问题时可以考虑用 eBPF监控正常但用户反馈有问题偶发的延迟毛刺或错误需要观测内核层的行为系统调用、网络包、文件操作传统工具看不到的盲区从网络排查入手门槛最低。Cilium 已经帮你封装好了 eBPF 的能力你只需要会用 cilium monitor、hubble observe 这些命令。写在最后这次”全身体检”让我对”可观测性”有了新的理解。以前我觉得Prometheus Grafana 日志 链路追踪就够了。现在发现这些工具只能看到”冰山上面”而 eBPF 能看到”冰山下面”。那些偶发的、瞬时的、内核层的问题传统工具根本看不到。而这些问题往往是最难排查的。但体检完之后我突然意识到一个问题eBPF 能看到内核层的行为却看不到配置层的问题。比如一个 Pod 有没有 cluster-admin 权限、API Server 有没有开匿名访问、有没有容器挂载了宿主机的 /etc——这些 eBPF 全都不知道。性能体检做了那安全体检呢于是我决定后面做一次专门的安全审计。这一审发现了 23 个高危漏洞——其中 3 个让我到现在都后怕。
K8s集群监控一切正常,用户却投诉“卡”?我用eBPF挖出5个后背发凉的真相。
我们的 K8s 集群Prometheus Grafana Alertmanager该配的告警都配了。P99 延迟正常、错误率正常、CPU 内存使用率正常。从监控面板看这是一个”健康”的集群。但用户反馈偶尔感觉”卡一下”刷新就好了。我翻遍了监控找不到任何问题。延迟曲线平滑得像一条直线错误率趋近于零。问题出在哪两周后我用 eBPF 给集群做了一次”全身体检”发现了 5 个传统监控根本发现不了的问题。其中第 3 个让我后背发凉。先说说什么是 eBPF一句话eBPF 让你在内核态运行自定义代码观测系统调用、网络包、文件操作开销极低。不需要改应用代码不需要重启服务不需要插桩。你只需要写一个 BPF 程序内核帮你执行。这不是新概念。2014 年 Linux 3.18 就引入了 eBPF但直到近几年工具链成熟后才真正能在生产环境用起来。Cilium 用它做网络策略Falco 用它做安全检测Pixie 用它做可观测性。而我用它来回答一个问题监控都正常为什么用户觉得卡发现一微突发流量Prometheus 采样不到现象用户反馈每天下午午 14 点-15 点之间偶尔感觉”卡一下”。持续时间很短刷新页面就好了。我查了 Prometheus这个时间段 QPS 正常、P99 正常、错误率正常。eBPF 怎么发现的我用 kubectl-trace 在网关 Pod 上跑了一个 BPF 程序统计每秒接收的包数量和字节数kubectl trace run -e kprobe:tcp_sendmsg { [comm] count(); } nginx-pod然后发现了一个规律每隔几分钟会有持续 200-500ms 的流量突增峰值是平时的 8-10 倍。为什么 Prometheus 看不到Prometheus 默认 scrape 间隔是 15 秒或 30 秒。一个持续几百毫秒的流量峰值在 15 秒的采样间隔里被”平均”掉了。就像你用相机拍一辆飞驰而过的车如果快门速度太慢拍出来是模糊的。根因一个定时任务每隔 5 分钟批量调用内部 API没有做限流。瞬时并发太高导致网关层积压。修复给定时任务加了令牌桶限流平滑请求曲线。发现二TCP 零窗口后端处理慢导致前端请求”假死”现象前端反馈某个页面偶尔会“卡住”转圈转很久但最后能加载出来。不是超时就是干等。等的时间还不固定有时候 5 秒有时候 30 秒。后端服务的监控呢一切正常。QPS 正常、P99 正常、错误率接近 0。排查过程我第一反应是前端的问题但前端同学说“网络请求发出去了pending状态就是在等响应。”那就是后端的事了我查了网关 Nginx 的 access log请求确实到了后端而且状态码是 200。再翻后端服务日志没有 error没有 exception一条正常的请求记录安安静静躺在那里。到这里我开始有点懵了请求发出去了后端也“处理”了日志显示正常返回那前端到底在等什么eBPF 怎么发现的我决定绕过应用层直接看 TCP 层到底在干什么。我在那个后端 Pod 所在的节点上用 bpftrace 挂了一个很底层的探测点tcp_send_probe0。这个函数是内核用来发送“零窗口探测包”的。简单说当接收方 TCP 窗口降为 0 时发送方会周期性发探测包来问“你那边腾出空间了吗”如果这个函数被频繁调用说明有人在丢窗口。结果我看到了异常高的零窗口探测频率而且都指向同一个后端 Pod。我接着跟踪了那个 Pod 的 TCP 接收行为发现一个规律每当它发起一次对外部 API 的调用它的 TCP 接收缓冲区就会被迅速打满然后窗口关闭数据“堵”在了内核里。根因原来这个后端服务是一个数据聚合服务。它收到前端请求后会调用一个外部 API 来获取原始数据几十 MB处理后再返回给前端。它的业务逻辑是流式的但内存配置给得很保守512Mi。当外部 API 开始大量回传数据时服务应用层来不及消费TCP 接收缓冲区很快被撑满。此时内核会向外部 API 宣告窗口为 0让对方暂停发送。于是整个请求就进入了一种“假死”状态前端在等后端返回后端在等外部 API 返回外部 API 在等后端的 TCP 窗口重新打开这就解释了前面的诡异现象后端日志显示“正常”因为它确实接收了请求只是后续处理被阻塞在等待数据上日志还没打到“处理完成”前端感觉“卡住”数据在 TCP 层被“冻”住了最终能加载出来应用层缓慢消费完缓冲区后窗口重新打开数据继续传输请求最终完成只是晚了 30 秒为什么传统监控看不到常规的 APM 延迟监控统计的是“请求从进入到离开的时间”。但是当请求卡在这种等待外部数据的内核缓冲区里时请求并没有离开这段时间不会被计入任何 P99 或平均延迟。它就像一个隐身的水下冰柱——TCP 窗口冻结在 0 的那段时间——把船体划破了水面上的监控却以为一切风平浪静。修复把这个服务的内存限制从 512Mi 调大到 2Gi调大操作系统的 TCP 接收缓冲区net.ipv4.tcp_rmem 4096 87380 16777216底层优化数据处理逻辑用更细粒度的流式消费避免一次性读取全部数据到内存修复后这个页面最慢的加载时间从偶尔 30 秒稳定到了 2 秒以内。发现三某个进程在偷偷做 fsync差点让数据库主从切换现象这是让我后背发凉的一个发现。我们的 MySQL 主从架构主库突然出现了几十秒的卡顿。监控显示CPU 正常、内存正常、磁盘 IO 正常。但 SHOW PROCESSLIST 显示大量查询处于”Waiting for table flush”状态。排查过程我怀疑是磁盘问题但 iostat 显示磁盘使用率只有 30%又怀疑是锁竞争但 SHOW ENGINE INNODB STATUS 没有死锁信息。eBPF 怎么发现的用 bcc-tools 的 biosnoop 跟踪磁盘 IObiosnoop-bpfcc输出里发现每隔几分钟有一个 PID 会发出大量 fsync 调用每次 fsync 耗时 200-500ms。追踪这个 PID发现是一个 Java 应用——不是 MySQL 进程。根因这个 Java 应用用了日志框架 Logback配置里开了 immediateFlushtrue。每次写日志都调 fsync把数据刷到磁盘。而它的日志量很大每分钟写几千条。更致命的是这个应用和 MySQL 主库跑在同一个节点上共享同一块磁盘。Java 应用的 fsync 风暴导致 MySQL 的 fsync 被阻塞进而导致”Waiting for table flush”。根源在于文件系统的日志journal是共享的。当Java应用的日志线程频繁提交fsync冲刷文件系统日志时会导致其他提交fsync的进程如MySQL在日志提交这一步被串行化阻塞。虽然内核有优化但在高负载下这种日志层面的争抢会显著放大延迟。为什么监控看不到磁盘 IO 监控看的是吞吐量和 IOPS。fsync 是小 IO数量不多但延迟极高不会体现在吞吐量上。修复把 Java 应用的 immediateFlush 关掉改成异步刷盘把 MySQL 和日志密集型应用分开部署给 MySQL 配置独立的 SSD后怕如果当时没发现这个问题主库卡顿时间过长可能会触发自动故障转移。而故障转移期间数据可能不一致。发现四TCP 重传集中在特定连接不是网络问题现象某个服务的错误率偶尔升高从 0.1% 涨到 2-3%持续几分钟又恢复。错误日志显示connection reset by peer。排查过程我怀疑是网络问题但 ping 和 mtr 都正常。怀疑是对端服务有问题但对端服务的监控也正常。eBPF 怎么发现的用 bpftrace 跟踪 TCP 重传bpftrace -e kprobe:tcp_retransmit_skb { [args-sk-__sk_common.skc_daddr] count(); }结果发现重传都集中在某个特定的后端 Pod IP 上进一步排查这个 Pod发现它的 conntrack 表满了。根因这个 Pod 是一个长连接网关每个连接都要走 conntrack。而节点的 conntrack_max 是默认值 65536不够用。当 conntrack 表满时新的连接会被丢弃表现为 TCP 重传。为什么监控看不到整体 TCP 重传率正常因为只有这个 Pod 有问题被平均掉了。修复调大 conntrack_max给这个 Pod 配置 hostNetwork: true绕过 conntrack考虑用 eBPF 做连接跟踪替代 conntrack发现五容器启动时遍历了 10 万个文件现象某个服务的滚动更新特别慢新 Pod 启动需要 2-3 分钟而其他服务只要 20-30 秒。排查过程我怀疑是镜像太大但 docker images 显示只有 200MB。又怀疑是健康检查配置问题但 readinessProbe 的 initialDelaySeconds 只有 10 秒。eBPF 怎么发现的用 bpftrace 跟踪文件打开bpftrace -e t:syscalls:sys_enter_openat { [comm] count(); } -p $(pgrep -n myapp)结果发现启动过程中这个服务打开了 10 万多个文件。根因这个服务的镜像里有一个 node_modules 目录里面有 10 万多个小文件而它的启动脚本里有一行 chown -R app:app /app递归修改文件所有者每次启动都要遍历 10 万个文件耗时 2-3 分钟。修复在镜像构建阶段就把文件所有者改好不要在启动时改用多阶段构建把 node_modules 放在单独的一层考虑用 fsGroup 替代 chowneBPF 会取代 Prometheus 吗排查完这 5 个问题我有一个感受eBPF 不是来取代 Prometheus 的而是来填补它的盲区。Prometheus 告诉你”集群整体健康”eBPF 告诉你”具体哪里有问题”。维度PrometheuseBPF观测粒度聚合指标P99、QPS、错误率单个请求、单个系统调用时间精度秒级或分钟级毫秒级或微秒级覆盖范围应用层内核层 应用层开销低极低但取决于 BPF 程序复杂度易用性高低需要理解内核但 eBPF 也有代价学习曲线陡你需要理解内核机制、系统调用、网络协议栈。写一个 BPF 程序比写一个 PromQL 查询难得多。工具链不成熟虽然有 kubectl-trace、bpftrace、bcc 等工具但和 Prometheus 生态相比还是太原始了。生产环境有风险BPF 程序跑在内核态如果写错了可能导致内核崩溃。虽然 eBPF 的 Verifier 会检查安全性但逻辑错误还是可能引发问题。数据量太大eBPF 能观测到每个系统调用、每个网络包数据量是 Prometheus 的 1000 倍。怎么存储、怎么分析、怎么告警都是问题。给运维人的建议不需要成为 eBPF 专家但要知道它能解决什么问题。当你遇到这些问题时可以考虑用 eBPF监控正常但用户反馈有问题偶发的延迟毛刺或错误需要观测内核层的行为系统调用、网络包、文件操作传统工具看不到的盲区从网络排查入手门槛最低。Cilium 已经帮你封装好了 eBPF 的能力你只需要会用 cilium monitor、hubble observe 这些命令。写在最后这次”全身体检”让我对”可观测性”有了新的理解。以前我觉得Prometheus Grafana 日志 链路追踪就够了。现在发现这些工具只能看到”冰山上面”而 eBPF 能看到”冰山下面”。那些偶发的、瞬时的、内核层的问题传统工具根本看不到。而这些问题往往是最难排查的。但体检完之后我突然意识到一个问题eBPF 能看到内核层的行为却看不到配置层的问题。比如一个 Pod 有没有 cluster-admin 权限、API Server 有没有开匿名访问、有没有容器挂载了宿主机的 /etc——这些 eBPF 全都不知道。性能体检做了那安全体检呢于是我决定后面做一次专门的安全审计。这一审发现了 23 个高危漏洞——其中 3 个让我到现在都后怕。