eBPF 零侵扰分布式追踪 3 分钟锁定 Java 程序 I/O 线程阻塞 原创
摘要: I/O 线程阻塞是Java 程序经常出现的问题之一,此类故障发生时 Java 程序的请求、响应在 I/O 线程向操作系统 Socket Buffer 读/写过程中发生阻塞,由于在业务代码插桩无法观测到 I/O 线程的工作情况和性能表现,因而导致故障非常隐蔽和难以诊断定位。通过本篇案例您将了解到,某银行的开发工程师如何使用 eBPF 技术带来的零侵扰追踪能力,在某次分布式核心交易系统上线信创平台的非功能测试(性能压测)故障诊断中,用 3 分钟时间锁定 Java 程序 I/O 线程阻塞。
0x0: 故障背景
近期,某银行的分布式核心交易系统 XX 中心业务按计划即将上线华为信创云,但在上线前的非功能测试(性能压测)过程中,业务响应时延不定时劣化,原因不明。
XX 中心的业务程序使用 Java 语言开发(SOFARPC 框架),采用 Netty 作为网络框架,该业务流程涉及 Client
、Gateway
、App-1
、App-2
,端到端业务访问路径如下:
- 1)
Client
使用 SOFARPC 协议访问Gateway
的接口; - 2)
Gateway
经过七层负载均衡将 SOFARPC 协议的 Request 转发至某个App-1
; - 3)
App-1
访问App-2
。
DeepFlow 通过 eBPF 以零侵扰的方式对每一次请求经过每一个位置时( 观测点 1~8 )的性能进行全链路采集和观测,整个过程对应用程序无需搬运或注入代码,也无需任何重启操作。
0x1: 故障诊断
在 DeepFlow 可观测性平台使用 “5W 方法” 对故障快速诊断。
什么是 “5W 方法”?
从宏观到微观有序调阅可观测性数据,逐步回答如下 5 个问题,可以快速、有效诊断出问题根因,称之为“5W 方法”:
- Who is in trouble?
- When is it in trouble?
- Which request is in trouble?
- Where is the root position?
- What is the root cause?
1. 确定首个观测对象
故障诊断的第一步,首先要回答 “Who is in trouble” 的问题,从而确定首个观测的对象。
此次性能压测的故障诊断场景中,已明确知道 Gateway ——> App-1
路径的响应时延存在异常,因此将其作为首个观测对象,在 DeepFlow 的「追踪」-「路径分析」功能入口检索该路径。
2. RED 指标监控,找出时间点
故障诊断的第二步,要回答 “When is it in trouble” 的问题。
打开 Gateway ——> App-1
路径的「右滑窗」,在其中观测 RED 指标(请求量、错误率、响应时延)。
由于分布式核心系统的业务等级非常重要,因此选择观测响应时延指标中的“最大时延”,并找出问题发生时间点。
通过分析该路径的最大时延,我们发现在压测阶段周期性出现慢响应(大于 2 秒),于是我们可以将右滑窗的观测时间范围缩小到问题时段(20:20~21:00)。
3. 调用日志检索,找出慢响应
当确定问题发生时间段之后,第三步便要回答 “Which request is in trouble” 的问题。
在「右滑窗」的「调用日志」中过滤或排序,在问题时段找到十余次慢响应请求。
4. 调用链追踪,找出根因位置
找到慢响应的请求列表后,第四步便可以通过调用链追踪,回答 “Where is the root position” 的问题。
选取其中一次约 2s 的慢响应请求做「调用链追踪」,经过追踪发现在观测点 8
的左侧存在约 2s 的空缺,说明:
观测点 7
(App-2
的 Pod 网卡)已及时收到 Request;观测点 8
(App-2
的应用进程)等待约 2s 才发起read()
读取该次 Request。
因此,根因位置是 App-2
的应用进程。
5. 根因分析
第五步,对根因位置进行分析,回答 “What is the root cause” 的问题。
为什么 App-2
在网卡已收到 Request 后约 2s 才发起 read()
的 Syscall?
答案很简单:Java 程序 Netty 的 I/O 线程繁忙。
Java 程序使用 Netty 网络框架时,当多路 Client 发起连接(TCP Connection)注册,Netty 的 BossGroup 会在 I/O 线程池(即 Woker Group)中选择一个 I/O 线程(Worker EventLoop)负责 TCP 建连处理(三次握手),接收应用请求,发送应用响应,因此 I/O 线程池中的一个 I/O 线程会被多个 TCP 连接所复用。
因此,虽然 Netty 的 NIO 通信模型很好的解决了高并发场景下的业务处理性能,并通过 I/O 线程池很好的控制了高并发量对线程资源的消耗,但 Netty 需要显式配置、明确限制 I/O 线程数量(默认取值:2 * CPU 核数),在瞬时请求量大的场景下 I/O 线程仍然存在拥塞风险,当某个 I/O 线程负责的 TCP 连接出现大并发时,有可能发生 Request 到达 Socket 的 recv buffer 后不能被 I/O 线程及时 read 的情况。
6. 修复方案及验证
锁定故障根因是“Netty 的 I/O 线程阻塞”后,开发人员在 JVM 中修改应用程序启动参数:java -XX:ActiveProcessorCount
,将取值调整为 实际 CPU 核数 的 2 倍,达到 I/O 线程超分的效果(Netty 中 I/O 线程数量默认取值 2 * ActiveProcessorCount
,此时等于 4 * 实际 CPU 核数
)。
通过调整 ActiveProcessorCount
参数,将应用程序 I/O 线程数量提升到原来的 2 倍之后,经过持续压测验证,问题解决。
实际生产中,可以根据应用程序的特性(I/O 密集型、CPU 密集型)来自定义线程数量,以达到更好的应用程序性能。对高度 I/O 密集型应用程序,可以增加 I/O 线程数量;对高度计算密集型应用程序,可减少 I/O 线程数量。
0x2: 总结
从该案例中我们不难发现,在传统 NPM 监控和 APM 监控体系下,操作系统内核、应用程序均存在大范围的盲区,导致故障定位极其困难,经常发生 APM 、NPM 呈现的服务性能不一致的情况,从 NPM 监控明显看到服务端应用程序响应慢,但从 APM 监控却认为所有请求的响应性能指标均正常,因而故障诊断中的边界不清,来回拉锯,甚至互相指责非常普遍。
DeepFlow 通过 eBPF 技术实现的零侵扰(Zero-Code)的分布式追踪,实现了任意一次应用请求(Any Request)从物理网络到容器网络、操作系统、应用进程的全技术栈(Full-Stack)、端到端(Real End-End)追踪能力,打通了应用运维、系统运维、网络运维的技术栈边界,构建统一的运维观测数据湖,从而将故障诊断效率提升到 3 分钟以内。
0x3: 技术 Tip——Netty 网络框架数据包读取过程
Netty 网络框架使用 OS Kernel 的 epoll 机制在内核空间、用户空间之间读/写数据。
那么 Netty I/O 线程如何与 OS Kernel 交互并获取通信对端发来的 Request 数据包呢?
可以通俗的描述为如下过程:
- 首先,Netty 的
Worker EventLoop
(I/O 线程
)调用 Kernel 中epoll
的epoll_wait()
方法,并进入阻塞状态,等待epoll
的通知; - 当网卡接收到 Request 数据包后,通过硬中断通知 Kernel,Kernel 处理数据包并放入 Socket 的
recv buffer
; - 然后,
epoll
监测到 Socket 的recv buffer
有数据包到达后,将可读的 Socket 列入就绪的事件列表; - 再然后,
Worker EventLoop
在epoll_wait
上被唤醒,并返回就绪的事件列表; - 最后,
Worker EventLoop
调用read()
方法从操作系统的recv buffer
中读取 Request 数据包。
至此,Netty 完成一次从 Kernel 的 Socket 读取数据的完整过程,如果 Worker EventLoop
没有其他需要读/写的数据,则再次回到第 1 步,调用 epoll
的 epoll_wait()
方法,进入阻塞状态,等待下一次数据包到达后的 epoll
通知,循环往复。
0x4: 什么是 DeepFlow
DeepFlow 是云杉网络开发的一款可观测性产品,旨在为复杂的云原生及 AI 应用提供深度可观测性。DeepFlow 基于 eBPF
实现了应用性能指标、分布式追踪、持续性能剖析等观测信号的零侵扰(Zero Code
)采集,并结合智能标签(SmartEncoding
)技术实现了所有观测信号的全栈(Full Stack
)关联和高效存取。使用 DeepFlow,可以让云原生及 AI 应用自动具有深度可观测性,从而消除开发者不断插桩的沉重负担,并为 DevOps/SRE 团队提供从代码到基础设施的监控及诊断能力。
GitHub 地址:https://github.com/deepflowio/deepflow
访问 DeepFlow Demo,体验零侵扰、全栈的可观测性。