当谈到云原生可观察性时,可能每个人都会提到​​OpenTelemetry (OTEL)​​,因为社区需要依赖标准来将所有集群组件开发指向到同一方向。OpenTelemetry 使我们能够将日志、指标(metrics)、跟踪(traces)和其他上下文信息组合到一个资源中。集群管理员或软件工程师可以使用此资源来获取在定义的时间段内集群中正在发生的事情的视图。

但是 Kubernetes 本身如何利用这个技术栈呢?

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

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

这意味着如果我们在 Kubernetes 中运行容器时遇到问题,那么我们就会开始查看其中一个组件。随着当今集群架构复杂性不断增加,查找问题的根本原因是我们面临的最耗时的操作之一。即使知道可能导致问题的组件,仍须考虑其他组件。

我们如何做到这一点?大多数人可能会坚持抓取日志,过滤它们并在组件边界上将它们组装在一起。我们也有 metrics 指标,但是将指标值与普通日志相关联使跟踪正在发生的事情变得更加困难。一些指标也不是为了调试目的而制定的。

OpenTelemetry 应运而生。该项目旨在将 ​​traces~跟踪​​​、​​metrics~指标​​​和 ​​logs~日志​​ 等信号组合在一起,以维护集群状态的统一视图。

Kubernetes 中 OpenTelemetry 跟踪的当前状态是什么?从 API server 的角度来看,自 Kubernetes v1.22 以来,我们对 tracing 提供了 alpha 支持,它将在即将发布的其中一个版本中升级为 beta。不幸的是,beta 毕业错过了 Kubernetes v1.26 版本。可以在 ​​API Server Tracing Kubernetes Enhancement Proposal (KEP)​​中找到该设计提案,其中提供了更多相关信息。

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

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

​containerd​​​ 从 v1.6.0 开始添加了 tracing 支持,可​​通过使用插件​​​ 获得。较低级别的 OCI 运行时,如 ​​runc​​​ 或 ​​crun​​​,根本不支持 OTEL,而且似乎不存在这方面的计划。我们必须考虑到在收集 traces 并将它们导出到数据接收器时会产生性能开销。我仍然认为在 OCI 运行时扩展的遥测收集看起来是值得评估的。让我们看看 Rust OCI 运行时 ​​youki​​ 将来是否会考虑类似的事情。

下面我会展示如何做。对于下面演示,我将坚持使用 runc、conmon-rs、CRI-O 和 kubelet 的单个本地节点堆栈。要在 kubelet 中启用跟踪,需要在​​KubeletConfiguration​​中配置以下内容:

apiVersion: /v1beta1
kind: KubeletConfiguration
featureGates:
KubeletTracing: true
tracing:
samplingRatePerMillion: 1000000

等于一​​samplingRatePerMillion​​​百万将在内部转化为对所有内容进行采样。必须将类似的配置应用于 CRI-O;我可以使用 and 启动二进制 ​​crio​​​文件,或者我们使用这样的嵌入式配置:​​--enable-tracing``--tracing-sampling-rate-per-million 1000000​

一个 ​​samplingRatePerMillion​​​ 等于 100 万将在内部转化为对所有内容的抽样。必须对 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

默认配置将指向的 ​​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 : /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 : /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 : /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)

可以看到 spans 有一个 trace ID,并且通常有一个附加 attached。日志等事件也是输出的一部分。在上述情况下,kubelet 的 Pod 生命周期事件生成器 (PLEG) 定期触发 ​​ListPodSandbox​​​ 调用 CRI-O 的 RPC。可以通过例如 ​​Jaeger​​​ 来显示这些 traces 。在本地运行跟踪堆栈时,默认情况下应公开一个 Jaeger 实例地址为:​​http://localhost:16686​​。

这些​​ListPodSandbox​​请求在 Jaeger UI 中直接可见:

提高 K8S 容器运行时的可观察性最佳方法之一_容器

这并不太令人兴奋,所以我将直接通过​​kubectl​​运行工作负载:

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

现在查看 Jaeger,我们可以看到有 ​​conmonrs​​​,​​crio​​​以及​​kubelet​​​的​​RunPodSandbox​​​和​​CreateContainer​​ CRI RPC 的 traces:

提高 K8S 容器运行时的可观察性最佳方法之一_容器_02

kubelet 和 CRI-O spans 相互连接,使调查更容易。如果我们现在仔细查看这些 spans,可以看到 CRI-O 的日志正确地包含了相应的功能。例如,可以像这样从 traces 中提取 container user:

提高 K8S 容器运行时的可观察性最佳方法之一_k8s_03

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

提高 K8S 容器运行时的可观察性最佳方法之一_containerd_04

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

感谢您阅读这篇博文,我很确定大家正在展望 Kubernetes 对 OpenTelemetry 支持的光明前景,以简化故障排除。

译自

作者:Sascha Grunert
原文:https://kubernetes.io/blog/2022/12/01/runtime-observability-opentelemetry/