Skip to content

Observability in Kubernetes: From Metrics to Meaning

“Kubernetes 让基础设施更可编程、更弹性、更可扩展——但也让生产系统的‘可理解性’(reasoning ability)断崖式下降。一个 HTTP 请求可能穿越 Ingress、Service、Deployment、StatefulSet、vLLM 推理 Pod、Redis Cluster、Kafka Topic 和 CSI 驱动的 PVC;而当它失败时,你看到的往往只是一行 503 Service Unavailable —— 这不是告警,这是谜题。”

背景动机:为什么传统监控在 K8s 里失效了?

过去十年,我们习惯用“监控”(monitoring)思维管理单体应用:固定 IP、静态进程、明确调用链、小时级变更窗口。Prometheus + Grafana 的黄金指标(CPU/内存/HTTP 5xx)曾是运维铁三角。

但 Kubernetes 彻底重构了运行时契约:

  • Workload mobility:Pod 可被驱逐、抢占、自动漂移,kubectl get pods -o wide 输出的 NODE 列每分钟都可能变化;
  • Replica churn:HorizontalPodAutoscaler(HPA)和 Cluster Autoscaler 协同下,一个 Deployment 的副本数可在 2→17→5→0 间高频震荡,传统基于“实例稳定”的指标聚合(如 avg by (instance))失去语义;
  • Dependency explosion:现代 AI 应用栈典型拓扑是 Ingress → Istio Gateway → vLLM Service → vLLM StatefulSet (GPU) → Redis Cluster → MinIO Bucket → PostgreSQL Primary + 3 Read Replicas —— 共 8 层异构组件,任意一层的延迟毛刺都可能被放大为端到端 P99 崩溃;
  • 语义鸿沟container_cpu_usage_seconds_total{pod=~"vllm.*"} 是数字,但 vllm_request_queue_time_seconds{model="qwen2-7b", stage="prefill"} 才是业务语言。

CNCF 这篇 2026 年的新文直指核心:Observability 不是监控的升级版,而是诊断范式的迁移——从“发生了什么”(what happened)转向“为什么发生”(why it happened),再跃迁至“如何防止重演”(how to prevent recurrence)。

这要求三件事同时成立:

  1. 信号完备性:Metrics(指标)、Logs(日志)、Traces(链路)、Profiles(性能剖析)、Events(事件)五维数据必须统一采集、关联、存储;
  2. 上下文可携带性:Span ID 必须透传至容器日志、Prometheus Label、K8s Event 注解;
  3. 领域建模能力:不能只画 CPU 火焰图,而要构建 vLLM Pod → GPU Memory Pressure → NCCL Timeout → KV Cache Eviction → Request Queue Backlog 的因果图谱。

否则,你拥有的不是可观测性,只是“高分辨率盲区”。

核心技术:构建 K8s 原生可观测性栈(附生产级 YAML)

1. OpenTelemetry Collector:唯一可信数据入口

拒绝在每个 Pod 中注入 Jaeger Agent 或 Fluentd Sidecar。采用 DaemonSet + Gateway 模式统一收口:

yaml
# otel-collector-gateway.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otel-collector
  namespace: observability
spec:
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
      annotations:
        # 关键:强制注入 trace context 到所有容器环境变量
        prometheus.io/scrape: "true"
        prometheus.io/port: "8888"
    spec:
      serviceAccountName: otel-collector
      containers:
      - name: otelcol
        image: otel/opentelemetry-collector-contrib:0.112.0
        args: [ "--config=/etc/otel-collector-config.yaml" ]
        ports:
        - containerPort: 4317  # OTLP gRPC
        - containerPort: 4318  # OTLP HTTP
        - containerPort: 8888  # Prometheus metrics endpoint
        volumeMounts:
        - name: config
          mountPath: /etc/otel-collector-config.yaml
          subPath: otel-config.yaml
      volumes:
      - name: config
        configMap:
          name: otel-collector-config
---
# configMap: otel-collector-config
apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-collector-config
  namespace: observability
data:
  otel-config.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
          http:
      prometheus:
        config:
          scrape_configs:
          - job_name: 'kubernetes-pods'
            kubernetes_sd_configs: [{ role: pod }]
            relabel_configs:
            - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
              action: keep
              regex: true
            - source_labels: [__meta_kubernetes_pod_phase]
              action: keep
              regex: Running
            - source_labels: [__meta_kubernetes_pod_annotation_otel_context]
              action: labelmap
              regex: otel_context_(.+)
    processors:
      batch:
      memory_limiter:
        limit_mib: 1024
        spike_limit_mib: 512
      resource:
        attributes:
        - key: k8s.pod.name
          from_attribute: k8s.pod.name
        - key: k8s.namespace.name
          from_attribute: k8s.namespace.name
        - key: service.name
          from_attribute: k8s.pod.name
          action: insert
    exporters:
      otlp:
        endpoint: "tempo:4317"
      prometheusremotewrite:
        endpoint: "https://prometheus-remote/api/v1/write"
        headers:
          Authorization: "Bearer ${PROM_REMOTE_TOKEN}"
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [batch, resource]
          exporters: [otlp]
        metrics:
          receivers: [otlp, prometheus]
          processors: [memory_limiter, batch]
          exporters: [prometheusremotewrite]

✅ 关键设计点:

  • relabel_configsotel_context_(.+) 提取 Pod Annotation(如 otel_context_model=qwen2-7b)作为 Span 标签,实现业务维度下钻;
  • resource.processor 将 K8s 元数据注入所有 trace/metric,消除“指标孤岛”;
  • memory_limiter 防止 Collector OOM——这是生产集群中 73% 的 Collector 故障主因(据 2026 年 CNCF Survey)。

2. eBPF 增强:填补 Kernel 层盲区

Prometheus 抓不到 socket connect timeout 的根本原因?用 eBPF 补全:

bash
# 使用 Pixie 自动注入(无需修改应用)
px deploy --cluster-name prod-us-east --with-ebpf
# 查看 vLLM Pod 的 TCP 重传率(非容器网络栈,是真实 NIC 行为)
px run px/tcp_retransmits --filter 'pod_name =~ "vllm.*"'

或直接部署 Cilium 的 Hubble:

yaml
# hubble-relay.yaml(启用流量拓扑 + 异常检测)
apiVersion: cilium.io/v2alpha1
kind: HubblePeer
metadata:
  name: hubble-relay
  namespace: kube-system
spec:
  peerAddress: "hubble-relay:4244"
---
# 启用异常规则:检测 >50ms 的跨节点 Pod 间延迟
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: detect-cross-node-latency
spec:
  policy:
  - endpointSelector:
      matchLabels:
        io.cilium.k8s.policy.serviceaccount: vllm-sa
    ingress:
    - fromEndpoints:
      - matchLabels:
          k8s:io.kubernetes.pod.namespace: default
      toPorts:
      - ports:
        - port: "8000"
          protocol: TCP
        rules:
          http:
          - method: "POST"
            path: "/generate"
    - fromEntities:
      - cluster
    - fromCIDRSet:
      - cidr: "10.244.0.0/16"
  status:
    - rule:
        - tcp:
            minRTT: 50000000  # 50ms in nanoseconds
            action: "alert"

3. Logs → Structured Tracing:用 LogQL 实现日志即 Trace

放弃 grep "timeout"。在 Loki 中定义结构化日志提取规则:

yaml
# loki-config.yaml(LogQL pipeline)
pipeline_stages:
- docker: {}
- json:
    expressions:
      trace_id: "trace_id"
      span_id: "span_id"
      model: "model"
      request_id: "request_id"
- labels:
    - trace_id
    - model
    - request_id
- drop:
    expression: "duration_ms < 100"  # 过滤健康请求,专注慢请求

查询语句示例(Grafana Explore):

{job="vllm-logs"} | json | duration_ms > 5000 
| line_format "{{.request_id}} {{.model}} {{.duration_ms}}"
| __error__ = ""
| traceID = "{{.trace_id}}"

→ 点击 traceID 直接跳转 Tempo 查看完整调用链。

运维建议:SRE 团队落地 checklist

阶段关键动作风险提示
Day 0在所有命名空间注入 otel-context Annotation:
kubectl annotate ns default otel_context_team=ai-infra
❌ 避免全局 * 标签,会导致 Prometheus label cardinality 爆炸(>10⁵ unique values)
Day 7对 GPU Pod 强制启用 nvidia-smi metrics 导出:
nvidia-smi -q -d MEMORY,UTILIZATION,POWER,CLOCK -x -l 5 | xmllint --xpath '//gpu' - | ...
⚠️ nvidia-smi 每次调用耗时 ~120ms,需用 node_exporter textfile collector 缓存,避免压垮 GPU
Day 30建立 vLLM SLO Dashboard
- vllm_request_duration_seconds_bucket{le="2.0", model="qwen2-7b"}
- vllm_gpu_memory_used_bytes / vllm_gpu_memory_total_bytes
- vllm_kv_cache_evict_count_total
🚫 拒绝“平均值陷阱”:P99 延迟 2s ≠ 平均延迟 200ms;必须按 model+batch_size+seq_len 多维分桶
Day 90实施 Auto-Root-Cause:用 PromQL 检测因果链
count by (pod) (rate(container_cpu_usage_seconds_total{job="kubelet", image!="", container!=""}[5m]) > 0.8)
and on (pod)
count by (pod) (rate(nvidia_smi_utilization_gpu_ratio{mode="utilization.gpu"}[5m]) > 0.95)
💡 真正的 SLO 工程不是设阈值,而是定义 if GPU utilization > 95% AND queue length > 200 THEN trigger vLLM autoscaling

延伸阅读

  • 🔗 CNCF Observability Whitepaper v2.1:首次明确定义 “K8s-native Observability” 的 5 层模型(Infrastructure → Platform → Workload → Application → Business);
  • 📘 《Kubernetes Observability Engineering》(O’Reilly, 2026):第 7 章详解 vLLM 在多租户场景下的 trace propagation 策略,含 NVIDIA Triton 与 vLLM 的对比 benchmark;
  • 🛠 Pixie + Tempo + Grafana 一键部署脚本:已适配 K3s/K8s 1.28+,内置 vLLM 专用 dashboard 和 SLO alert rules;
  • 🧪 实验建议:用 hey -z 5m -q 100 -c 50 http://vllm-service:8000/generate 注入可控负载,观察 vllm_request_queue_lengthcontainer_memory_working_set_bytes 的相关性——你会发现,当 GPU 显存使用率 >85%,队列长度呈指数增长,而非线性,这是典型的 NUMA 绑定失效信号。

可观测性不是堆砌工具,而是建立一种“系统心智模型”的能力。当你能从 503 看见 NCCL timeout,从 CPU idle 看见 GPU kernel launch stall,从 log timestamp skew 看见 etcd raft log lag——你才真正穿过了 Kubernetes 的抽象迷雾。那之后,运维不再是救火,而是编排确定性。