使用 Runtime Observability 创建 Kubernetes 集群
本文得到了 Sebastian Choren、Adnan Rahić 和 Ken Hamric 的贡献。
Kubernetes 是一个广泛用于云原生领域的开源系统,用于在云中部署和扩展容器化应用。它可以观察日志和指标的能力是众所周知且有文档记录的,但它对于应用程序跟踪的可观察性是新的。
以下是 Kubernetes 生态系统中的一些最新活动摘要:
- 第一次讨论始于 2018 年 12 月,首个用于实现仪表化的 PR 可在此处找到。
- 于 2020 年 1 月创建了一个 KEP(Kubernetes 增强提案),并后来将其范围限定在 API Server(KEP 647 - API Server Tracing)中。2021 年 7 月,还提出了一个用于 Kubelet 的新 KEP(KEP 2831 Kubelet Tracing)。
- etcd(Kubernetes 用作内部数据存储)于 2020 年 11 月开始讨论跟踪功能(此处),并于 2021 年 5 月合并了首个版本。
- 用于 Kubernetes 的两个容器运行时接口 containerd 和 CRI-O 于 2021 年开始实现跟踪功能(CRI-O 于 2021 年 4 月,containerd 于 2021 年 8 月)。
- API Server 跟踪在 v1.22 中以 α 版发布(可在此处查看)(2021 年 8 月),并在 v1.27 中以 β 版发布(可在此处查看)(2023 年 4 月)。
- Kubelet 跟踪在 v1.25 中以 α 版发布(可在此处查看)(2022 年 8 月),并在 v1.27 中以 β 版发布(可在此处查看)(2023 年 4 月)。
在调查使用 Kubernetes 进行跟踪的当前状态时,我们发现很少有文章文档记录了如何启用它,如这篇关于 kubelet
可观察性的 Kubernetes 博客文章。因此,我们决定记录我们的发现,并提供逐步说明,以便在本地设置 Kubernetes 并检查跟踪数据。
您将学习如何使用 Kubernetes 中的这些仪表化特性来观察 API(kube-apiserver)、节点代理(kubelet)和容器运行时(containerd),并通过在本地设置可观察性环境和稍后进行本地安装来启用跟踪功能。
首先,在本地计算机上安装以下工具:
- Docker:用于在容器环境中运行容器化环境的工具
- k3d:用于在 Docker 中运行 k3s(轻量级 Kubernetes 分发版)的封装工具
- kubectl:用于与集群交互的 Kubernetes 命令行工具
设置可观察跟踪的 Observability Stack
要设置可观察性堆栈,您将运行 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}}
{{- 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
文件夹中打开一个终端,并运行以下命令来创建一个集群。在运行此命令之前,请将命令中的 [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 控制平面运行在 https://0.0.0.0:60503
CoreDNS 运行在 https://0.0.0.0:60503/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server 运行在 https://0.0.0.0:60503/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy
返回到可观察性环境的日志,您应该看到一些内部 Kubernetes 操作产生的跟踪片段被发布到 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中看到这些操作的跟踪。
打开浏览器,导航至Jaeger UI的地址:http://localhost:16686/search。您将看到apiserver
、containerd
和kubelet
服务发布的跟踪:
选择apiserver
,然后点击**“Find Traces”**。这里您将看到来自Kubernetes控制平面的跟踪:
让我们使用kubectl
对Kubernetes运行一个示例命令,比如运行一个echo命令:
$ kubectl run -it --rm --restart=Never --image=alpine echo-command -- echo hi
# 输出
# 如果您看不到命令提示符,请尝试按下回车键。
# 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相关的跟踪:
展开一个跟踪,您将看到创建此Pod的操作:
结论
即使在测试版中,为了帮助开发人员了解Kubernetes内部发生的情况并开始调试问题,kubelet和apiserver的跟踪都能起到一定的作用。
这对于创建自定义任务的开发人员(例如Kubernetes Operators用于更新内部资源并为Kubernetes添加更多功能)将会非常有帮助。
作为专注于在可观察性领域构建开源工具的团队,帮助整个OpenTelemetry社区的机会对我们来说非常重要。这就是为什么我们在研究从核心Kubernetes引擎中收集跟踪的新方法。通过Kubernetes目前暴露的观测水平,我们希望发布我们的研究结果,以帮助其他对Kubernetes引擎中的分布式跟踪感兴趣的人。
Daniel Dias和Sebastian Choren正在开发Tracetest,这是一个开源工具,允许您使用OpenTelemetry开发和测试分布式系统。它与任何符合OTel标准的系统兼容,并支持基于跟踪的测试。您可以在https://github.com/kubeshop/tracetest上查看它。
本文中使用的示例源码和设置说明可在Tracetest存储库中找到。