本文已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已不正确。

使用 OpenTelemetry 增强 Kubernetes 容器运行时可观察性

当谈到云原生领域的可观察性时,可能每个人都会在对话中的某个时刻提到 OpenTelemetry (OTEL)。这很好,因为社区需要依靠标准来将所有集群组件朝着相同的方向发展。OpenTelemetry 使我们能够将日志、指标、跟踪和其他上下文信息(称为 baggage)组合到单个资源中。集群管理员或软件工程师可以使用此资源来获取有关集群在定义的时间段内发生的情况的视口。但是 Kubernetes 本身如何利用这项技术堆栈呢?

Kubernetes 由多个组件组成,其中一些是独立的,而另一些则堆叠在一起。从容器运行时角度来看架构,从上到下分别是:

  • kube-apiserver:验证和配置 API 对象的数据
  • kubelet:在每个节点上运行的代理
  • CRI 运行时:容器运行时接口 (CRI) 兼容的容器运行时,例如 CRI-Ocontainerd
  • OCI 运行时:较低级别的 开放容器倡议 (OCI) 运行时,例如 runccrun
  • Linux 内核Microsoft Windows:底层操作系统

这意味着如果我们在 Kubernetes 中运行容器时遇到问题,那么我们首先会查看这些组件之一。查找问题的根本原因是我们面对当今集群设置中不断增加的架构复杂性时最耗时的操作之一。即使我们知道似乎导致问题的组件,我们仍然必须考虑其他组件,以保持对正在发生的事件的心理时间线。我们如何实现这一目标?嗯,大多数人可能会坚持抓取日志、过滤它们并将它们跨组件边界组合在一起。我们也有指标,对吧?没错,但是将指标值与纯日志关联起来会使跟踪正在发生的事情变得更加困难。一些指标也不是为调试目的而设计的。它们是根据集群的最终用户视角定义的,用于链接可用的警报,而不是用于开发人员调试集群设置。

OpenTelemetry 来救援:该项目旨在将 跟踪指标日志等信号组合在一起,以保持对集群状态的正确视口。

Kubernetes 中 OpenTelemetry 跟踪的当前状态是什么?从 API 服务器的角度来看,自 Kubernetes v1.22 以来,我们对跟踪有 alpha 支持,这将在即将发布的版本之一中升级为 beta 版。不幸的是,beta 版的升级错过了 v1.26 Kubernetes 版本。可以在 API 服务器跟踪 Kubernetes 增强提案 (KEP) 中找到设计提案,其中提供了有关此的更多信息。

kubelet 跟踪部分在 另一个 KEP 中跟踪,该 KEP 在 Kubernetes v1.25 中以 alpha 状态实现。截至撰写本文时,尚未计划 beta 版升级,但可能会在 v1.27 发布周期中推出更多。除了这两个 KEP 之外,还有其他方面的努力,例如 klog 正在考虑 OTEL 支持,这将通过将日志消息链接到现有跟踪来提高可观察性。在 SIG Instrumentation 和 SIG Node 中,我们也在讨论 如何将 kubelet 跟踪链接在一起,因为目前它们专注于 kubelet 和 CRI 容器运行时之间的 gRPC 调用。

CRI-O 自 v1.23.0 起 具有 OpenTelemetry 跟踪支持,并且正在不断改进它们,例如通过 将日志附加到跟踪或将 跨度扩展到应用程序的逻辑部分。这有助于跟踪的用户获得与解析日志相同的信息,但具有增强的范围和过滤到其他 OTEL 信号的功能。CRI-O 维护人员也在努力开发一个容器监控替代品来替代 conmon,它被称为 conmon-rs 并且完全用 Rust 编写。采用 Rust 实现的好处之一是能够添加 OpenTelemetry 支持之类的功能,因为这些功能的 crates(库)已经存在。这允许与 CRI-O 紧密集成,并让消费者看到来自其容器的最低级别跟踪数据。

containerd 人员自 v1.6.0 起添加了跟踪支持,该支持可通过 使用插件获得。较低级别的 OCI 运行时,例如 runccrun 根本不支持 OTEL,而且似乎没有对此进行计划。我们始终必须考虑到收集跟踪以及将它们导出到数据接收器时会产生性能开销。我仍然认为值得评估扩展的遥测收集在 OCI 运行时中可能是什么样子。让我们看看 Rust OCI 运行时 youki 将来是否会考虑类似的事情。

我将向您展示如何尝试。对于我的演示,我将坚持使用具有 runc、conmon-rs、CRI-O 和 kubelet 的单个本地节点的堆栈。要在 kubelet 中启用跟踪,我需要应用以下 KubeletConfiguration

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
  KubeletTracing: true
tracing:
  samplingRatePerMillion: 1000000

samplingRatePerMillion 等于 100 万将内部转换为采样所有内容。类似的配置必须应用于 CRI-O;我可以启动带有 --enable-tracing--tracing-sampling-rate-per-million 1000000crio 二进制文件,或者我们使用如下所示的 drop-in 配置

cat /etc/crio/crio.conf.d/99-tracing.conf
[crio.tracing]
enable_tracing = true
tracing_sampling_rate_per_million = 1000000

要配置 CRI-O 以使用 conmon-rs,您至少需要最新的 CRI-O v1.25.x 和 conmon-rs v0.4.0。然后可以使用如下所示的配置 drop-in 使 CRI-O 使用 conmon-rs

cat /etc/crio/crio.conf.d/99-runtimes.conf
[crio.runtime]
default_runtime = "runc"

[crio.runtime.runtimes.runc]
runtime_type = "pod"
monitor_path = "/path/to/conmonrs" # or will be looked up in $PATH

就这样,默认配置将指向 OpenTelemetry 收集器 gRPC 端点 localhost:4317,它也必须启动并运行。有多种运行 OTLP 的方法,如 文档中所述,但是也可以 kubectl proxy 到 Kubernetes 中正在运行的现有实例中。

如果一切都设置正确,则收集器应记录有传入的跟踪

ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope go.opentelemetry.io/otel/sdk/tracer
Span #0
    Trace ID       : 71896e69f7d337730dfedb6356e74f01
    Parent ID      : a2a7714534c017e6
    ID             : 1d27dbaf38b9da8b
    Name           : github.com/cri-o/cri-o/server.(*Server).filterSandboxList
    Kind           : SPAN_KIND_INTERNAL
    Start time     : 2022-11-15 09:50:20.060325562 +0000 UTC
    End time       : 2022-11-15 09:50:20.060326291 +0000 UTC
    Status code    : STATUS_CODE_UNSET
    Status message :
Span #1
    Trace ID       : 71896e69f7d337730dfedb6356e74f01
    Parent ID      : a837a005d4389579
    ID             : a2a7714534c017e6
    Name           : github.com/cri-o/cri-o/server.(*Server).ListPodSandbox
    Kind           : SPAN_KIND_INTERNAL
    Start time     : 2022-11-15 09:50:20.060321973 +0000 UTC
    End time       : 2022-11-15 09:50:20.060330602 +0000 UTC
    Status code    : STATUS_CODE_UNSET
    Status message :
Span #2
    Trace ID       : fae6742709d51a9b6606b6cb9f381b96
    Parent ID      : 3755d12b32610516
    ID             : 0492afd26519b4b0
    Name           : github.com/cri-o/cri-o/server.(*Server).filterContainerList
    Kind           : SPAN_KIND_INTERNAL
    Start time     : 2022-11-15 09:50:20.0607746 +0000 UTC
    End time       : 2022-11-15 09:50:20.060795505 +0000 UTC
    Status code    : STATUS_CODE_UNSET
    Status message :
Events:
SpanEvent #0
     -> Name: log
     -> Timestamp: 2022-11-15 09:50:20.060778668 +0000 UTC
     -> DroppedAttributesCount: 0
     -> Attributes::
          -> id: Str(adf791e5-2eb8-4425-b092-f217923fef93)
          -> log.message: Str(No filters were applied, returning full container list)
          -> log.severity: Str(DEBUG)
          -> name: Str(/runtime.v1.RuntimeService/ListContainers)

我可以看到跨度具有跟踪 ID,并且通常附加了父级。日志之类的事件也属于输出的一部分。在上面的示例中,kubelet 定期触发 PLEG(Pod 生命周期事件生成器)导致的 ListPodSandbox RPC 到 CRI-O。可以通过例如 Jaeger 来显示这些跟踪。在本地运行跟踪堆栈时,Jaeger 实例应默认在 https://127.0.0.1:16686 上公开。

ListPodSandbox 请求直接在 Jaeger UI 中可见

ListPodSandbox RPC in the Jaeger UI

这没什么令人兴奋的,所以我将通过 kubectl 直接运行工作负载

kubectl run -it --rm --restart=Never --image=alpine alpine -- echo hi
hi
pod "alpine" deleted

现在查看 Jaeger,我们可以看到我们有 conmonrscrio 以及 kubeletRunPodSandboxCreateContainer CRI RPC 的跟踪

Container creation in the Jaeger UI

kubelet 和 CRI-O 跨度相互连接,以便于调查。如果我们现在仔细查看跨度,那么我们可以看到 CRI-O 的日志与相应的功能正确地关联在一起。例如,我们可以从跟踪中提取容器用户,如下所示

CRI-O in the Jaeger UI

conmon-rs 的较低级别跨度也属于此跟踪的一部分。例如,conmon-rs 维护一个内部 read_loop 用于处理容器和最终用户之间的 IO。读取和写入字节的日志是跨度的一部分。这同样适用于 wait_for_exit_code 跨度,该跨度告诉我们容器已成功退出,代码为 0

conmon-rs in the Jaeger UI

将所有这些信息与 Jaeger 的过滤功能并排放在一起,使得整个堆栈成为调试容器问题的绝佳解决方案!提到“整个堆栈”也显示了整体方法的最大缺点:与解析日志相比,它在集群设置之上增加了明显的开销。用户必须维护像 Elasticsearch 这样的接收器来持久化数据,暴露 Jaeger UI,并可能考虑性能上的缺陷。无论如何,它仍然是提高 Kubernetes 可观察性的最佳方法之一。

感谢您阅读这篇博文,我确信我们正在展望 Kubernetes 中 OpenTelemetry 支持的光明未来,以使故障排除更加简单。