可观测性——Metrics、Logging、Tracing 的集群实践
一个运行在 Kubernetes 上的分布式系统,出问题时最难回答的问题是"发生了什么"。单机时代,登录服务器查看进程状态和日志文件,基本能还原现场。容器化之后,Pod 随时可能被调度到不同节点,崩溃后立即重启,原始日志消失,这套方法彻底失效。可观测性(Observability)是对这个问题的系统性回答:通过在系统内部埋点、采集、存储和展示三类信号——Metrics(指标)、Logging(日志)、Tracing(链路追踪)——让工程师在不登录服务器的情况下理解系统行为。
Kubernetes 本身对可观测性有明确的架构分工。采集点(kubelet 内嵌的 cAdvisor、node-exporter DaemonSet、应用 SDK)由各组件负责暴露,聚合层(Prometheus、metrics-server)负责拉取和存储,展示层(Grafana、Jaeger UI)负责查询和可视化。这种"采集点内建、存储和展示留给生态"的设计,让 Kubernetes 本身保持简洁,同时允许不同规模的集群选择不同的存储方案。
理解可观测性的关键区分点在于:kube-state-metrics 和 metrics-server 看起来相似,实际上服务完全不同的场景;DaemonSet 日志采集和 sidecar 日志采集各有适用边界;OpenTelemetry 正在成为三类信号的统一采集标准,但具体的存储和展示仍然分散。
架构全景
1 | |
Metrics:三层采集架构
node-exporter:节点硬件指标
node-exporter 以 DaemonSet 形式运行,在每个节点上暴露 /metrics 端点,采集 CPU、内存、磁盘 I/O、网络接口、文件描述符等操作系统级别的指标。这些指标是排查节点资源瓶颈的基础,例如 node_disk_io_time_seconds_total 可以判断磁盘是否饱和。
cAdvisor:容器资源用量
cAdvisor(Container Advisor)内嵌在 kubelet 进程中,通过 /metrics/cadvisor 端点暴露容器级别的资源用量,包括 container_cpu_usage_seconds_total、container_memory_working_set_bytes 等。这是 Prometheus 抓取容器指标的主要数据源。
kube-state-metrics vs metrics-server:两者的本质区别
这是最常见的混淆点,值得专门澄清:
kube-state-metrics 监听 Kubernetes API 对象的状态,把对象状态转换为 Prometheus 格式的指标。它回答的是"集群里有多少个 Pod 处于 Pending 状态"“这个 Deployment 的期望副本数和就绪副本数各是多少”“这个 Node 的 Ready condition 是否为 True”。这些是离散的状态值,不是连续的资源用量数据。kube-state-metrics 的数据通常持久化到 Prometheus,用于告警和长期趋势分析。
metrics-server 实现了 Kubernetes Metrics API(metrics.k8s.io),提供实时的 CPU 和内存用量数据,数据窗口约 15 秒,不持久化。它的数据源来自 kubelet 的 /metrics/resource 端点(而非 cAdvisor 的完整 metrics)。HPA controller 通过 metrics-server 获取 Pod 的实时 CPU 用量来决定是否扩缩容,kubectl top node 和 kubectl top pod 也依赖 metrics-server。
两者不能互相替代:metrics-server 不存储历史数据,无法做趋势分析;kube-state-metrics 不提供实时资源用量,HPA 无法使用它。
Prometheus Operator:自动化 scrape 配置
Prometheus Operator 通过 ServiceMonitor CRD 让 Prometheus 自动发现监控目标。用户声明一个 ServiceMonitor 对象,描述要监控哪个 namespace 下的哪类 Service(通过 label selector),Prometheus Operator 把它转换为 Prometheus 的 scrape_config,实现监控目标的动态注册。新增一个服务的监控,只需 apply 一个 ServiceMonitor 对象,无需重启 Prometheus 或修改配置文件。
AlertmanagerConfig CRD 同理:用户声明告警规则(PrometheusRule)和路由配置(AlertmanagerConfig),Operator 自动注入到 Alertmanager 配置。
kube-prometheus-stack 是 Prometheus Operator 的生产级打包方案(Helm Chart),一次安装包含:Prometheus Operator、Prometheus、Alertmanager、Grafana、node-exporter DaemonSet、kube-state-metrics、默认告警规则集(涵盖 etcd、API Server、kubelet、节点资源等 200+ 规则)。在新集群上建立基础可观测性,helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack 是最快路径,5-10 分钟即可获得开箱即用的 Grafana Dashboard。
Logging:日志采集架构
容器日志的存储位置
容器的 stdout 和 stderr 由容器运行时(containerd/CRI-O)捕获,写入节点上的 /var/log/containers/ 目录,文件名格式为 <pod-name>_<namespace>_<container-name>-<container-id>.log。kubelet 负责管理这些日志文件的轮转(log rotation)。
kubectl logs <pod> 实际上是 kubelet 读取这些文件并通过 API 返回给客户端,--previous 参数读取上一个容器实例的日志(Pod 重启后当前容器的日志不包含重启前的内容)。
DaemonSet 采集 vs Sidecar 模式
DaemonSet 模式:在每个节点上运行一个 Fluentbit(或 Fluentd)实例,挂载节点的 /var/log/containers/ 目录,采集所有 Pod 的日志,发送到 Loki 或 Elasticsearch。这是最常见的部署模式,资源效率高(每节点一个采集进程),不需要修改应用 Pod 的配置。
Sidecar 模式:在每个需要采集日志的 Pod 中注入一个日志采集 sidecar 容器。适用于应用把日志写入文件(非 stdout)的场景,或需要对不同 Pod 的日志做不同处理的场景。代价是每个 Pod 增加一个 sidecar 容器的资源开销,且 Pod 数量多时总资源消耗显著高于 DaemonSet 模式。
生产环境的主流选择是 DaemonSet 模式,应用统一输出到 stdout/stderr,由 Fluentbit 采集。Sidecar 模式仅在有特殊需求(如遗留应用日志写文件)时使用。
Loki vs Elasticsearch
Loki 的设计理念是"日志的 Prometheus":只对标签(labels)建立索引,不对日志内容全文索引。查询时先用标签过滤(如 {app="nginx", namespace="prod"}),再在过滤结果中做文本搜索(LogQL)。存储成本远低于 Elasticsearch(通常低 10 倍以上),适合日志量大、查询模式以标签过滤为主的场景。
Elasticsearch 对日志内容全文索引,支持复杂的全文搜索和聚合分析,适合需要在日志内容上做复杂查询的场景(如安全审计、合规分析)。但存储成本和运维复杂度都显著高于 Loki。
Tracing:链路追踪
OpenTelemetry:统一采集标准
OpenTelemetry(OTel)是 CNCF 的可观测性标准项目,把 Metrics、Logging、Tracing 的采集 SDK 和协议统一化。核心组件:
OTel SDK:应用侧的埋点库,支持 Java、Go、Python、Node.js 等主流语言,负责生成 trace span 和 metric 数据。
OTel Collector:独立的采集代理,接收来自应用 SDK 的数据(支持多种协议:OTLP、Jaeger、Zipkin),做过滤、采样、批处理后,转发到后端(Jaeger、Tempo、Prometheus)。
在 Kubernetes 上,OTel Collector 通常以 DaemonSet(节点级采集)或 Deployment(集中式采集)部署。通过 Cert-manager 和 OTel Operator,可以实现 Pod 的自动 sidecar 注入,无需修改应用代码即可获取基础的 HTTP 和 gRPC trace 数据。
Jaeger / Tempo:trace 存储
Jaeger 是 CNCF 的链路追踪存储和展示系统,支持 Cassandra、Elasticsearch 作为后端存储。Tempo 是 Grafana Labs 的链路追踪存储,与 Loki 类似,只对 trace ID 索引,通过 Loki 的标签查询找到 trace ID,再用 trace ID 查询 Tempo。Tempo + Loki + Grafana 的组合实现了"从日志跳转到 trace"的关联查询,是当前轻量级可观测性栈的流行选择。
eBPF 可观测性:无侵入式采集
传统的应用层可观测性依赖 SDK 埋点,需要修改应用代码。eBPF(Extended Berkeley Packet Filter)是 Linux 内核的动态可编程机制,允许在不修改内核代码和应用代码的情况下,在内核事件(系统调用、网络包、函数调用)触发时执行自定义逻辑,实现"零侵入"的可观测性采集。
在 Kubernetes 场景中,eBPF 可观测性的典型应用:
网络流量可观测性:Cilium 的 Hubble 组件通过 eBPF 在网络层拦截 Pod 间的所有流量,生成 L4/L7 级别的流量拓扑图和延迟统计,无需在应用中注入任何代码。可以看到"Pod A 的哪个接口调用了 Pod B 的哪个接口,延迟是多少"。
自动 trace 注入:Pixie(CNCF 项目)通过 eBPF 自动采集应用的 HTTP、gRPC、MySQL、Redis 等协议的请求-响应对,生成 trace span,无需应用集成 OTel SDK。适合遗留应用或无法修改源码的场景。
性能剖析(Profiling):Parca、Pyroscope 通过 eBPF 采集 CPU 火焰图,每隔固定时间采样所有进程的调用栈,持续 profiling 让定位 CPU 热点从"复现问题时手动触发"变为"随时可查历史数据"。
eBPF 可观测性的限制:需要 Linux 内核 4.14+(部分功能需要 5.x+),某些云服务商的节点内核版本或安全策略可能限制 eBPF 使用。Windows 节点不支持 eBPF。
告警与 SLO:从指标到响应
Prometheus 告警规则
Prometheus 的告警规则(PrometheusRule CRD)定义了何时触发告警。规则本质是 PromQL 表达式,持续满足条件超过 for 指定时长后,触发告警并发送到 Alertmanager。
常见的集群级别告警:
1 | |
Alertmanager 负责告警的路由、分组和去重:把同一个 Deployment 的多个 Pod 告警合并为一条,按 severity 路由到不同的通知渠道(PagerDuty、Slack、Email)。
SLO:从告警转向服务水平目标
传统的基于阈值的告警(CPU > 80% 持续 5 分钟)有大量误报,工程师对告警产生疲劳。SLO(Service Level Objective)是更成熟的可观测性实践:把业务目标(99.9% 的请求在 200ms 内响应)转化为 error budget(每月允许 43.8 分钟不满足 SLO),只有 error budget 消耗速度过快时才触发紧急告警。
在 Kubernetes 上实现 SLO 监控的常见工具:
- Sloth:根据 SLO 定义自动生成 PrometheusRule
- pyrra:SLO 感知的 Prometheus recording rules 生成器
- Google SLO Generator:遵循 SRE 手册的 SLO 实现
Grafana Dashboard 分层
生产环境的 Grafana dashboard 通常分三层:
集群总览层:节点数量、Pod 调度成功率、API Server 延迟、etcd 响应时间。出现红色时,可以下钻到下一层。
Namespace/服务层:按 namespace 展示 Pod CPU/内存用量、Deployment 就绪状态、HPA 扩缩容历史。定位问题所在的服务。
Pod/容器层:单个 Pod 的 CPU throttling 率(container_cpu_cfs_throttled_seconds_total)、OOMKill 次数、网络 I/O。定位具体原因。
Grafana 的 Exemplar 功能把 Prometheus 的某个具体数据点(如某个高延迟请求的 P99)关联到对应的 trace ID,点击后直接跳转到 Jaeger/Tempo 查看该请求的完整调用链。这是 Metrics → Tracing 关联查询的核心机制。
Prometheus 远程写入与长期存储
Prometheus 本地存储默认保留 15 天数据,对于长期趋势分析(季度容量规划、年度 SLO 报告)不够用。remote_write 配置让 Prometheus 把采集到的 metrics 同步推送到外部存储:
1 | |
常用的远程写入后端:Thanos Receive(接收并持久化到对象存储)、Cortex/Mimir(水平扩展的 Prometheus 兼容存储)、VictoriaMetrics(高压缩率的时序数据库,写入性能优于原生 Prometheus)。远程写入引入网络延迟和反压风险,queue_config 中的 capacity 是内存队列容量,网络中断时缓冲写入请求,超出容量后丢弃最旧数据而不阻塞 Prometheus 本地采集。
采样策略与成本控制
链路追踪最大的成本挑战是数据量:高流量系统每秒产生数万条 trace,全量存储代价极高。OTel Collector 支持多种采样策略:
头部采样(Head-based sampling):在 trace 开始时随机决定是否采样(如 1%),简单但会丢失所有低频异常 trace。
尾部采样(Tail-based sampling):等 trace 完整结束后,基于 trace 的结果(是否有错误、延迟是否超阈值)决定是否保存。需要把同一 trace 的所有 span 路由到同一个 Collector 实例(通过 trace ID 做一致性 hash),工程复杂度较高但质量好。
优先级采样(Priority sampling):错误 trace 100% 保留,慢 trace(P99 以上)高比例保留,正常 trace 低比例随机采样。是实际生产中最常用的折中方案。
API Server 与 etcd 的自身可观测性
Kubernetes 的控制面组件本身也需要被监控。API Server 暴露的关键指标:
apiserver_request_total:按动词(GET/LIST/WATCH/CREATE/UPDATE)和资源类型分类的请求计数,用于分析 API Server 负载来源。apiserver_request_duration_seconds:API 请求延迟分布,P99 超过 1s 通常意味着 etcd 或网络有问题。apiserver_current_inflight_requests:当前进行中的请求数,有 mutating 和 readOnly 两个维度的上限(默认 200/400)。
etcd 的关键指标:
etcd_server_leader_changes_seen_total:leader 切换次数,频繁切换说明 etcd 集群不稳定(通常是网络或磁盘 I/O 问题)。etcd_disk_wal_fsync_duration_seconds:WAL 日志的 fsync 延迟,P99 超过 10ms 说明磁盘 I/O 成为瓶颈,会拖慢整个集群的写操作。etcd_mvcc_db_total_size_in_bytes:etcd 数据库当前大小,接近 quota(默认 2GB)时需要压缩历史版本(etcdctl compact)或扩大 quota。
这些指标由 kube-prometheus-stack Helm chart 自动收集,对应的告警规则也预置在 chart 中(KubeAPIErrorsHigh、etcdHighNumberOfLeaderChanges 等)。
日志结构化与查询效率
结构化日志的重要性
非结构化日志(纯文本)在 Loki 中只能做全文搜索,效率低。结构化日志(JSON 格式)可以把关键字段(request_id、user_id、status_code、latency_ms)作为 Loki 的 label 或 structured metadata,查询时直接过滤,速度快一个数量级。
Java 应用推荐用 logback + logstash-logback-encoder 输出 JSON 日志,Go 应用推荐 zap 或 slog(Go 1.21+ 标准库),Node.js 应用推荐 pino。标准字段名遵循 OpenTelemetry 语义约定(service.name、trace_id、span_id)便于跨工具关联。
Loki 的 label 基数(cardinality)是关键性能约束:label 的不同取值组合数决定了 Loki 需要维护的 stream 数量。pod_name 不适合作为 label(Pod 数量多时 cardinality 极高),应该作为 structured metadata 或在查询时用正则过滤。推荐的 label 集合:namespace、app、container、level(日志级别),其余字段通过 | json 管道在查询时动态解析。
LogQL 常用查询模式
Loki 的 LogQL 语法示例:
1 | |
LogQL 的 | json 管道操作符会自动解析 JSON 日志行,把 JSON 字段提取为可过滤的 label,结合 unwrap 可以做数值聚合(类似 PromQL),把日志系统的分析能力提升到接近 Metrics 的水平。
可运行实验
1 | |
实验结果映射到 K8s 对象
kubectl top node 的数据来自 metrics-server 聚合的 kubelet /metrics/resource 端点,刷新间隔约 15 秒。如果 metrics-server 未安装,HPA 将无法工作(kubectl describe hpa 会显示 unable to fetch metrics)。
curl http://localhost:8080/metrics | grep kube_deployment_status_replicas 的输出展示了 kube-state-metrics 把 Deployment 对象的 .status.replicas、.status.readyReplicas 等字段转换为 Prometheus gauge 指标。这些指标可以用于告警规则,例如"期望副本数和就绪副本数不一致持续 5 分钟"触发 PagerDuty。
Fluentbit 采集到的日志中,Kubernetes metadata(Pod name、namespace、container name、node name)由 Fluentbit 的 kubernetes filter 插件从 kubelet API 查询并附加到日志记录,这让后续在 Loki 中按 namespace 或 Pod 过滤日志成为可能。
多集群可观测性
单集群的可观测性方案相对直接,多集群场景引入了额外复杂度:每个集群的 Prometheus 独立运行,如何聚合查询?
Thanos 和 Cortex 是解决多集群 Metrics 聚合的两条主流路径。Thanos 在每个集群的 Prometheus 旁部署 Sidecar,把数据上传到对象存储(S3/GCS/OSS),由中央 Thanos Query 组件跨集群联邦查询。Cortex 则把 Prometheus 改造为水平可扩展的远程写入模式,所有集群的 metrics 推送到中央 Cortex,统一存储和查询。两者都支持全局视图(cross-cluster PromQL),区别在于 Thanos 保留本地 Prometheus 完整能力,Cortex 架构更集中但运维复杂度更高。
日志多集群聚合较简单:Fluentbit 的 output 直接指向中央 Loki 或 Elasticsearch 集群,加上 cluster label 区分来源即可。Trace 数据同理,OTel Collector 把 trace 发往中央 Jaeger/Tempo,按 cluster resource attribute 做过滤。
实践中,小规模多集群(3-5 个)通常用 Grafana 的多数据源功能(在同一个 Dashboard 里配置多个 Prometheus 数据源),免去 Thanos/Cortex 的运维成本。规模超过 10 个集群后,统一存储层带来的查询效率和运维简化才开始值回部署成本。
模式提炼
可观测性是"白盒 + 标准接口":系统主动暴露内部状态(白盒),通过标准化的接口(Prometheus 格式、OTel 协议)让任何兼容工具采集。Kubernetes 把采集点内建到核心组件(kubelet、API Server 都暴露 /metrics),把存储和展示留给生态选择。
三类信号的关联查询是可观测性成熟度的标志:从 Grafana dashboard 看到某时刻 P99 延迟飙升(Metrics),跳转到该时间段的相关 Pod 日志(Logging),再通过 trace ID 查看具体请求的调用链(Tracing),这条路径在 Grafana + Loki + Tempo 的组合下可以全程不离开同一个界面。
工程迁移表
| 传统模式 | Kubernetes 等价 | 关键差异 |
|---|---|---|
| Spring Actuator /metrics | Prometheus SDK + /metrics endpoint | 拉取模型(Prometheus pull)vs 推送 |
| JVM JMX | cAdvisor + Prometheus JMX exporter | 容器感知,无需手动配置端口 |
| ELK Stack (Filebeat+ES+Kibana) | Fluentbit + ES + Kibana 或 Loki + Grafana | Loki 成本更低,ES 查询更强 |
| Zipkin | Jaeger / Tempo + OTel SDK | OTel 统一了多语言 SDK |
| Nagios / Zabbix | Prometheus + Alertmanager | 声明式告警规则,云原生集成 |
常见误解
误解一:kube-state-metrics 和 metrics-server 功能相同
两者的数据来源和用途完全不同。kube-state-metrics 监听 Kubernetes API 对象状态,回答"集群对象处于什么状态",数据持久化到 Prometheus,支持长期趋势分析。metrics-server 提供实时 CPU/内存用量,服务于 HPA 和 kubectl top,数据窗口短,不持久化。可以同时部署两者,它们不冲突。
误解二:kubectl logs 适合生产日志分析
kubectl logs 只能查看单个 Pod 的当前实例日志,无法跨 Pod 搜索,无法查看已删除 Pod 的历史日志,也无法做聚合分析。生产环境日志分析需要集中式日志系统(Loki/Elasticsearch):Fluentbit 把所有 Pod 的日志采集到中央存储,保留足够长的历史(通常 7-30 天),支持跨 Pod、跨 namespace 的搜索和过滤。kubectl logs 是开发调试工具,不是生产分析工具。
误解三:sidecar 日志采集比 DaemonSet 更推荐
sidecar 日志采集适合特定场景(应用写文件日志、需要对不同 Pod 做不同处理),不是通用推荐。DaemonSet 模式资源效率更高(每节点一个采集进程 vs 每 Pod 一个 sidecar),配置更简单,是生产环境的主流选择。选择 sidecar 模式需要有明确的理由,而不是默认选项。
练习
-
在本地集群安装 metrics-server(
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml),验证kubectl top node和kubectl top pod可以正常工作,然后创建一个 HPA(kubectl autoscale deployment myapp --cpu-percent=50 --min=1 --max=5),用压测工具(hey 或 ab)打流量,观察 HPA 扩容过程中kubectl get hpa -w的 TARGETS 字段变化。 -
部署 kube-state-metrics,端口转发后用 curl 查看指标,找出以下三类信息:当前集群中处于 Pending 状态的 Pod 数量(
kube_pod_status_phase)、某个 Deployment 的期望副本数和就绪副本数(kube_deployment_status_replicasvskube_deployment_status_replicas_ready)、某个 Node 的 Ready condition(kube_node_status_condition)。 -
用 Helm 安装 Loki Stack(
helm install loki grafana/loki-stack --set grafana.enabled=true),在 Grafana 中配置 Loki 数据源,用 LogQL 查询某个 namespace 的所有 Pod 日志({namespace="default"}),然后过滤包含"error"的日志行({namespace="default"} |= "error")。对比kubectl logs和 Loki 查询的体验差异。
系列导航
- 00 核心概念导读
- 01 API Server 与声明式 API
- 02 etcd 与持久化
- 03 控制器模式与 Informer 机制
- 04 Pod 生命周期
- 05 调度器深入
- 06 kubelet 与容器运行时
- 07 网络模型与 CNI
- 08 Service 与 kube-proxy
- 09 Ingress 与 Gateway API
- 10 存储体系
- 11 配置与密钥管理
- 12 RBAC 与安全模型
- 13 资源管理与自动伸缩
- 14 CRD 与 Operator 模式
- 15 Helm 与应用打包
- 16 可观测性(本篇)
- 17 生产集群运维
