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

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

在谈论云原生领域的可观测性时,大家可能都会在某个时候提到 OpenTelemetry (OTEL)。这很棒,因为社区需要依赖标准来引导所有集群组件朝着同一个方向发展。OpenTelemetry 使我们能够将日志、指标、追踪以及其他上下文信息(称为 Baggage)组合成一个单一资源。集群管理员或软件工程师可以使用此资源在指定的时间段内获取集群运行状况的概览。但是,Kubernetes 本身如何利用这项技术栈呢?

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

  • kube-apiserver: 验证和配置 API 对象的数据
  • kubelet: 运行在每个节点上的代理
  • CRI runtime: 容器运行时接口 (CRI) 兼容的容器运行时,例如 CRI-Ocontainerd
  • OCI runtime: 底层 Open Container Initiative (OCI) 运行时,例如 runccrun
  • Linux kernelMicrosoft Windows: 底层操作系统

这意味着如果我们在 Kubernetes 中运行容器时遇到问题,我们会从这些组件之一开始排查。查找问题的根本原因是我们面对当今集群设置日益增长的架构复杂性时最耗时的操作之一。即使我们知道哪个组件似乎导致了问题,我们仍然需要考虑其他组件,以维持事件发生的心中时间线。我们如何做到这一点呢?大多数人可能会坚持抓取日志,过滤它们,并将它们跨组件边界组合在一起。我们也有指标,对吗?没错,但是将指标值与纯日志关联起来,使得追踪正在发生的事情变得更加困难。一些指标也不是为了调试目的而设计的。它们是基于集群最终用户的视角定义的,用于关联可用的警报,而不是为了开发者调试集群设置而定义的。

OpenTelemetry 来救援了:该项目旨在将 追踪 (traces)指标 (metrics)日志 (logs) 等信号组合在一起,以保持对集群状态的正确观察视角。

OpenTelemetry 追踪在 Kubernetes 中的当前状态如何?从 API 服务器的角度来看,我们自 Kubernetes v1.22 起就提供了追踪的 Alpha 支持,它将在后续版本中升级到 Beta。遗憾的是,Beta 升级未能赶上 Kubernetes v1.26 版本发布。设计方案可以在API Server Tracing Kubernetes 增强提案 (KEP) 中找到,其中提供了更多相关信息。

kubelet 的追踪部分在另一个 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 追踪,并正在持续改进,例如通过将日志附加到追踪或将 span 扩展到应用的逻辑部分。这有助于追踪的用户获取与解析日志相同的信息,但具有增强的范围界定和过滤到其他 OTEL 信号的能力。CRI-O 维护者还在开发一个替代 conmon 的容器监控工具,名为 conmon-rs,完全使用 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 二进制文件,或者使用如下的 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 CollectorgRPC 端点 localhost:4317,该端点也必须启动并运行。运行 OTLP 的方法有多种,如文档中所述,但也可以通过 kubectl proxy 连接到 Kubernetes 内运行的现有实例。

如果一切设置妥当,Collector 应该会记录到有传入的追踪:

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 由于 Pod 生命周期事件生成器 (PLEG) 定期触发对 CRI-O 的 ListPodSandbox RPC 调用。这些追踪可以通过例如 Jaeger 来显示。当在本地运行追踪堆栈时,Jaeger 实例默认应该暴露在 http://localhost: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,我们可以看到对于 RunPodSandboxCreateContainer 这两个 CRI RPC,我们有来自 conmonrscrio 以及 kubelet 的追踪:

Container creation in the Jaeger UI

kubelet 和 CRI-O 的 Span 彼相互连接,方便调查。如果我们仔细查看这些 Span,就可以看到 CRI-O 的日志与相应的功能正确关联。例如,我们可以像这样从追踪中提取容器用户:

CRI-O in the Jaeger UI

conmon-rs 的底层 Span 也是此追踪的一部分。例如,conmon-rs 维护了一个内部 read_loop 来处理容器与最终用户之间的 IO。读写字节的日志是该 Span 的一部分。wait_for_exit_code Span 也是如此,它告诉我们容器以代码 0 成功退出:

conmon-rs in the Jaeger UI

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

感谢阅读这篇博客文章,我非常确信 Kubernetes 未来在 OpenTelemetry 支持方面前景光明,这将使故障排除变得更简单。