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

使用 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 Server Tracing 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-rs 的容器监控替代品,用于替代 conmon,它完全用 Rust 编写。拥有 Rust 实现的一个好处是能够添加 OpenTelemetry 支持等功能,因为这些功能的 crates(库)已经存在。这允许与 CRI-O 紧密集成,并让消费者看到来自其容器的最低级别的追踪数据。

containerd 的开发者自 v1.6.0 起添加了追踪支持,可以通过使用插件来使用。像 runccrun 这样的底层 OCI 运行时完全不支持 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 在内部会转换为对所有内容进行采样。类似的配置也必须应用于 CRI-O;我可以使用 --enable-tracing--tracing-sampling-rate-per-million 1000000 启动 crio 二进制文件,或者使用如下的插入式配置

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。然后可以使用如下的配置插入来使 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

就是这样,默认配置将指向一个 localhost:4317OpenTelemetry 收集器 gRPC 端点,该端点也必须启动并运行。有多种方式可以运行 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)

我可以看到 span 有一个追踪 ID,并且通常附加了一个父级。事件(例如日志)也是输出的一部分。在上述情况下,kubelet 正在周期性地向 CRI-O 触发一个 ListPodSandbox RPC,这是由 Pod 生命周期事件生成器 (PLEG) 引起的。可以通过例如 Jaeger 来显示这些追踪。当在本地运行追踪堆栈时,Jaeger 实例默认应该暴露在 https://: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 支持的光明未来将使故障排除变得更简单。