eBPF零侵扰分布式追踪的进展和探索 原创
本文为 DeepFlow 在第 23 期得物技术沙龙上的演讲实录。回看链接,PPT 下载。
感谢得物稳定生产团队的邀请,很高兴能有这样一个机会来给大家分享一下我们在 eBPF 可观测性方面做的一些工作。DeepFlow 是一个聚焦在 eBPF 技术上的可观测性产品,同时也是一个开源项目。今天我来分享一下 DeepFlow 使用 eBPF 实现分布式追踪的历史发展和未来探索。
如果我们用 eBPF Tracing 去搜索,在 Google 上可能几十万的结果。但是用 eBPF Distributed Tracing 搜索的话,你能找到的真正有用信息很可能只有 DeepFlow。基于我们过去几年在这个方向上的积累,今天我将从背景、进展、案例和展望四个方面来进行分享。
01
分布式追踪在云环境下的痛点
首先,这幅图展示了对于分布式追踪我们看到的一个痛点,特别是在云原生环境下。APM 的分布式追踪解决方案依靠 Code Instrumentation(代码插桩),比如利用 Java Agent 或引入插桩的 SDK 等。插桩之后,APM 能够回答一次调用的响应时间(Response Time,RT),比如图中客户端 RT 为 500ms,服务端的 RT 是 10ms。真实环境中,有可能服务端不是一个 Java 应用,插桩困难导致无法得知它的响应时间;也有可能服务端是 MySQL 等数据库、Redis 或 Kafka 等中间件,这种情况下一般是无法插桩的。
即使在最理想的情况下,APM 能回答客户端和服务端的响应时间,也通常会发现类似上图中的情况:客户端看到的响应时间和服务端差距太大。那到底图中的小哥哥、小姐姐谁能说服谁呢?这中间其实有很多交互过程,除了业务逻辑以外,还有 DNS Lookup(K8s 环境下一次 DNS 查询可能涉及到七八次递归查询)、TCP 三次握手、TLS 四次握手,因此我们可以发现在云原生环境下 APM 的分布式追踪其实会有很多观测盲点存在。实际上即使没有上面这些复杂的非业务流程,仅仅是 RPC 调用过程本身,也会因为经过中间繁多的系统调用、容器网络、K8s 网络、网关等环节而一步步引入或大或小的时延,最终导致客户端和服务端观测到的调用时延差距越来越大。
应用插桩困难、基础设施无法插桩所引发的问题,实际上正是 eBPF 技术可以大展拳脚之地。
上图左侧是一个简单的应用拓扑,它的入口是一个 Nginx 网关,接下来是 Spring Cloud Gateway 作为微服务网关,然后进入到 K8s Pod 中的 Envoy Sidecar 中,进而到达一个 Java 业务进程;这个 Java 服务继续请求上游 Golang 服务之前会先做 DNS 解析,上游 Golang 服务继续依赖于 Redis 缓存和 MySQL 数据库,而后者在提供服务时可能会进行一些文件读写。通常来说,如果说我们只用 Java Agent 这种最便利的 APM 插桩方式的话,看到的分布式追踪可能只有图中黄色部分这么两三个 Span,就是说只能把 Java 应用入口、内部、出口的调用链展现出来。
而 eBPF 在理想的情况下能做到什么样程度呢?实际上只要在每个主机上部署一个独立的 eBPF Agent,不需要对任何业务进程和基础设施进程做修改或重启,就能得到图右侧的完整调用链,它包含了所有业务进程的调用,也包括了与 DNS、缓存、数据库、文件系统之间的调用链。图中除了彩色部分的 eBPF Span 以外,DeepFlow 还通过 cBPF 获取到了每个网卡上的 Span(容器网卡、主机网卡等),并将他们关联到整个 Trace 中,呈现出一个全栈的分布式追踪火焰图。
我今天分享的内容主要就聚焦在这个问题上:DeepFlow 的 AutoTracing 如何通过零侵扰的方式来去将这样的分布式追踪火焰图画出来。我们对这个问题以及 DeepFlow 的解决方案进行阐述的论文也发表在了去年的 ACM SIGCOMM 会议上,论文在我们的官网 https://deepflow.io 上可以下载。在真实的业务场景中,调用行为实际上是复杂多样的,可能有异步调用,也可能出现进程内部的调用链跨线程的情况,如果不对他们做处理会导致零插桩调用链断开,那 DeepFlow 是如何一步步解决其中的问题,得到越来越完美的答案的呢?
02
eBPF 零侵扰分布式追踪的进展
我们先介绍一下做这件事的一个最初的洞察,实际上我们是用到了 eBPF 的一个很好的感知能力。这样的感知能力其实一直依赖都是存在的,比如通过 Linux 内核模块。但是 eBPF 的颠覆之处在于它给了一种非常安全的方式。你不止不需要修改或重启进程,还不用担心不同版本 Linux 内核之间的差异,也不用担心会引发内核的内存泄漏或崩溃。eBPF 通过自身的 Verifier 机制保证了代码的内存安全性以及不会进入到死循环中。
那我们为什么能够去用 eBPF 来实现分布式追踪呢?其实最初的一个洞察就是,eBPF 能轻松获知每一个调用在系统中各个位置的 Thread ID 和 TCP SEQ。例如,我们可以使用 eBPF uprobe 获取用户态函数的这两个信息,用 kprobe/tracepoint 获取内核系统调用的这两个信息,用 AF_PACKET 获取网卡收发流量时的 TCP SEQ。在这些信息中,DeepFlow 利用 Thread ID 将一个进程入向和出向的调用关联到一个 Trace 中(我们称之为 System Span),同时利用 TCP SEQ 将两个服务之间的调用关联到一个 Trace 中(我们称之为 Network Span)。除此之外,DeepFlow 也会利用 eBPF Hook 文件读写的系统调用,将文件 IO 事件与应用调用之间进行关联,从而构建更加全栈的调用链。
想要获取到完整的分布式调用链,仅仅依靠 eBPF 是有很多挑战的,我们遇到的第一个挑战是 NIO(Non-blocking IO,非阻塞 IO)机制下的追踪。上图右侧是 DeepFlow SIGCOMM 论文中的一张示意图。图中 (a) 是比较简单的情况,从 Service A 接收调用到向 Service B 发起调用,全部在一个线程内完成,所以我们使用 eBPF 获取调用的 Thread ID 就能关联这个调用链。图中 (b) 展示了更为复杂但更接近真实的情况,Service A 中的线程在被持续复用,此时我们必须将 Trace 的边界识别出来,使得追踪结果中不要包含多余的调用,很显然我们可以根据时间特征和调用的闭环序列来进行分割,实现对 Trace 的切分。而图中 © 又更复杂了一些,同时也更贴近真实场景,我们看到 Service A 的线程使用了 NIO 机制,这是一种常见的高性能网络 IO 机制,在 NIO 机制下 Service A 可能会交错的处理多个 Trace 中的调用。那么 eBPF 能解决 NIO 的场景吗?答案是肯定的,DeepFlow 在 eBPF 程序利用了同一个 Trace 的相邻两次系统调用之间几乎不会有其他 Trace 的调用打断进来的观察,为每个调用生成了一个无需注入到消息内容中的 syscall_trace_id
,利用一系列 syscall_trace_id
我们就可以将图中紫、蓝、粉三个颜色的 Trace 区分开。这张图片只是整个追踪逻辑的一个简化示意图,大家可以阅读我们的论文或者 GitHub 代码来了解更多的细节。
今天我想着重分享的,其实是 DeepFlow 在面临第二个挑战 —— 业务逻辑跨线程处理下的持续探索和巨大的进展。跨线程的场景很多,例如:1)应用程序内部跨线程,应用接收和应答调用与处理业务逻辑的线程不一样;2)中间件内部跨线程,网关和消息队列等中间件与客户端(下游)通信的线程和与服务端(上游)通信的线程不一样;3)访问缓存和数据库时跨线程,应用访问 Redis 缓存、数据库,以及分布式数据库进程内部处理一个 SQL 命令时跨线程。
下面我依次来介绍 DeepFlow 解决跨线程追踪问题用到了哪些思路。
我们用到的第一个思路是利用协程、Socket 的「生命周期」来进行追踪。比如对于 Golang 语言的应用而言,由于 Golang 是一个原生的协程语言,我们不可能通过 Thread ID 来进行调用链追踪。但我们知道一个 Golang 应用为了处理它所提供的 API,会将所有阻塞调用使用 Goroutine 运行,稍加分析我们就会发现,这种在协程 A 内接收 API 请求、在协程 B/C/D/… 内继续请求上游依赖服务的场景,我们可以使用 eBPF uprobe 观测协程的生命周期,例如当关注到协程 B/C/D/… 是由协程 A 创建的,此时我们可以将 A/B/C/D/… 等协程标记上同样的 Pseudo Thread ID
,从而将这个问题转化为一个线程语言的调用链追踪问题,而后者我们已经有了很好的基于 syscall_trace_id
的解决方案。
再比如对于一些网关服务而言,我们可以跟踪 Socket 的生命周期来实现跨线程追踪。例如在 Nginx 中,一个与 upstream 之间建立的 Socket 通常是在线程 A 中创建的、在另一个线程 B 中使用的,我们会在 eBPF 程序中跟踪这个 Socket 的创建和读写,从而利用 Socket 的生命周期关联在一段时间内有业务联系的两个线程中的 Socket。
第三个比如说 Tomcat,它的 Acceptor Thread 是接收请求的,除此之外还有个 Poller Thread 是去监听请求的读写就绪事件,监听对应的 FD 上是不是读写就绪了。但是它见听到可读或可写时,不会直接进行读写,而是将事件发送给 Worker Thread 进行处理。也就是说,实际上 Socket 的读写动作已经在同一个线程中了。Dubbo 也是类似的,当它使用 DirectDispather 时 IO 线程也会承担业务处理的职责,因此在 IO 线程中可以直接将调用链追踪出来。
我们用到的第二个思路是利用网关、消息队列的一些「事实标准」进行追踪。同样不需要应用做任何的改造,只需要对基础设施或中间件做一些配置调整,即可将中间件前后的调用链自动串联起来。比如几乎对于所有应用层网关而言,Nginx、HAProxy、BFE、Envoy、各家云网关等,都支持向 HTTP 消息中自动注入 X-Request-ID
头字段。当网关接收到一个请求时,它会生成一个随机的 UUID,即 X-Request-ID 并注入到发往 upstream 的请求头中;待接到 upstream 的响应以后,网关会查询为对应的请求生成的 X-Request-ID 并将它注入到对客户端的响应头中。所以,我们通过解析 HTTP Header 中用于表达 X-Request-ID 含义的头字段,就能将网关前后的 HTTP 调用关联起来。实际上不论流量经过了几级网关,只要其中有一级网关开启了 X-Request-ID,整个调用链就都能依靠它完整的追踪出来。
同样我们也来看看消息队列,虽然纯异步的消息通信缺少追踪信息可以利用,但如果应用使用消息队列来实现 Request-Response 模式的通信时,一定会有额外特征可提供追踪信息。例如 Kafka 和 ActiveMQ 的协议中就会要求存在一个用于请求、响应关联的 Correlation ID
字段。实际上业务方也是强依赖这个 ID 的,只有客户端能从响应消息中获取到这个 Correlation ID,才能将它和对应的请求关联匹配上来。因为有了这样的关联信息,DeepFlow 能自动将跨越消息队列的调用追踪出来,同样不需要应用做任何改造。
我们用到的第三个思路是利用「行业规范」来进行追踪。在具体的业务场景中,通常有一些用于跟踪业务流程的唯一标识字段,例如金融行业的交易流水号。DeepFlow Agent 上支持编写 Wasm Plugin,因此即使对于这些业务相关的信息,我们也可以非常便捷的通过编写一个 Golang 等语言的 Plugin 来实现业务追踪 ID 的提取。除了提取业务追踪 ID 以外,实际上 Wasm Plugin 的能力会更加丰富,例如提取 Json、XML、Protobuf 的消息体中的 User ID、Order ID 等业务字段,以及扩展 DeepFlow Agent 内置的协议解析能力(目前 Agent 已经内置了将近 20 种协议的解析能力)。
至此我们已经有三种方法来解决跨线程的问题了,但仍然还不能覆盖所有的情况。实际上在这个问题上 DeepFlow 的用户也在进行着积极的创新。我们知道实现一个全功能的 APM Instrumentation 可能涉及到很多工作,包括上下文的传递、数据的采集、数据的上报等,由于 APM Agent 实现的能力很多,会面临更多的性能消耗的担忧和频繁升级的挑战。我们的一些用户看到这种情况,开始实现一些轻量级的 In-Process Agent/SDK,这些 Agent/SDK 只会传播 Trace 信息并将其注入到协议头中,代码量非常小、升级频率非常低,因此性能和稳定性隐患也非常低。由于 DeepFlow Agent 支持解析众多应用协议中的 TraceID 和 SpanID,同时支持自定义 Header 字段名,二者相结合我们能得到一个非常好的方案,可以完美解决进程内部跨线程、异步调用的问题,实现无限接近零侵扰的分布式追踪。
基于这类轻量级 In-Process Agent/SDK 的能力,我们还能玩出很多花样。上图右侧是利用 SQL 语句的注释能力,我们将 TraceID 注入到注释中,并通过 DeepFlow Agent 提取出来,从而实现了对于应用侧使用 SQL 线程池时的跨线程调用链追踪。
实际上 DeepFlow 中 AutoTracing 除了能够追踪分布式调用以外,还有很多其他关联能力,例如关联网络流日志、文件 IO 事件等。下面这种图展示了对 IO 事件的关联效果。
当我们在火焰图中看到 MySQL 进程的一个 SQL 调用、Redis 进程的一次命令执行时,依靠 DeepFlow 的文件 IO 事件关联能力,这个调用生命周期内执行的所有慢 IO 事件都能呈现出来。因此我们得以更精细的观测 SQL 调用、Redis 命令的高时延毛刺,分析文件 IO —— 特别是使用共享磁盘的场景 —— 对调用性能的影响。上图展示了一次 SQL 调用关联的多个 binlog 文件写入操作的数据量和时延。
默认情况下 DeepFlow 仅采集应用调用生命周期内、处理应用调用的同一线程上发生的文件 IO 事件,并且仅会采集其中的慢 IO 事件。当然我们也可以很方便的调整 DeepFlow Agent 的配置,切换为采集「所有线程中的文件 IO」事件,同时也可调低慢 IO 的判断标准。
在进展层面,另一个值得分享的事情是我们对低版本内核的适配。在春节前发布的 6.4 版本中,DeepFlow 基于 eBPF 的能力已经支持了 CentOS 7.9 / RedHat 7.6 的 3.10 内核!
03
DeepFlow 用户的典型落地案例
讲完了进展,下面我分享一点 DeepFlow 典型用户的落地案例,这些案例在我们的 Meetup 和公众号中已经有很多了,今天我主要结合腾讯 IEG 对 DeepFlow 的使用,谈一谈零侵扰可观测性可以如何提升研发效能。
上图左侧是 DeepFlow 社区版最近发布的一个开箱即用 Dashboard。社区版仅支持 Grafana GUI,在此之前我们已经内置了丰富的 Dashboard,但对于新上手的用户会感到无所适从,而且不同的 Panel 之间无法联动,分析能力较弱。这个开箱即用的 Dashboard 将接口的性能指标、调用日志、分布式追踪在一个 Grafana 面板上展现出来,并且 Panel 之间支持点击联动。我们希望它成为你安装 DeepFlow 社区版之后快速体验零侵扰可观测性的第一个窗口,无需对应用做任何调整,你就能看到所有接口的性能数据,并层层下钻到一个特定调用的分布式追踪结果。同时我们也希望它成为日常工作的伴侣,能非常轻便的融入到发版、扩容、缩容、配置变更等工作流中,提升研发和运维团队的效能。
那么作为一个游戏业务的运维人员,因为有了零侵扰可观测性能力,它不用再去驱动开发者修改代码进行插桩。以腾讯 IEG 为例,除了有自研游戏以外,他们还有很多代理游戏,难以推动这些第三方开发的游戏进行 APM 插桩。即使是自研游戏,通常也是使用 C++ 等编译型语言实现,一方面必须使用 SDK 插桩,另一方面也要求运维团队能构建 C++ 技术栈从而长期维护 SDK。总结来讲就是推动插桩难、维护插桩 SDK 难,eBPF 能很好的解决这两个痛点。
在腾讯 IEG,DeepFlow 已经覆盖了很多游戏业务,而且已经不仅用于事后的故障排查,已经融入了业务变更之前的依赖确认、扩缩容过程中的性能监测、业务发版之后的性能对比、基础设施的性能优化等工作,显著提高了研发和运维团队的工作效率。
04
探索 eBPF 可观测性能力的边界
最后这部分分享一下我们对未来的一些展望。在分布式追踪方面,我们一直在思考的一个问题是,如何彻底解决跨线程的调用链串联问题。除了有之前我介绍的各种方法以外,最近我们也在实验一些统计推理的方法。像这张图里面展示的那样,我们是否可以通过大量数据的统计,来智能的推理出一个应用的 Ingress 调用和 Egress 调用之间的关联关系?目前我们已经做了一些实验,发现推理结果随数据量的增长可以得到非常可观的改善,再结合 eBPF USDT 能力,预期可以得到非常准确的分布式调用链。这个探索能将我们在跨线程场景下的零侵扰追踪能力前进一大步,期待我们在下个大版本 v6.6 中能发布这个能力。
除了对跨线程追踪的思考以外,我们还在探索如何基于 eBPF 的调用日志生成 API 拓扑。这里面的主要挑战在于,直接使用调用日志数据只能生成服务拓扑,这类拓扑会在网关、消息队列处连接上一大片服务,打破业务的边界,不易于业务方使用;而直接使用 Trace 数据时,由于 Span 中缺乏 TraceID(在没有 APM 插桩的情况下),难以将一个 Trace 的所有 Span 发往一台机器上聚合。
在这个问题上我们正在进行两方面的探索:首先通过网关的 X-Request-ID 和消息队列的 Topic 信息,计算这些中间件前后的服务的关联关系,避免服务拓扑的爆炸,提供一个「有业务边界」的服务拓扑;其次我们也在利用 Linux 5.10 内核中的 eBPF 新能力,尝试将 DeepFlow 的 syscall_trace_id 注入到 TCP Option 中,从而为一个 Trace 中的每条调用日志标记上唯一的 Trace ID,便于将调用日志聚合为 API 拓扑。
第三个方面,我们也在探索 eBPF 在第三大可观测性信号 —— 日志 —— 的创新。我们看到一方面 eBPF 的采集方式无需日志落盘,能够降低日志读写的资源开销;另外 eBPF 采集到的数据可以利用 Thread ID 和追踪数据天然关联起来,天然消除了数据孤岛。
最后,我们也在关注 eBPF uprobe 性能优化方面的工作。我们看到 bpftime 是很好的解决方案,期待它能走向生产。。
最后,展示一张 DALL.E 的大作,通向零侵扰的可观测性之路。我们期待 eBPF 可以分摊越来越多的开发工作,期待生产环境中的内核升级更快,让零侵扰的可观测性能力提升整个 DevOps 流程的效率。
05
什么是 DeepFlow
DeepFlow 是云杉网络开发的一款可观测性产品,旨在为复杂的云基础设施及云原生应用提供深度可观测性。DeepFlow 基于 eBPF 实现了应用性能指标、分布式追踪、持续性能剖析等观测信号的零侵扰(Zero Code)采集,并结合智能标签(SmartEncoding)技术实现了所有观测信号的全栈(Full Stack)关联和高效存取。使用 DeepFlow,可以让云原生应用自动具有深度可观测性,从而消除开发者不断插桩的沉重负担,并为 DevOps/SRE 团队提供从代码到基础设施的监控及诊断能力。
GitHub 地址:https://github.com/deepflowio/deepflow
访问 DeepFlow Demo,体验零插桩、全覆盖、全关联的可观测性。