使用运行时可观测性创建 Kubernetes 集群

博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。

贡献者:Sebastian ChorenAdnan RahićKen Hamric

Kubernetes 是一个开源系统,在云原生领域被广泛用于提供在云中部署和扩展容器化应用程序的方法。它观察日志和指标的能力 众所周知且有详细记录,但其应用程序跟踪的可观测性是新的。

这是 Kubernetes 生态系统中近期活动的一个简要概述

在调查 Kubernetes 当前的跟踪状态时,我们发现很少有文章记录如何启用它,例如这篇关于 `kubelet` 可观测性的 Kubernetes 博客文章。我们决定记录我们的发现,并提供分步说明来本地设置 Kubernetes 并检查跟踪。

您将了解如何将此仪器化与 Kubernetes 结合使用,通过设置本地可观测性环境,然后进行本地 Kubernetes 安装并启用跟踪,来开始在其 API(kube-apiserver)、节点代理(kubelet)和容器运行时(containerd)上观察跟踪。

首先,在本地机器上安装以下工具

  • Docker:一个容器环境,允许我们运行容器化环境
  • k3d:一个运行 k3s(一个轻量级 Kubernetes 发行版)的包装器,使用 Docker
  • kubectl:一个用于与集群交互的 Kubernetes CLI

设置可观测性堆栈以监控跟踪

要设置可观测性堆栈,您将运行 OpenTelemetry (OTel) Collector,这是一个接收来自不同应用程序遥测数据并将其发送到跟踪后端的工具。作为跟踪后端,您将使用 Jaeger,这是一个收集跟踪并允许您查询它们的开源工具。

在您的机器上,创建一个名为 `kubetracing` 的目录,并创建一个名为 otel-collector.yaml 的文件,将以下代码片段的内容复制并保存在您偏好的文件夹中。

此文件将配置 OpenTelemetry Collector 以 OpenTelemetry 格式接收跟踪,并将其导出到 Jaeger。

receivers:
  otlp:
    protocols:
      grpc:
      http:
processors:
  probabilistic_sampler:
    hash_seed: 22
    sampling_percentage: 100
  batch:
    timeout: 100ms
exporters:
  logging:
    logLevel: debug
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [probabilistic_sampler, batch]
      exporters: [otlp/jaeger, logging]

之后,在同一个文件夹中,创建一个 docker-compose.yaml 文件,其中将包含两个容器:一个用于 Jaeger,另一个用于 OpenTelemetry Collector。

services:
  jaeger:
    healthcheck:
      test:
        - CMD
        - wget
        - --spider
        - localhost:16686
      timeout: 3s
      interval: 1s
      retries: 60
    image: jaegertracing/all-in-one:latest
    restart: unless-stopped
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    ports:
      - 16686:16686
  otel-collector:
    command:
      - --config
      - /otel-local-config.yaml
    depends_on:
      jaeger:
        condition: service_started
    image: otel/opentelemetry-collector:0.54.0
    ports:
      - 4317:4317
    volumes:
      - ./otel-collector.yaml:/otel-local-config.yaml

现在,通过在 `kubetracing` 文件夹中运行以下命令来启动可观测性环境

docker compose up

这将启动 Jaeger 和 OpenTelemetry Collector,使它们能够接收来自其他应用程序的跟踪。

使用运行时可观测性创建 Kubernetes 集群

设置好可观测性环境后,创建配置文件以在 `kube-apiserver`、`kubelet` 和 `containerd` 中启用 OpenTelemetry 跟踪。

在 `kubetracing` 文件夹内,创建一个名为 `config` 的子文件夹,其中包含以下两个文件。

首先是 apiserver-tracing.yaml,它包含 `kube-apiserver` 使用的跟踪配置,用于导出包含 Kubernetes API 执行数据的跟踪。在此配置中,设置 API 以 `samplingRatePerMillion` 配置发送 100% 的跟踪。将端点设置为 `host.k3d.internal:4317`,以便 `k3d/k3s` 创建的集群能够调用您机器上的另一个 API。在这种情况下,通过 `docker compose` 部署的 OpenTelemetry Collector 监听端口 `4317`。

apiVersion: apiserver.config.k8s.io/v1beta1
kind: TracingConfiguration
endpoint: host.k3d.internal:4317
samplingRatePerMillion: 1000000 # 100%

第二个文件是 kubelet-tracing.yaml,它为 `kubelet` 提供了附加配置。在这里,您将启用功能标志 `KubeletTracing`(在 Kubernetes 1.27 中为 beta 功能,即本文撰写时的当前版本),并设置与 `kube-apiserver` 中设置的相同的跟踪设置。

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
  KubeletTracing: true
tracing:
  endpoint: host.k3d.internal:4317
  samplingRatePerMillion: 1000000 # 100%

返回 `kubetracing` 文件夹,创建最后一个文件 config.toml.tmpl,这是一个由 `k3s` 用于配置 `containerd` 的模板文件。此文件与 `k3s` 使用的默认配置类似,但在文件末尾增加了两个部分,用于配置 `containerd` 发送跟踪。

version = 2

[plugins."io.containerd.internal.v1.opt"]
  path = "{{ .NodeConfig.Containerd.Opt }}"
[plugins."io.containerd.grpc.v1.cri"]
  stream_server_address = "127.0.0.1"
  stream_server_port = "10010"
  enable_selinux = {{ .NodeConfig.SELinux }}
  enable_unprivileged_ports = {{ .EnableUnprivileged }}
  enable_unprivileged_icmp = {{ .EnableUnprivileged }}

{{- if .DisableCgroup}}
  disable_cgroup = true
{{end}}
{{- if .IsRunningInUserNS }}
  disable_apparmor = true
  restrict_oom_score_adj = true
{{end}}

{{- if .NodeConfig.AgentConfig.PauseImage }}
  sandbox_image = "{{ .NodeConfig.AgentConfig.PauseImage }}"
{{end}}

{{- if .NodeConfig.AgentConfig.Snapshotter }}
[plugins."io.containerd.grpc.v1.cri".containerd]
  snapshotter = "{{ .NodeConfig.AgentConfig.Snapshotter }}"
  disable_snapshot_annotations = {{ if eq .NodeConfig.AgentConfig.Snapshotter "stargz" }}false{{else}}true{{end}}
{{ if eq .NodeConfig.AgentConfig.Snapshotter "stargz" }}
{{ if .NodeConfig.AgentConfig.ImageServiceSocket }}
[plugins."io.containerd.snapshotter.v1.stargz"]
cri_keychain_image_service_path = "{{ .NodeConfig.AgentConfig.ImageServiceSocket }}"
[plugins."io.containerd.snapshotter.v1.stargz".cri_keychain]
enable_keychain = true
{{end}}
{{ if .PrivateRegistryConfig }}
{{ if .PrivateRegistryConfig.Mirrors }}
[plugins."io.containerd.snapshotter.v1.stargz".registry.mirrors]{{end}}
{{range $k, $v := .PrivateRegistryConfig.Mirrors }}
[plugins."io.containerd.snapshotter.v1.stargz".registry.mirrors."{{$k}}"]
  endpoint = [{{range $i, $j := $v.Endpoints}}{{if $i}}, {{end}}{{printf "%q" .}}{{end}}]
{{if $v.Rewrites}}
  [plugins."io.containerd.snapshotter.v1.stargz".registry.mirrors."{{$k}}".rewrite]
{{range $pattern, $replace := $v.Rewrites}}
    "{{$pattern}}" = "{{$replace}}"
{{end}}
{{end}}
{{end}}
{{range $k, $v := .PrivateRegistryConfig.Configs }}
{{ if $v.Auth }}
[plugins."io.containerd.snapshotter.v1.stargz".registry.configs."{{$k}}".auth]
  {{ if $v.Auth.Username }}username = {{ printf "%q" $v.Auth.Username }}{{end}}
  {{ if $v.Auth.Password }}password = {{ printf "%q" $v.Auth.Password }}{{end}}
  {{ if $v.Auth.Auth }}auth = {{ printf "%q" $v.Auth.Auth }}{{end}}
  {{ if $v.Auth.IdentityToken }}identitytoken = {{ printf "%q" $v.Auth.IdentityToken }}{{end}}
{{end}}
{{ if $v.TLS }}
[plugins."io.containerd.snapshotter.v1.stargz".registry.configs."{{$k}}".tls]
  {{ if $v.TLS.CAFile }}ca_file = "{{ $v.TLS.CAFile }}"{{end}}
  {{ if $v.TLS.CertFile }}cert_file = "{{ $v.TLS.CertFile }}"{{end}}
  {{ if $v.TLS.KeyFile }}key_file = "{{ $v.TLS.KeyFile }}"{{end}}
  {{ if $v.TLS.InsecureSkipVerify }}insecure_skip_verify = true{{end}}
{{end}}
{{end}}
{{end}}
{{end}}
{{end}}

{{- if not .NodeConfig.NoFlannel }}
[plugins."io.containerd.grpc.v1.cri".cni]
  bin_dir = "{{ .NodeConfig.AgentConfig.CNIBinDir }}"
  conf_dir = "{{ .NodeConfig.AgentConfig.CNIConfDir }}"
{{end}}

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
  runtime_type = "io.containerd.runc.v2"

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = {{ .SystemdCgroup }}

{{ if .PrivateRegistryConfig }}
{{ if .PrivateRegistryConfig.Mirrors }}
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]{{end}}
{{range $k, $v := .PrivateRegistryConfig.Mirrors }}
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."{{$k}}"]
  endpoint = [{{range $i, $j := $v.Endpoints}}{{if $i}}, {{end}}{{printf "%q" .}}{{end}}]
{{if $v.Rewrites}}
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors."{{$k}}".rewrite]
{{range $pattern, $replace := $v.Rewrites}}
    "{{$pattern}}" = "{{$replace}}"
{{end}}
{{end}}
{{end}}

{{range $k, $v := .PrivateRegistryConfig.Configs }}
{{ if $v.Auth }}
[plugins."io.containerd.grpc.v1.cri".registry.configs."{{$k}}".auth]
  {{ if $v.Auth.Username }}username = {{ printf "%q" $v.Auth.Username }}{{end}}
  {{ if $v.Auth.Password }}password = {{ printf "%q" $v.Auth.Password }}{{end}}
  {{ if $v.Auth.Auth }}auth = {{ printf "%q" $v.Auth.Auth }}{{end}}
  {{ if $v.Auth.IdentityToken }}identitytoken = {{ printf "%q" $v.Auth.IdentityToken }}{{end}}
{{end}}
{{ if $v.TLS }}
[plugins."io.containerd.grpc.v1.cri".registry.configs."{{$k}}".tls]
  {{ if $v.TLS.CAFile }}ca_file = "{{ $v.TLS.CAFile }}"{{end}}
  {{ if $v.TLS.CertFile }}cert_file = "{{ $v.TLS.CertFile }}"{{end}}
  {{ if $v.TLS.KeyFile }}key_file = "{{ $v.TLS.KeyFile }}"{{end}}
  {{ if $v.TLS.InsecureSkipVerify }}insecure_skip_verify = true{{end}}
{{end}}
{{end}}
{{end}}

{{range $k, $v := .ExtraRuntimes}}
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes."{{$k}}"]
  runtime_type = "{{$v.RuntimeType}}"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes."{{$k}}".options]
  BinaryName = "{{$v.BinaryName}}"
{{end}}

[plugins."io.containerd.tracing.processor.v1.otlp"]
  endpoint = "host.k3d.internal:4317"
  protocol = "grpc"
  insecure = true

[plugins."io.containerd.internal.v1.tracing"]
  sampling_ratio = 1.0
  service_name = "containerd"

创建完这些文件后,在 `kubetracing` 文件夹中打开终端并运行 `k3d` 来创建集群。在此命令之前,请将 `[CURRENT_PATH]` 占位符替换为 `kubetracing` 文件夹的完整路径。您可以通过在该文件夹的终端中运行 `echo $PWD` 命令来获取它。

k3d cluster create tracingcluster \
  --image=rancher/k3s:v1.27.1-k3s1 \
  --volume '[CURRENT_PATH]/config.toml.tmpl:/var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl@server:*' \
  --volume '[CURRENT_PATH]/config:/etc/kube-tracing@server:*' \
  --k3s-arg '--kube-apiserver-arg=tracing-config-file=/etc/kube-tracing/apiserver-tracing.yaml@server:*' \
  --k3s-arg '--kube-apiserver-arg=feature-gates=APIServerTracing=true@server:*' \
  --k3s-arg '--kubelet-arg=config=/etc/kube-tracing/kubelet-tracing.yaml@server:*'

此命令将创建一个版本为 `v1.27.1` 的 Kubernetes 集群,并在您的机器上设置三个 Docker 容器。如果您现在运行 `kubectl cluster-info` 命令,您将看到以下输出

Kubernetes control plane is running at https://0.0.0.0:60503
CoreDNS is running at https://0.0.0.0:60503/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://0.0.0.0:60503/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy

回到可观测性环境的日志,您应该会看到一些内部 Kubernetes 操作的 span 被发布到 OpenTelemetry Collector,如下所示

Span #90
    Trace ID       : 03a7bf9008d54f02bcd4f14aa5438202
    Parent ID      :
    ID             : d7a10873192f7066
    Name           : KubernetesAPI
    Kind           : SPAN_KIND_SERVER
    Start time     : 2023-05-18 01:51:44.954563708 +0000 UTC
    End time       : 2023-05-18 01:51:44.957555323 +0000 UTC
    Status code    : STATUS_CODE_UNSET
    Status message :
Attributes:
     -> net.transport: STRING(ip_tcp)
     -> net.peer.ip: STRING(127.0.0.1)
     -> net.peer.port: INT(54678)
     -> net.host.ip: STRING(127.0.0.1)
     -> net.host.port: INT(6443)
     -> http.target: STRING(/api/v1/namespaces/kube-system/pods/helm-install-traefik-crd-8w4wd)
     -> http.server_name: STRING(KubernetesAPI)
     -> http.user_agent: STRING(k3s/v1.27.1+k3s1 (linux/amd64) kubernetes/bc5b42c)
     -> http.scheme: STRING(https)
     -> http.host: STRING(127.0.0.1:6443)
     -> http.flavor: STRING(2)
     -> http.method: STRING(GET)
     -> http.wrote_bytes: INT(4724)
     -> http.status_code: INT(200)

测试集群运行时

设置好可观测性环境和 Kubernetes 集群后,您现在可以针对 Kubernetes 触发命令,并在 Jaeger 中查看这些操作的跟踪。

打开浏览器,导航到位于 https://:16686/search 的 Jaeger UI。您会看到 `apiserver`、`containerd` 和 `kubelet` 服务正在发布跟踪。

Jaeger screen with services dropdown open showing apiserver, containerd and kubelet services as options

选择 `apiserver` 并单击**“查找跟踪”**。在这里,您可以看到来自 Kubernetes 控制平面的跟踪。

Jaeger screen showing a list of spans found for apiserver

让我们使用 `kubectl` 运行一个针对 Kubernetes 的示例命令,例如运行一个 echo 命令

$ kubectl run -it --rm --restart=Never --image=alpine echo-command -- echo hi

# Output
# If you don't see a command prompt, try pressing enter.
# warning: couldn't attach to pod/echo-command, falling back to streaming logs: unable to upgrade connection: container echo-command not found in pod echo-command_default
# Hi
# pod "echo-command" deleted

现在,再次打开 Jaeger,选择 `kubelet` 服务,操作 `syncPod`,并添加标签 `k8s.pod=default/echo-command`,您应该能够看到与此 pod 相关的 span。

Jaeger screen showing a list of spans found for the syncPod operation on kubelet service

展开一个跟踪,您将看到创建此 pod 的操作。

Jaeger screen showing a single syncPod expanded

结论

即使处于 beta 阶段,`kubelet` 和 `apiserver` 的跟踪(分别是 kubeletapiserver)都可以帮助开发人员了解 Kubernetes 内部的运行情况并开始调试问题。

这将有助于开发创建自定义任务,例如 Kubernetes Operators,它们更新内部资源以添加更多 Kubernetes 功能。

作为一个专注于在可观测性领域构建开源工具的团队,为整个 OpenTelemetry 社区做出贡献的机会对我们来说很重要。因此,我们一直在研究寻找从核心 Kubernetes 引擎收集跟踪的新方法。鉴于 Kubernetes 当前暴露的可观测性水平,我们希望发布我们的发现,以帮助其他对分布式跟踪在 Kubernetes 引擎中的当前状态感兴趣的人。Daniel Dias 和 Sebastian Choren 正在开发 Tracetest,这是一个开源工具,允许您使用 OpenTelemetry 开发和测试您的分布式系统。它与任何 OTel 兼容系统协同工作,并能够创建基于跟踪的测试。访问 https://github.com/kubeshop/tracetest 查看。

本文使用的 示例源代码设置说明 可在 Tracetest 存储库中找到。

参考

最后修改于 2025 年 5 月 22 日:[chore] Accessible links 4 (#6052) (1609d60e)