九、云原生链路追踪
追踪面向的是请求,可以通过获取请求执行的相关数据,轻松分析出请求中的异常点,针对云原生架构下的追踪,大体可以分为针对主机的动态追踪(DynamicTracing),以及针对微服务的应用行为追踪。
动态追踪是一种高级的内核调试技术,通过探针机制,采集内核态或者用户态程序的运行信息,而不需要修改内核和应用程序的代码。这种机制性能损耗小,不会对系统运行构成任何危险。因此,它能够以非常低的成本,在短时间内获得丰富的运行信息,进而可以快速地分析、排查、发现系统运行中的问题。
要实现动态追踪,通常需要在 Linux 中使用相应的探测手段,甚至涉及编写并编译成内核模块,这可能会在生产系统中导致灾难性后果。经过多年的发展,尽管它们的执行已经变得更加安全了,但是编写和测试仍然很麻烦。
eBPF 似乎为上述问题找到了解决的福音,eBPF 通过一种软件定义的方式,提供并支持了丰富的内核探针类型,提供了强大的动态追踪能力。开发者通过编写 eBPF 程序,可实现相应的追踪脚本,而 eBPF 利用自身的实现机制,保障了在内核执行动态追踪的效率以及安全性。
eBPF 正是设计和实现了一种对内核进行软件定义(SoftwareDefine Kernel)的方式。控制平面是用户空间的各种 eBPF 程序,实现 eBPF 程序在内核的加载点以及执行逻辑;数据平面则是内核各种操作的执行单元,这些加载点可以是一个系统调用,甚至是一段确定的实现代码;控制平面和数据平面通过 bpf()系统调用进行通信,将用户空间的控制平面逻辑,加载到内核空间数据平面的准确位置。
eBPF 程序的类型分为两个方面:追踪(Tracing)和网络(Networking)。
追踪:eBPF 可以通过各种类型的追踪点访问与特定程序相关的内存区域,从正在运行的进程中提取信息并执行跟踪。这样开发者就可以获取关于系统的行为及其所运行的硬件的直接信息,甚至还可以直接访问为每个特定进程分配的资源,包括文件描述符、CPU、内存等的使用情况。
网络:对内核网络的操作。eBPF 程序允许开发者监控并且操作计算机系统中的网络流量,这也是 BPF 原始设计时的核心功能点。eBPF 允许对来自网络接口的数据包进行过滤,甚至可以完全拒绝这些数据包。不同类型的 eBPF 程序可以加载到内核网络中不同的处理阶段。
在网络数据包的处理上,eBPF 通常会与 Linux 内核的另外一个重要功能 XDP(Express Data Path)一起实现。XDP 是一个安全的、可编程的、高性能的、内核集成的包处理器,它位于 Linux 网络数据路径中,当网卡驱动程序收到包时就会执行 eBPF 程序,XDP 程序会在尽可能早的时间点对收到的包进行删除、修改或转发到网络堆栈等操作。XDP 程序是通过 bpf()系统调用控制的,使用 eBPF 程序实现相应的控制逻辑。
BPFTrace 是 eBPF 的高级追踪语言。它允许开发者用简洁的领域特定语言(DSL)编写 eBPF 程序,并将它们保存为脚本,开发者可以执行这些脚本,而不必在内核中手动编译和加载它们。它的灵感来自其他一些著名的追踪工具,比如 awk 和 DTrace 等,一些用户甚至认为 BPFTrace 将会是 DTrace 的一个很好的替代品。
无论是 DTrace、SystemTap,还是 BPFTrace,其实现动态追踪都是通过探针的机制,依赖于在追踪点实现的探针,进而获取相应的追踪数据。探针是用于捕获事件数据的检测点,BPFTrace 在实现内核行为追踪时使用的探针主要包括动态探针(Kprobe/Kretprobe)和静态探针(Tracepoint)两种,这些探针延续了以往常见的动态追踪工具所使用的探针设计。
动态探针:Kprobe/Kretprobe
eBPF 支持的内核探针功能,允许开发者在几乎所有的内核指令中以最小的开销设置动态的标记或中断。当内核运行到某个标记的时候,就会执行附加到这个探测点上的代码,然后恢复正常的流程。对内核行为的追踪探测,可以获取内核中发生任何事件的信息,比如系统中打开的文件、正在执行的二进制文件、系统中发生的 TCP 连接等。
内核动态探针可以分为两种:Kprobe 和 Kretprobe。二者的区别在于,根据探针执行周期的不同阶段,来确定插入 eBPF 程序的位置。Kprobe 类型的探针用于跟踪内核函数调用,是一种功能强大的探针类型,让我们可以追踪成千上万的内核函数。由于它们是用来跟踪底层内核的,开发者需要熟悉内核源代码,理解这些探针的参数、返回值的意义。
静态探针:Tracepoint
Tracepoint 是在内核代码中所做的一种静态标记,是开发者在内核源代码中散落的一些 hook,开发者可以依托这些 hook 实现相应的追踪代码插入。
开发者在/sys/kernel/debug/tracing/events/目录下,可以查看当前版本的内核支持的所有 Tracepoint,在每一个具体 Tracepoint 目录下,都会有一系列对其进行配置说明的文件,比如可以通过 enable 中的值设置该 Tracepoint 探针的开关等。
与 Kprobe 相比,它们的主要区别在于,Tracepoint 是内核开发人员已经在内核代码中提前埋好的,这也是为什么称它们为静态探针的原因。而 Kprobe 更多的是跟踪内核函数的进入和返回,因此将其称为动态的探针。但是内核函数会随着内核的发展而出现或者消失,因此 Kprobe 对内核版本有着相对较强的依赖性。
分布式追踪是实现应用链路追踪的一种重要技术手段,同时也是实现云原生可观测性的重要组成部分,其主要用于应用程序性能管理(APM,ApplicationPerformance Management)和故障定位等。常见的分布式追踪工具包括 Dapper、Zipkin、Jaeger、SkyWalking、Canopy、鹰眼、Hydra、Pinpoint 等,其中常用的开源分布式追踪工具为 Zipkin、Jaeger、SkyWalking 和 Pinpoint。这些分布式追踪工具大致可分为以下三类。
1)基于 SDK 的分布式追踪工具。以 Jaeger 为例,Jaeger 提供了大量可供追踪使用的 API,通过侵入微服务业务的软件系统,在系统源代码中添加追踪模块以实现分布式追踪。此类工具可以最大限度地抓取业务系统中的有效数据,提供了足够多的可参考指标;但其通用性较差,需要针对每个服务进行重新实现,部署成本较高,工作量较大。
2)基于探针的分布式追踪工具。以 SkyWalking Java 探针为例,在使用 SkyWalking Java 探针时,需将探针文件打包到容器镜像中,并在镜像启动程序中添加-javaagent agent.jar 命令以实现探针的启动,并完成 SkyWalking 在微服务业务上的部署。SkyWalking 的 Java 探针实现原理为字节码注入,将需要注入的类文件转换成 byte 数组,通过设置好的拦截器注入到正在运行的程序中。这种探针通过控制 JVM 中类加载器的行为,侵入运行时环境以实现分布式追踪。此类工具无须修改业务系统的源代码,相对 SDK 有更好的通用性,但其可获取的有效数据相对 SDK 类工具较少。
3)基于代理实现。Sidecar 作为服务代理,为其所管理的容器实现服务发现、流量管理、负载均衡和路由等功能。在流量管理过程中,Sidecar 可以抓取进出容器的网络请求与响应数据,这些数据可以记录该服务所完成的一次单个操作,可与追踪中的跨度信息对应,因此可将 Sidecar 视为一种基于数据收集的分布式追踪工具。Sidecar 无须修改业务系统代码,也不会引入额外的系统的开销。但由于 Sidecar 所抓取的跨度不包含追踪链路上下文,要将 Sidecar 所抓取的跨度数据串联成追踪链路是很困难的。
若要对一个微服务业务系统进行分布式追踪,会产生两个基本问题。第一,业务系统运行时可能会产生很多脏数据或发生数据丢失,需要在这种环境下准确地生成追踪数据。第二,面对成百上千的服务所生成的追踪数据,需要设计合适的收集与存储方案。追踪链路是以跨度为根节点的树形数据结构,在微服务中,从客户端发起一次 API 调用,往往后面会产生多次服务间的 API 调用,因此追踪链路代表一次完整操作,其中包含了很多子操作。
虽然分布式追踪技术在应用方面已经取得了一些进展,但其仍然存在着一定的局限性。当前的分布式追踪工具或者需要侵入微服务软件系统的源代码,或者需要侵入业务系统的镜像与运行环境,或者在生成的跨度信息与追踪链路的准确性与完整性上仍有缺失。
版权声明: 本文为 InfoQ 作者【穿过生命散发芬芳】的原创文章。
原文链接:【http://xie.infoq.cn/article/e71ceb15c50c917943f2b45e8】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论