云原生之深入解析提高K8S监控可观察性的实战操作

时间:2024-09-30 22:34:22
  • 当谈到云原生可观察性时,可能每个人都会提到 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: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
  KubeletTracing: true
tracing:
  samplingRatePerMillion: 1000000
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 等于 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
  • 1
[crio.tracing]
enable_tracing = true
tracing_sampling_rate_per_million = 1000000
  • 1
  • 2
  • 3
  • 要将 CRI-O 配置为使用 conmon-rs,至少需要最新的 CRI-O v1. 和 conmon-rs v0.4.0,然后像如下这样配置插件可以让 CRI-O 使用 conmon-rs:
cat /etc/crio/crio.conf.d/99-runtimes.conf
  • 1
[crio.runtime]
default_runtime = "runc"

[crio.runtime.runtimes.runc]
runtime_type = "pod"
monitor_path = "/path/to/conmonrs" # or will be looked up in $PATH
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 默认配置将指向的 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     : 2023-07-07 09:50:20.060325562 +0000 UTC
    End time       : 2023-07-07 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     : 2023-07-07 09:50:20.060321973 +0000 UTC
    End time       : 2023-07-07 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     : 2023-07-07 09:50:20.0607746 +0000 UTC
    End time       : 2023-07-07 09:50:20.060795505 +0000 UTC
    Status code    : STATUS_CODE_UNSET
    Status message :
Events:
SpanEvent #0
     -> Name: log
     -> Timestamp: 2023-07-07 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 可以看到 spans 有一个 trace ID,并且通常有一个附加 attached,日志等事件也是输出的一部分。在上述情况下,kubelet 的 Pod 生命周期事件生成器 (PLEG) 定期触发 ListPodSandbox 调用 CRI-O 的 RPC。可以通过例如 Jaeger 来显示这些 traces,在本地运行跟踪堆栈时,默认情况下应公开一个 Jaeger 实例地址为:http://localhost:16686。
  • 这些 ListPodSandbox 请求在 Jaeger UI 中直接可见:

Jaeger UI 中的 ListPodSandbox RPC

  • 效果不是很理想,因此将直接通过 kubectl 运行工作负载:
kubectl run -it --rm --restart=Never --image=alpine alpine -- echo hi
  • 1
hi
pod "alpine" deleted
  • 1
  • 2
  • 现在查看 Jaeger,可以看到有 conmonrs,crio 以及 kubelet 的 RunPodSandbox 和 CreateContainer CRI RPC 的 traces:

Jaeger UI 中创建容器 traces

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

Jaeger UI 中的 CRI-O Traces

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

Jaeger UI 中的 conmon-rs Traces

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