Kubernetes 观测:基于 eBPF 的云原生深度可观测性实践

云原生可观测容器容器与中间件

picture.image

Kubernetes 观测 VKO(全称 Volcengine Kubernetes Observability)是火山引擎推出的一套面向 Kubernetes 的一体化、全栈式可观测套件,全面支持容器基础、容器集群核心系统组件、AI Infra、网络性能、应用性能等观测能力。

来源 | 火山引擎云原生团队

困局:云原生可观测面临挑战

随着云原生技术栈的迅速发展,系统复杂性逐渐下沉到服务网格、网关、通用 sidecar、serverless 运行时、内核等基础设施层面,诚然这大大减轻了业务开发同学的心智负担,让其可以更专注于业务本身,但却给可观测性带来了巨大的挑战:

  • 盲点多

基础设施逐渐“黑盒”化 ,应用往往仅仅是较薄的一层 ,如果这个时候继续沿用传统 APM 观测方案,会存在大量的盲点,在问题发生时可能只能看到应用层的问题表象,而无法快速定位根因。

传统的容器网络观测方案通常只关注自身维度, 缺乏上下游视角,且维度信息非常有限 ,在日益复杂的网络环境下,难以回答诸如“究竟是谁访问我发生了故障”“我究竟影响了下游哪些实例”“是什么原因导致发生了丢包” 等问题。

  • 埋点困难

传统 APM 方案需要依赖 SDK/Javaagent 的方式来进行插桩埋点,这给在多协议、多语言场景下统一所有业务线的接入造成了极大挑战。同时,传统 APM 方案也无法对基础设施实施插桩。

传统基于 cadvisor 的容器观测方案只能看到 Kernel 主动暴露的数据,而 Kernel 对于 微服务层面的隔离和可观测性 还不太够,如果需要深入内核进行插桩,传统的方式可能会需要重新编译内核,成本和风险极高。

  • 数据孤岛,缺少全栈视角的串联分析

相关调查数据显示,超过 65% 的企业组织拥有超过 10 种监控工具,而这些工具通常作为独立解决方案单独运行,以支持不同团队的特定需求。可观测性并非简单的数据堆砌,更重要的是将数据通过一定的关联纽带有机串联起来,而不同监控工具可能都有各自的元数据语义化标准,难以实现对齐统一。

各个观测数据之间也缺乏必要的因果关系,在根因定位的时候难以实现有效关联。

picture.image

可观测带来效率挑战:25%的工时被用于基础工作

而要应对上述挑战,我们不难总结出几个核心诉求

  • 从应用层到内核,自顶向下,需要能够尽可能全面地进行覆盖;
  • 接入成本需要尽可能低;
  • 需要能够有统一标准的语义化标签和因果关系,来帮助我们关联分析各个离散的可观测数据。

可观测性成熟度模型回顾

在解决这些问题之前,我们先来回顾一下可观测性成熟度模型经典分层:

picture.image

  • 监控 :需要我们回答各个组件的运行状态。 这并不陌生,也很容易实现,我们只需要监控组件单个特定状态,如果超出阈值则触发告警即可;
  • 可观测性 :要求我们回答组件为何不工作。 其更多的是对组件内部可见性的一个要求,我们通常可以引入日志和传统 APM 工具,来帮我们提高组件系统内部的可见性。

前 2 层借助传统的观测能力就可以比较快速实现,但如果只达成这两层,并没有真正解决可观测性面临的问题。因此我们可能需要实现第三层:“ 因果可观测性 ”。它要求我们能够回答:

  • 问题在整个堆栈中是如何传播的?
  • 问题根因究竟在哪?
  • 问题开始的时候堆栈是什么样子的?
  • 问题发生,哪些组件会受到影响?
  • 海量的观测数据及告警应该如何关联?

这些问题,也正是真正困扰技术团队的问题。根据可观测性模型理论,要能够回答这些问题,核心要实现的 2 个必要维度便是: 拓扑时间

拓扑可视化让工程师得以在全栈活动的上下文中查看来自网络、基础设施、应用程序和其他领域的遥测数据;它还提供了重要的背景信息,方便工程师了解发生故障时业务会受到怎样的影响。

picture.image

来源:《可观测性成熟度模型》

当然,仅仅一个静态拓扑也无法应对日益频繁变化的微服务部署架构,我们还需要 结合时间维度来绘制一个动态拓扑 ,并且让这个动态拓扑能够和其他可观测数据(例如日志、指标、事件、trace)有机地关联起来。

一个可以纵向关联各种可观测性数据,横向可以追溯任意时序状态的动态拓扑,可以向我们展示跨不同层、数据孤岛、团队和技术的任何更改或故障的原因和影响。这将显著缩短我们解决问题的时间,也同时让我们具备开始自动化根本原因分析、业务影响分析和警报关联的基础。

因此摆在我们面前的问题可能就变成了:有没有一种技术,能够在低侵入的前提下,既可以帮我们自顶向下、深入内核挖掘更多的可观测性,实现 纵向关联打通 ;又可以横向通过访问关系、Trace 串联,打通各个可观测数据之间的因果关系,实现可以追溯 任意时序状态 的动态拓扑?

破局:eBPF 全栈深度观测能力

eBPF 简介

eBPF 是一种数据包过滤技术,从 BPF (Berkeley Packet Filter) 技术扩展而来,它起源于 Linux 内核,可以在操作系统内核中运行沙盒程序。eBPF 被用于安全有效地扩展内核的功能,而无需更改内核源代码或加载内核模块,同时 eBPF 程序在加载的时候有严格的 Verifier 进行校验,可以确保代码的正确性,避免死循环或者非法内存访问等问题,这大大提高了内核拓展的安全性

picture.image

来源:eBPF 社区

eBPF 借助 JIT 机制将字节码转换为机器特定指令集,这使得 eBPF 程序的运行效率与本机内核代码几乎一样高效,并且整个插桩过程对应用程序来说都是无感知、无侵入的。优秀的性能和无侵入的接入方式,很好地回答了前文提到的接入成本的问题。

picture.image

来源:eBPF 社区

eBPF 具备全栈深度观测潜力

除了提供了很多预定义的 Hook 之外,eBPF 还允许我们创建内核探针 (kprobe) 或用户探针 (uprobe) 来将 eBPF 程序附加到内核或用户应用程序中的几乎任何位置。

如下图所示,工程师几乎可以在任何内核子模块、系统库、应用程序中进行插桩,实现观测能力覆盖。这大大提高了技术团队对内核的可编程能力,以解锁更多深度观测能力,也回答了刚刚的可观测性覆盖度问题。

picture.image

来源:eBPF 社区

Kubernetes 观测 VKO

VKO 简介

Kubernetes 观测(VKO)是火山引擎推出的一套面向 Kubernetes 的一体化、全栈式可观测套件,全面支持容器基础、容器集群核心系统组件、AI Infra、网络性能、应用性能等观测能力。

picture.image

VKO 提供深度观测能力。在传统容器基础观测能力之上,VKO 基于 eBPF 实现全栈式采集能力增强,深入内核采集运行时、存储层、网络层、应用层等观测数据,并结合高性能的应用层协议解析模型,实现自顶向下的观测能力全面覆盖,能够将可观测数据自动与 Kubernetes 元数据进行关联,以标准化语义打通流量与资源之间的串联关系。

picture.image

Microscope Agent 支持 Collection 插件拓展机制,以插件化的方式拓展采集能力,自顶向下,全面覆盖用户态框架及系统库、网络层、存储层、运行时等。

picture.image

构建网络、应用拓扑观测能力

如前文所述,eBPF 可以帮助工程师以无侵入、高性能、安全的方式在任意位置进行插桩,从而加深纵向的观测覆盖度。在这个基础上,VKO 进一步拓展了横向因果关联这块的功能,同样借助 eBPF 构建起了时序拓扑能力。

Linux 数据包收发流程

以一个 HTTP 数据包发送流程为例:

  • 发送数据包之前得先建立连接,建连起始于用户空间的 socket 框架函数,再来到内核态 L4 层,经过关键函数 tcp_v4_connect ,最后建立连接;
  • 连接建立之后,后续的数据包也是先从用户空间出发,在 L4 层,会经过关键的 tcp_sendmsg 函数,层层调用之后来到 tcp_transmit_skb 函数完成 TCP 协议处理,封闭 TCP 包头,调用 ip 层的 ip_queue_xmit 进入后续流程。

针对这个流程,我们重点关注以下两个函数,并进行 eBPF 插桩:

  • tcp_v4_connect/tcp_v6_connect:获取连接建立相关数据;
  • tcp_sendmsg:获取 tcp 流量相关数据。

picture.image

收包同理,不过值得注意的是,统计接收数据包我们没有去 hook tcp_recvmsg,主要是考虑到 tcp_cleanup_rbuf 的执行次数会远低于 tcp_recvmsg,性能开销更小,而选择了 tcp_cleanup_rbuf。

L4 网络拓扑

至此我们就可以拿到最基本的流量收发数据了,但这并不意味着可以直接绘制最基本的 L4 网络拓扑。在实际落地过程中,我们发现拓扑需要具备基本的客户端和服务端方向概念,服务端回给客户端的回包也会经过 tcp_sendmsg,那 L4 网络流量该如何区分流量是来自客户端还是服务端?

众所周知,TCP 服务端会维护两个队列:半连接和全连接队列。如下图所示:

picture.image

所以针对这个问题我们的解决方案是通过内核 sock 对象里的 sk_max_ack_backlog 来判断。sk_max_ack_backlog 记录的是 accept queue 的最大长度限制,而服务端的这个参数不可能为 0,基于这个原理,我们就可以轻松识别客户端和服务端身份。

至此,一个最基本的 L4 网络拓扑已经可以成型了。基于这个拓扑,我们可以拓展更多的网络层性能指标,如丢包、重传、Reset、超时、Overflow 等等,完整网络层关键 hook 点如下:

picture.image

L7 应用拓扑

只有 L4 拓扑,在覆盖度方面还是不太够。比如基于 L4 拓扑,我们只能感知到一些网络层的异常情况,当需要观测应用层具体错误码或者哪个接口异常的场景,就无从入手了。因此,我们还需要额外实现 L7 的拓扑能力。

L7 协议流量追踪会比 L4 复杂度更高,需要额外关注应用层协议内容。实现的方案也比较多,既可以和传统 APM 的 SDK/Javaagent 一样,利用 Uprobe 去追踪框架稳定的函数,也可以追踪 socket 相关 Syscall 函数。具体选取哪种 hook 方式,需要具体场景具体分析:

  • HTTP/1.1 场景:我们可以在 socket 层拿到每次完整的 buf 数据,那就可以考虑去 hook socket 相关 syscall,比如通用的 read、write 函数。不过这种方式我们会监听到所有的socket 读写流量,比如磁盘io读写。因此我们需要先在内核进行协议推断,过滤掉不需要关心的数据,然后在用户态进行协议解析才能完成整个流程;
  • HTTPS 场景:由于加密的原因,我们在 socket 层只能拿到加密后的 buf 数据,因此只能考虑在加密之前进行 hook;
  • HTTP/2 场景:由于头部压缩算法 HPACK,我们在 socket 层也是拿不到完整的数据,因此需要在压缩之前就进行 hook。

○ 深入 Syscall

对于实现 L7 流量拓扑,我们需要拿到最核心的两个内核参数:

  • buf:原始报文数据,这边提取是为了用于后续的协议解析,识别具体 L7 协议内容;
  • sockaddr:访问的远端地址,这边提取是为了构建访问关系。

下面以 HTTP/1.1 为例,我们可以先看下从 syscall 里 socket 相关函数可以拿到什么参数。

picture.image

值得注意的是,单纯 hook read/write 这些系统调用,我们只能拿到 buf 数据,无法拿到 sockaddr,而传统的方式,通常会去 hook socket syscall 的 connect 函数来获取 sockaddr,如下所示:

picture.image

但是这种方案有一个问题:在长连接场景,我们的 eBPF 程序启动之前,connect 和 accept 可能就已经发生了,此时就会遗漏这些事件,还需要额外在用户态通过 netlink 去补偿获取,这种方式会让整个链路变得非常冗长。

而我们的方案是通过 bpf_get_current_task 来获取 task_struct ,并根据对应 offset 来获取 socket 对象,进而拿到 sockaddr。

picture.image

如上图所示,我们可以直接通过 bpf_get_current_task 来读取到 sock,顺序大致为 task_struct → files_struct → fd 数组 → file 结构 → sock 指针 → socket 信息:

  • 通过 bpf_get_current_task() 系统调用可以获取到当前进程的 task_struct 指针;
  • task_struct 中有一个 files 字段,类型是 files_struct *,它指向当前进程的文件描述符表 files_struct;
  • files_struct 中维护了当前进程打开文件的文件描述符表,其中包括 socket 在内的所有文件描述符。可以通过遍历这个表,根据 socket 的 fd ,获取对应文件的 file 结构;
  • 每个文件描述符都对应一个 file 结构,socket 的 file 中包含了一个 sock 字段,其类型是 socket *,即这个 socket 的内核对象指针。

有了 sock 指针后,就可以通过 sock 访问 socket 的所有内核信息,如我们最需要的 sockaddr 等。

○ 完整流程

依旧是以 HTTP/1.1 为例,大致流程如下:

  • 基于稳定的 tracepoint 追踪 socket syscall 相关的函数(如 read/write/sendto/recvfrom/close 等);
  • 提取相应的 buf 参数,并从 task_struct 提取 socket 元信息,构建原始 event;
  • 在内核态进行相应的协议推断,判断是否是我们支持的协议,不支持的或者未开启采集的协议数据可以直接从内核态丢弃,减少 perf_buffer 的压力;
  • 将需要进一步处理的 socket buf event 通过 perf_buffer/ringbuffer(5.8 以上内核使用 ringbuffer)推送给用户态程序;
  • 用户态进行数据分帧、协议解析、请求&响应匹配聚合、构建 Flow 数据;
  • 将 Flow 数据转换成对应 Metrics/Traces/Logs 数据并输出。

元数据关联

无论是 L4 还是 L7,我们基于 eBPF 拿到的原始数据都是 socket 五元组(自身 IP 和 Port、对端 IP 和 port、L4 协议类型)、PID、netns 等原始信息 ,在与 K8s 的资源关联起来前,它们还需要经过元数据关联加工。

首先是节点上可以拿到的元数据关联。 基于 /proc 和 CRI 来构建 PID、 Netns、Container 元数据的本地缓存,然后根据 eBPF 拿到的原始 netns、pid 等信息进行反查即可。

其次是远端 IP 的关联。 当远端 IP、Netns 在节点上找不到的情况,我们就会 fallback 到基于 Kubernetes APIServer的方案来获取。 而为了避免每个节点都直连 Kubernetes APIServer,造成大规模场景下 APIServer 的性能压力,我们会将需要进一步加工的数据吐给集群维度的 Opentelemetry Collector,并通过拓展 processor 的方式实现了 K8s 相关的 metadata 关联加工能力。

最后就是 NAT 问题。 在 K8s 场景下,部分 CNI 的实现会对数据包进行 NAT,我们拿到的可能是一个 K8s Service 的 VIP,无法知道真正流量流向的 POD。 这个时候我们就需要 hook conntrack 相关函数来追踪 NAT 行为,并记录 NAT 之后的 IP。

基于 VKO 排查问题

可观测性需要帮助用户很好地反馈上下文,设计上需要以指标、链路、日志为基本,尽可能将信息放在同一个页面中:依托指标发现故障,依托链路判断影响面并分析问题所在,通过日志定位根因,有效组织信息,降低理解成本,进而加速问题排查。 典型的排 查路径为:

  • 通过 RED 指标和 L4 网络指标的观测和告警,及时发现发生故障的资源;
  • 查看资源的接口、上下游调用的应用性能指标,结合网络性能指标和服务访问关系,定位问题根因所在的服务;
  • 向下钻取,关联基础指标资源、日志/时间和内核指标,进行根因分析,排除故障并进行针对性改进。

典型场景一:服务响应慢

服务响应慢的问题在云原生环境中很常见,导致该问题的原因很多,在排障过程中,我们可以用 VKO 开展以下步骤:

定位发现服务响应时间长的资源:收到告警或报障、或者通过资源列表查看排序时,发现某资源(Deployment、StatefulSet、Pod)的服务响应时间高。

  • 例:告警中心触发了 rating-v1 的响应时间长的告警,响应时间已超过 2 秒

picture.image

查看资源详情中该资源的接口性能:查看资源服务的接口的应用性能,将问题范围缩小到具体接口。

  • 例:查看 rating-v1 服务端视角的接口调用响应时间,发现服务自身的响应时间正常

picture.image

  • 例:查看从上游客户端访问 rating-v1 的响应时间,发现响应时间较长,超过 2 秒,可以说明问题并不在应用层接口逻辑本身,而是可能发生在客户端侧的网络层面,我们可以继续往下排查

picture.image

查看指标,定位问题域:查看服务请求、错误与异常、服务延迟、网络通信等各类指标,梳理各指标的关联,确定问题域(应用 or 网络)。

  • 例:进一步查看应用指标和网络指标,发现 TCP 重传和建连耗时指标异常,定位到是网络问题引发的服务响应慢

picture.image

查看日志/事件 ,确定根因 : 在容器服务的日志中心和事件中心查看对应资源和问题域的日志/事件,定位根因,排除故障。

典型场景二:服务拓扑

随着当下技术架构、部署架构越来越复杂,除了定位问题,影响面分析也是一个比较棘手的难题,这时一张拓扑图就非常必要。

服务关系感知:在异常定位的过程中,需要了解流量入口在哪里、有哪些工作负载、每个工作负载的运行状态如何、是否有内外部组件依赖等信息,才能了解故障影响范围,进而采取对应的排障动作。

依赖分析和影响面分析:异常定位中常见的一种情况是问题出现在下游依赖,当这些依赖没有足够的可观测性时,会导致无法进一步分析下去,所以从服务拓扑中进行上下游依赖分析非常重要。 通过应用调用和网络通信的性能指标将上下游用调用关系关联起来,形成调用图,并能够查看任意上下游之间的依赖和指标变化,就能够快速分析上下游依赖是否存在问题。

如下图所示,当 productpage、review、rating 多个资源相互访问的耗时长时,通过服务拓扑的依赖关系,我们就能够发现调用链路是 loop-productpage → productpage → review → rating,问题大概率是 review → rating 的访问耗时高导致,之后便可以将故障排查集中在 review → rating 这一段。

picture.image

典型场景三:网络性能监测

在容器环境中,网络问题是比较棘手的问题之一,因为 Kubernetes 本身的网络架构非常复杂,网络拓扑不是一成不变的,同时网络问题的排查需要大量的专业知识。

典型的 Kuberentes 场景的网络问题包括 IP 冲突、DNS 解析失败/耗时长、配置问题导致的服务网络不同等。以下是一个排查 core-dns 问题的典型示例:

  • 告警中心触发 DNS 的响应时间长的告警;

picture.image

  • 在拓扑图中查看到 coredns 的访问,展开详情发现很多 serverFailed 的错误,进一步将故障定位在 DNS 的服务端;

picture.image

  • 之后可以进一步查看 coredns 的监控看板和 coredns 和上级 DNS 服务端之间的 DNS 指标做进一步排查。
总结

VKO 的 “因果可观测性” 核心思路就是:通过使用 eBPF 无侵入的采集多语言、多网络协议的性能指标(包括应用性能的 RED 指标和网络性能指标),结合 Kubenretes 对象的各类上下文和元数据,将应用观测、网络观测、Kubernetes 资源观测、日志事件、内核事件等关联起来,整合显示在同一个资源的观测页面中,实现从应用性能视角出发,自上而下地处理和分析服务响应异常、网络性能故障等场景的观测排障流程,进而实现了 Kubernetes 的一站式可观测。

目前,VKO 已经开始面向企业用户提供服务。未来,火山引擎云原生团队也将持续推进 VKO 的长期迭代和功能完善,主要围绕以下几个方面:

持续丰富深度观测能力

下方核心功能正在陆续上线中,以更好支撑企业可观测需求:

  • 日志、事件中心
  • 观测能力市场,补齐更多的预置告警模板
  • 支持更多应用层协议
  • 补齐更丰富的网络层、资源层指标
  • 网络异常事件分析
  • eBPF AutoTracing
  • CPU/GPU Profiling

结合 AIOps 实现主动可观测性

AIOps 是可观测性成熟度模型中的最高级别,需要建立在先前级别模型核心功能之上,而 “因果可观测性” 是 AIOps 非常重要的依赖。

picture.image

Gartner 指出拓扑可以大大提高因果关系确定的准确性和有效性。传统的拓扑方案,数据可能会来自不同孤岛,这在数据规范化、相关性和质量方面提出了挑战,这些挑战可能需要新功能甚至组织变革来解决。此外,很难大规模收集和操作高质量的拓扑数据,尤其是在不太现代化的环境中。

在没有全面数据基础的情况下应用 AI/ML,实际上可能会造成损害。基于传统拓扑方案来实现,可能会需要一个观测数据集中清洗的流程,而这个会是一个非常漫长的过程,也是传统方案下 AIOps 落地效果不尽人意的重要原因。

而基于 eBPF 构建的统一采集方案,可以很好地避免这个问题。因为我们可以借助 eBPF 的全栈采集能力,统一收敛绝大部分的元数据标签和数据协议,提供高度统一的高质量可观测数据。这为更多企业成功迈向 AIOps ,提供了极大的便利。

欢迎感兴趣的用户扫码咨询、使用!

picture.image

相关链接

[1] 火山引擎: www.volcengine.com

[2] 火山引擎 VKE: www.volcengine.com/product/vke

火山引擎云原生团队

火山引擎云原生团队主要负责火山引擎公有云及私有化场景中 PaaS 类产品体系的构建,结合字节跳动多年的云原生技术栈经验和最佳实践沉淀,帮助企业加速数字化转型和创新。产品包括容器服务、镜像仓库、分布式云原生平台、函数服务、服务网格、持续交付、可观测服务等。

picture.image

picture.image

picture.image

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论