作者:梵登、千陆
本文基于 KubeCon China 2023 分享整理
我们今天分享的主题是基于 eBPF 构建下一代智能可观测系统。
在开始之前呢,我先介绍一下我们自己。我是刘恺,花名是千陆,目前是阿里云 ARMS K8s 监控子产品的负责人。这位是我的同事董善东博士,花名梵登,他是阿里云 ARMS 产品 AIOps 领域的负责人。
K8s 中的可观测挑战
本次的分享主要分为三部分内容。我们先来看第一部分,K8s 中的可观测挑战。
随着云原生、K8s、微服务等概念的兴起,我们的应用发生了很多变化,比如微服务化、容器化等等。那么一切都朝着统一的标准发展,这就带给我们非常多的好处。比如极致的弹性、高效的运维、标准的运行时环境等等。但同时,K8s 也为开发者带来了很多的问题。
我们在公有云上收集了 1000 多个 K8s 中的工单,开发者将自己的基础架构迁移到 K8s 之后,实际都遇到了哪些问题呢?
通过分析工单,我们可以分析出三大挑战:
- 第一个,K8s 基础设施问题不容乐观,从统计图中可以看到网络相关的问题占比在 56% 以上,那么开发者在遇到这些问题的时候,作为可观测系统,我们需要采集什么数据才能回答异常的原因呢?
- 第二个,由于应用很大的一部分复杂度下沉到了基础设施中,那么我们不仅仅需要采集应用层指标、还需要采集容器、网络等观测数据,那么在 K8s 中,我们怎样能够优雅的采集这些数据呢?
- 第三个,当我们采集到这么多的数据之后,我们又该如何去使用这些数据来协助开发者排查问题?
K8s 中的数据采集方案
好的,让我们带着这三个问题,我们进入后面的内容:K8s 中的数据采集方案。
我们通常来说 K8s 应用我们可以分为很多层。最上面一层是业务层,包括我们的一些前端页面、小程序;应用层就是对标我们的后端应用;容器层可以有很多种运行时,比如 containerd,docker 等;基础设施层包括 K8s。那么到此就结束了吗?其实并没有。因为 K8s 应用它的复杂度在不断的下降,所以说往往我们需要继续去深入。可能会涉及到网络插件、Linux 内核,甚至是一些硬件驱动等。到此我们就可以回答之前提到的第一个问题了。在 K8s 中我们到底需要哪些数据?答案就是我们****目之所及的所有数据。 无论在任何一层出现故障,都可能会导致我们整体系统的异常。
接下来我们再回顾第二个问题,就是我们怎样去采集这些数据。
其实目前来说,我们可观测领域在每一层都有很多独立的解决方案。比如在业务层,有拨测、前端监控、业务日志分析等方案;在应用层,有 Opentelemetry、Jaeger 等 APM 系统;对于容器层和基础设施层,有 Prometheus 搭配多种 Exporters 的观测方案。但是当他继续深入的时候,这些传统的可观测系统就很少涉及了。这就出现了观测盲区的问题。另外呢,虽然在很多层都有各自的观测方案,但是他们的数据缺乏关联性。它们彼此都是相对独立的,没有一个统一的关联维度能将各层的数据进行融合,因此存在数据孤岛现象。
那么有没有办法去解决这些问题呢?
答案就是 eBPF 技术。 我先简单介绍一下 eBPF 技术。eBPF 是一个运行在 Linux 内核中的虚拟机,它提供一套特殊的指令集并允许我们在不重新编译内核、也不需要重启应用的情况下加载自定义的逻辑。
该图展示了使用 eBPF 的基本流程。我们需要写一段 eBPF 代码,然后通过编译工具将其编译成字节码。然后我们可以将这段字节码通过 bpf 系统调用,挂载到指定的挂载点。挂载点可以是系统调用,也可以是内核方法,甚至也可以是用户空间的业务代码。比如右下角,我们可以动态的将 eBPF 字节码挂载到 hello_world 方法的进入和返回位置,并通过 eBPF 的指令集采集方法的运行时信息。
eBPF 技术具有三大特点:第一是无侵入, 动态挂载,目标进程无需重启;第二是高性能, eBPF 字节码会被jit成机器码后执行,效率非常高;第三是更加安全, 它会运行在自己的沙箱中,不会导致目标进程崩溃,而且在加载时有严格的 verifier 验证,会保证我们的这加的这段代码是正常的,不会有死循环或者访问非法内存等问题。
该表格对比了 eBPF 与传统 apm 探针的差异。eBPF 技术在覆盖性、侵入性、性能和安全上都具有明显的优势。
我们基于 eBPF 能做到能怎样去采集数据呢?我们构建的第一个能力就是架构感知。 架构感知可以自动的去分析集群的应用架构、运行状态以及网络流向。
K8s 微服务之间的通信,从内核层面来看,就是 Linux 网络协议栈的收发包过程。我以 udp 收包为例,简单介绍一下收包过程。从左上角开始,当数据包到达网卡时候,会触发软中断,由 do_softirq 来处理软中断并进入到 net_rx_action 方法中,该方法会通过网卡驱动,主动的 Poll 网卡上的数据包。
拿到数据包后,会将数据包递交到网络协议栈中。也就是一个非常关键的方法 netif_receive_skb,很多抓包工具也都工作在这个位置。该方法会根据 Packet 的协议,比如 arp 还是 ip,tcp 还是 udp,经过不同的数据包处理逻辑之后,放到用户进程的 skb 接收队列中。我们的用户进程,在调用 recvfrom syscall 来发起收包的时候,就会将 skb 队列中的报文读取到用户空间。
有了这个理论基础,再来回顾架构感知,它其实要做的就是探测网络流向和网络质量。所以,我们可以在 Linux 收发包路径中,观测一些重要的内核方法来实现这些目的。
该表格中我列举了部分观测点,比如 netif_receive_skb 和 dev_queue_xmit,可以统计收发数据包的次数、大小以及网络流向,tcp_drop/tcp_retransmit_skb 等方法用来观测丢包和重传,可以衡量网络质量。
该页面就是架构感知在我们在我们产品中的一个落地页。我们通过这张图可以非常清晰的看到集群的概览、网络流向,拓扑信息以及每个节点的状态。从中我们可以看到上面这两有两个节点它是标黄了,它是变成了橙色。那就说明这两个节点它的网络状态是有问题的。可能说它的丢包会增高了,或者说重传增高了。但是它向上最终会反映到应用层是什么样的现象呢?
接下来就是我们构建的第二个能力, 就是应用性能的一个观测。 其实刚才的架构,我们的架构感知更多的是从传输层去衡量这个基础设施的健康度的。其实在更多我们日常的开发过程中,我们会更希望去看到一些应用层的一些信息。比如说我们 HTTP 服务的一个 4XX 和 5XX 错误是不是增高了,然后它的处理时延是不是增高了等等这些。
那么如何去观测应用层呢?我们先来分析 K8s 应用之间的调用流程,通常我们的应用会调用三方 RPC 库,再由三方库调用系统调用,接下来到了内核中,会依次经过传输层、网络层最后通过网卡驱动发送出去。
那么对于传统可观测探针,通常都是在 RPC 库的位置进行埋点,这一层会受到编程语言和通信框架的影响。
eBPF 同样也可以选择和传统 APM 一样的挂载点,在 RPC 库中通过 uprobe 来采集数据。但是,由于 eBPF 的挂载点是非常多的,我们还可以将埋点位置下沉,比如系统调用层、IP 层、网卡驱动层,那么我们就可以拦截网络的收发字节流,然后再通过协议解析来分析网络请求和响应,从而得到服务性能数据。这样的好处就是,网络字节流就与编程语言解耦了,我们就无需去适配每种 RPC 协议的不同语言、不同框架的实现了,大幅度的降低了开发的复杂度。
该表格对比了不同挂载位置的优劣。可以看到在系统调用这个位置埋点是相对更优的方案。因为 syscall 具备进程信息,方便我们向上关联一些容器的 metadata;而且 syscall 的挂载点稳定、开销也很低。
那么确定了挂载位置为 syscall 之后,我再来简单介绍一下我们分析服务性能的原理。
以 Linux 系统为例,对于数据读写的 syscall 是可以枚举的,比如 read、write、sendto、recvfrom 等,因此我们选择将 eBPF 字节码挂载到这些 syscall 中,就可以很直观的获取读写的应用层数据。举个例子,比如我们在观测一个 HTTP 服务,我们通过 read 埋点采集到了上面示例的接收数据,通过 write 埋点采集到了下面示例的发送数据。
接着,收发数据会分别进入到协议解析器中。协议解析器,会按照应用协议的标准,对数据进行解析。比如 HTTP 协议的规定了,请求行中,首行的格式是 Method、Path、Version,响应首行格式是 Version、StatusCode、Message,所以我们就可以提取出本次 HTTP 请求的 Method 是 Get、Path 是 /index.html,返回状态码是 200。此外,根据 read 和 write 触发的时间,我们可以计算出服务端从收到请求到返回响应的处理时延。
提取到这些关键信息之后,经过一些聚合处理流程,我们就可以将服务数据导出到可观测平台。
这张图片是应用性能观测我们在我们产品的一个落地页。我们可以通过这张图片可以清晰的看到,我们在通过 eBPF 可以无侵入的去观测到 HTTP Server 服务的一个黄金三指标,也就是请求数、错误数,还有平均延迟。在这里面还可以看到一些问题,就是它的请求数发生了下降。它为什么发生下降?这可能是出现了一些异常,但是在这张图中我们没有办法去找到他的原因是什么。
所以说我们需要做第三件事情,就是多维数据的关联。 之前提到了,其实我们在针对 K8s 应用中的每一层都有对应的解决方案。但是他们都是没有办法去互相融合的。这里说所说的融合就是他们缺乏统一的关联维度。比如说我们针对容器的一些观测数据,它通常都会包含 container ID 的维度。比如说 Container 的 CPU/Memory 使用率。然后针对于 K8s 资源的观测,它们都会有 K8s 实体这样一个维度,比如说是哪个 Pod、哪个 Deployment。那么针对应用性能监控,就比如说像我们的一些传统的一些 APM 平台,它通常会包含 ServiceName 或者 trace ID 等维度。因此,它们是没有统一的维度来进行关联的,可以这样就没有办法让观测数据进行融合。
如何去融合这些数据呢?eBPF 就是一个非常好的一个枢纽,因为它运行在内核态,它可以采集到一些额外的信息。举几个例子,首先我们比如说针对一些进程类的挂载点。那么 eBPF 在挂载的时候可以去采集到它的一些进程号,还有 PID namespace 等信息。这些信息再结合到容器的一个运行时,我们就可以很方便的去分析出来它的 container ID。这样我们就可以完成与容器数据的一个融合。
再比如网络相关的一些挂载点。我们可以通过 eBPF 采集到 skb 的三元组,也就是请求端的地址,对端的地址,还有它的网络命名空间。我们通过这些地址信息,去反查 K8s 的 API Server,就可以决议出它属于哪个 K8s 实体,这样就完成了 K8s 资源的一个融合。
最后是应用性能监控的数据。通常来说 APM 系统会将 serviceName、trace ID 等 trace context 携带在应用层协议中的某些字段中。那么我们在eBPF进行协议解析的时候,就可以额外解析这些数据,然后从而完成这一层的关联。比如 HTTP 协议会通过 Headers 携带 trace context 信息。
好,接下来的这个数据就是我们的产品的一些落地。我可以看到这张图中可以能看到架构感知与应用层数据的一个融合。
然后这张图展示了 K8s 资源与容器观测数据的一个融合。
以上的的内容中,我主要介绍了我们之前提到的两大问题:我们需要采集哪些数据以及我们怎样去采集这些数据。那么,在得到这些数据之后,如何去使用它们进行故障定位呢?接下来由我的同事董善东博士继续为大家分享。
K8s 中的故障定位探索实践
大家下午好,我是董善东。非常感谢千陆刚才的介绍和分享。千陆向我们详细介绍了 K8s 中可观测方面的挑战,并介绍了使用 eBPF 技术进行数据采集和关联的方法。接下来,我很高兴带领大家一起探索一下在 K8s 中故障定位的实践。
首先我们先来看一下在 ARMS K8s 监控中,人工排查故障的一个路径。假设在 gateway 到 product service 存在着一个网络慢调用的故障。这时候人工一般是怎么发现和排查呢?首先我们会对于入口应用和关键应用都会配上告警,如应用的黄金三指标 RT(平均响应时间),Error Rate(错误率),请求量(QPS)指标的告警。当收到告警时,我们可以知道 gateway 应用出现异常。但是我们如何知道问题是哪里导致的,是下游节点还是存在网络问题呢?
我们首先要关注的是左边这个拓扑图中的 gateway 服务节点。可以发现在黄金指标中,RT 出现了突增,同时还有慢调用的指标。
沿着这个拓扑图点击 gateway 的下游节点,包括 cart service、 nacos 和 product service。发现,只有 product service 的黄金指标与入口的 gateway 指标相似,可以初步判断故障的可能发生在 gateway 到 product service 的链路上。接下来,我们可以重复刚刚的动作,继续分析 product service 的下游节点,可以发现其下游节点都是正常的。
最后,我们可以再确认从 gateway 到 product service 的网络调用是否存在问题。点击图中的虚线(gateway 到 product service 的网络调用边),可以看到网络调用的指标,如包重传数和平均 RT。发现这两个指标也出现了类似突增的现象。
综上所述,根据拓扑图的人工游走和分析,可以初步判断故障发生在 gateway 到 product service 的链路上,与网络调用的两个指标也存在关联,大概率为网络问题。
人工排查的路径对我们做智能根因定位有很大的参考价值。直觉思考是我们应该直接将上述流程实现自动化。
可以先检查 Gateway 自身服务指标情况,发现黄金三指标中的 RT 出现异常突增,同时其资源指标,如 CPU、内存和磁盘使用率,都是正常的。这表明自己的服务指标存在问题。
紧接着我们通过拓扑关系可以获取所对应的下游的节点。和检查 Gateway 类似的思路:去检查下游所有节点的服务黄金三指标和资源指标是否存在异常。发现下游节点中只有 product service 是异常的。clothservice 的错误率虽然也有一个负相关的下跌,但错误率下跌从业务上看并不是异常,结合业务语义可以排除掉。
最后,可以检查服务的网络调用是否存在问题。发现 gateway 调用 product service 的网络指标存在相似的异常。到这里,其实就把刚刚人工分析的流程实现了自动化。
更进一步,我们可以对日志做分析。例如:我们可以通过日志模板提取的方式,对错误服务节点日志进行模式识别。可以发现:调 product service 接口出现了 time out 的错误日志,且次数是 24 次。对关键错误日志汇总后,将自动化分析过程和结果,以及错误日志模板信息和次数,输出为检查报告发送给运维专家。专家则根据这些信息实现智能辅助定位。
那故事到这里就结束了吗?
我们再回顾下刚刚整个流程,发现其实只是对整个系统做了一个异常扫描。我们把这些所有异常的信息汇聚在一起,提供给了运维专家。那我们可以是否可以再进一步的去直接推导出出根因结论呢?这是第一个思考。
第二个思考则是整套方法体系是在 K8s 监控的场景里面应用的。我们是不是可以把它推广到应用监控,前端监控、业务监控,甚至基础设施监控。不同监控场景下用同一套算法模型来实现?
经过总结,我们给出的答案就是我们左边的这三个步骤,分别是维度归因、异常定界和 FTA(故障树分析法)。 我们来详细的看一下这三个核心步骤。
首先是维度下钻分析,那为什么要做维度下钻分析呢?
我们刚刚从 gateway 知道它的下游是三个节点,然后我们需要去遍历查看三个下游的。如果我们有了维度归因,我们其实可以从直接从 gateway 的指标上可以分析出它的下游是 product service 出现问题。
看一下业界常见的维度下钻的做法:整体指标出现了异常后,目标想知道异常部分是哪些维度导致的?由于整体指标是各个维度值 group by 组成。举个例子,如整体错误率指标,其中维度包括了 service,region,host。每一个维度里存在多种值,例如:region 下有北京、上海、杭州。
当发现整体的指标出现异常时,通过对每一个维度进行下钻分析来确定异常是由哪个维度哪个值引起的。在各个维度分析的过程中,发现有些维度(如 region)每个值都异常,而有些维度(host,service)只有部分值存在异常。对于这种只有一部分值异常的维度,则是我们更需要关注的。通过单维度下钻定位,我们可以知晓各个维度下哪些值存在异常。
接着,我们可以对异常维度和值进行组合,实现从单维度分析到组合维度分析。例如发现 host 的值和 service 的值存在交集,即异常值既是 host1,也是 service1。那么则可以进行合并,从而确定根因为组合维度:host1 & service1。
总结下,通过对单维度下钻分析和组合维度分析,我们可以找到导致整体指标异常的特定维度和值,快速缩小了故障范围。
而在微服务拓扑中,节点是存在多层调用关系的。有了单层的维度归因,还需要知道朝着哪些方向去做维度归因。
在 K8s 的环境中,整体的拓扑图是比较复杂的,里面可能有应用的拓扑,有资源的拓扑。那我们对于整个微服务的拓扑做了一个简化。可以简单认为拓扑图只有两种类型的节点,一种叫服务节点,一种叫机器节点。简化节点后,对应的关系则分为:服务跟服务之间是调用和被调用关系,服务跟机器之间则是依赖和被依赖的关系。通过这样一个梳理,我们将一个杂乱无章,看起来无从下手的的微服务拓扑图,转为从任意一个节点开始,都可以做水平和垂直的下钻分析。
以刚才的故障为例,假设 gateway 是目标异常服务。从它开始,可以去水平的做维度归因,完成调用的下钻分析。在垂直方向上,则主要是资源依赖的关系。沿着资源层做下钻分析来检查是否存在异常的机器 ip。最后一步则是看他们所依赖的这个网络边服务调服务的网络通信是不是有问题。
通过对水平归因、垂直归因,以及对网络通信的归因,可以更有层次和逻辑的实现根因分析。结合逐层的服务拓扑分析,我们可以分析出整个链条中所有的异常节点。
最后一步则是这个故障树分析法。我们定位到了这个异常节点或者异常网络通信后,我们知道在哪些链路上发生了问题。但是具体是什么类型的问题,如是网络的慢调用问题?还是资源的 CPU 问题?
这里通常的有几种方案,一种是做一个有监督的模型,实现故障分类,但这种方案对于有标签的数据集要求很高,以及模型的泛化性、可解释性都较差。第二种则是故障树分析法的方案。具体来看下:
首先需要将我们在做微服务排障和治理的经验,整理总结为一个 FTA 的树。有了 FTA 树后,在定位到具体的节点和边后,可以进行类似决策树的条件判定来实现故障的分类。例如已经定位到是 gateway 调 product service 节点这条链路存在问题。我们可以检查 gateway 是否有下游服务依赖,是否有错慢下游,以及它的网络指标是不是存在问题。三个条件都满足,那则说明这是一个网络类型的问题。
通过这样一个方式实现了更加有逻辑性和可解释性的故障分类模型。
最后,我们将根因定位的三个核心步骤模型集成到了根因分析产品 Insights 中,如上图所示,展示了 Insights 分析的异常事件列表和根因报告。Insights 提供了两个核心页面:事件异常事件列表页面和根因分析报告页面。 目前 Insights 支持应用监控、Kubernetes 监控等多种场景。
而在具体的产品使用流程上,当应用接入 ARMS 后,Insights 会进行实时的智能异常检测。检测到的异常事件则在异常事件列表页面中展示出来。点击事件详情即可查看详细的根因分析报告,报告中包括了事件的现象描述、关键指标、异常根因概要、根因列表以及故障传播链路图。 用户还可以通过点击根因列表中的具体异常,查看更多的异常分析结果,例如异常调用方法栈、MySQL 执行的慢 SQL 信息、异常信息等。
总结
好,到这里我跟千陆的分享内容就结束了。简单总结一下,我们今天介绍了 K8s 环境下可观测的三大挑战,然后讲解了在 K8s 环境中做数据采集的方案:如何通过 eBPF 去实现是不同层级的数据采集,以及实现数据的关联。最后则分享了在 K8s 环境中做智能根因定位的实践探索。其核心的步骤是维度归因、异常定界和 FTA 故障树分析。
希望今天的内容能给大家带来一些启示和思考,谢谢大家的倾听!