Files
Qiniu/technical/k8s/k8s-08-observability.md
T

24 KiB
Raw Blame History

tags, create time
tags create time
k8s
observability
logging
monitoring
devops
2026-07-07 15:48

Kubernetes 可观测性 — 日志、监控与健康检查

概述

可观测性(Observability)是理解系统内部状态的能力,在 Kubernetes 中通常由三大支柱构成:Logging(日志)、Metrics(指标) 和 Tracing(链路追踪)。日志告诉你「发生了什么」,指标告诉你「系统状态如何」,追踪告诉你「请求经过了哪里」。三者相互补充,缺一不可。在 K8s 环境中,由于 Pod 生命周期短暂、服务动态伸缩,传统的 SSH 进机器查日志方式不再适用,必须依赖系统化的可观测性方案。本文将从健康检查探针入手,逐步覆盖日志采集、指标监控和事件管理的完整实践。


健康检查(Probe)深入

Kubernetes 通过探针(Probe)机制持续监测容器的健康状态,自动完成故障隔离与自愈。这是 K8s 可观测性的第一道防线——不需要外部工具,原生能力就能实现基本的服务可用性保障。

三种探针详解

K8s 提供三种探针,各有分工:

探针 作用 失败后果 典型场景
livenessProbe 检测容器是否存活 杀死容器并重启(遵循 restartPolicy) 应用死锁、无限循环
readinessProbe 检测容器是否就绪接收流量 从 Service 的 Endpoints 中摘除 依赖未就绪、预热加载中
startupProbe 检测容器是否完成启动 杀死容器并重启 慢启动应用(如 Java、大型模型加载)

配置参数说明

所有探针共享以下参数:

参数 类型 默认值 说明
initialDelaySeconds int 0 容器启动后多久开始第一次探测
periodSeconds int 10 探测间隔(秒)
timeoutSeconds int 1 探测超时时间(秒)
failureThreshold int 3 连续失败多少次后判定为不健康
successThreshold int 1 连续成功多少次后恢复(liveness 固定为 1)

[!tip] startupProbe 与 livenessProbe 的协作 当 startupProbe 配置后,livenessProbe 和 readinessProbe 会在 startupProbe 成功之后才生效。这意味着你可以用一个宽松的 startupProbe(比如 failureThreshold: 30、periodSeconds: 10,即允许最多 300 秒启动时间),而 livenessProbe 保持较严格的配置,避免慢启动应用被误杀。

探测方式

K8s 支持四种探测机制,选择哪种取决于应用特性和暴露的端口。

exec — 执行命令探测

适用于:自定义健康检查逻辑、需要检查内部状态的场景(如数据库连接池)。

livenessProbe:
  exec:
    command:
      - /bin/sh
      - -c
      - "pg_isready -U postgres"
  initialDelaySeconds: 15
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

容器内执行指定命令,退出码为 0 表示健康,非 0 表示不健康。适用于需要检查进程内部状态(如自定义脚本检测连接池)的场景。

httpGet — HTTP 请求探测

适用于:Web 服务、API 服务,最常用的探测方式。

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
    httpHeaders:
      - name: X-Custom-Header
        value: "k8s-probe"
  initialDelaySeconds: 10
  periodSeconds: 15
  timeoutSeconds: 3
  failureThreshold: 3

向容器的指定端口和路径发起 HTTP GET 请求,2xx/3xx 状态码视为健康。建议为健康检查设计轻量级的专用端点,避免在 / 上做复杂业务校验。

tcpSocket — TCP 端口探测

适用于:非 HTTP 协议的 TCP 服务(如数据库、Redis、gRPC 服务端口)。

readinessProbe:
  tcpSocket:
    port: 3306
  initialDelaySeconds: 5
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3

尝试建立 TCP 连接,连接成功即视为健康。适合 MySQL、Redis 等数据库服务,或尚不支持 HTTP 健康检查端点的遗留服务。

gRPC — gRPC 健康检查探测

适用于:gRPC 微服务(K8s 1.24+ GA)。

readinessProbe:
  grpc:
    port: 50051
    # 可选:指定服务名
    # service: "my-grpc-service"
  initialDelaySeconds: 10
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

利用 gRPC 标准的 Health Checking Protocol 进行探测,比 httpGet 更自然地适配 gRPC 服务。需要应用实现 grpc.health.v1.Health 服务。

探针设计原则

[!tip] 原则一:慢启动应用必须用 startupProbe Java 应用、机器学习模型加载、大型缓存预热等场景,启动时间可能超过 livenessProbe 的 initialDelaySeconds。此时应该用 startupProbe 兜底,而不是无限增大 initialDelaySeconds——后者会导致所有重启都白白等待固定时长。

# 典型的慢启动应用探针配置
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30      # 允许最多 30 次失败
  periodSeconds: 10          # 每 10 秒检查一次
  # 总共允许 30 × 10 = 300 秒启动时间

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 15
  timeoutSeconds: 3
  failureThreshold: 3        # 启动后 3 次失败即重启

[!tip] 原则二:readiness ≠ liveness,职责不能混

  • readinessProbe:「我准备好接收请求了吗?」——可以是暂时性的(如依赖 Redis 还没连上),摘除流量后恢复即可重新加入。
  • livenessProbe:「我还活着吗?」——失败意味着进程级故障(死锁、崩溃),需要重启。
  • 常见错误:把依赖检查(如数据库连接)放在 livenessProbe 里。数据库短暂不可达会导致 Pod 被重启,反而加重问题。

[!tip] 原则三:健康端点要轻量 livenessProbe 的 /healthz 端点不应该检查下游依赖(数据库、Redis 等),否则下游抖动会导致 Pod 被误杀。readinessProbe 可以做适度的依赖检查,但也要控制超时。

[!warning] 避免探针与应用抢占资源 exec 探针会 fork 子进程,在资源受限的容器中可能加剧 OOM。高并发场景下优先使用 httpGet 或 tcpSocket。

探针决策流程图

flowchart TD
    A[应用需要健康检查] --> B{启动时间 > 30 秒?}
    B -->|是| C[配置 startupProbe]
    B -->|否| D{需要检查依赖?}
    C --> D
    D -->|是, 如 DB/Redis| E[readinessProbe 检查依赖]
    D -->|否| F[readinessProbe 基础端口/路径]
    E --> G{livenessProbe 如何配置?}
    F --> G
    G --> H[仅检查进程存活<br>不检查下游依赖]
    H --> I{协议类型?}
    I -->|HTTP| J[httpGet /healthz]
    I -->|gRPC| K[grpc health check]
    I -->|TCP/数据库| L[tcpSocket port]
    I -->|自定义逻辑| M[exec command]

日志管理

K8s 日志架构

在 Kubernetes 中,日志的生命周期跨越三个层级:容器层、节点层、集群层。理解这个链路是做好日志管理的前提。

flowchart LR
    subgraph 容器层
        A[应用进程] -->|stdout / stderr| B[容器运行时<br>CRI]
    end

    subgraph 节点层
        B -->|写入文件| C["/var/log/pods/"]
        C --> D[kubelet 日志轮转]
    end

    subgraph 集群层
        D --> E[日志采集 Agent<br>Fluentd / Promtail / Filebeat]
        E --> F[存储后端<br>Elasticsearch / Loki / S3]
        F --> G[查询 UI<br>Kibana / Grafana]
    end

    A -.->|Sidecar 模式| H[日志 Sidecar 容器]
    H --> F

K8s 推荐的最佳实践是:应用将日志输出到 stdout/stderr,由 kubelet 负责写入节点磁盘,再由集群级采集工具统一收集。这样做解耦了应用和日志基础设施,应用无需关心日志的存储和传输。

节点级日志

目录结构

kubelet 将每个容器的 stdout/stderr 写入节点上的日志文件,路径格式为:

/var/log/pods/<namespace>_<pod-name>_<pod-uid>/<container-name>/<restart-count>.log

实际示例:

/var/log/pods/default_nginx-7d8b49557c-abc123_5d1f3a2e-1234-5678-abcd-ef0123456789/
├── nginx/
│   ├── 0.log        # 第 0 次启动的日志
│   └── 1.log        # 第 1 次重启后的日志
└── nginx/
    └── 0.log -> /var/log/containers/nginx-xxx.log  # 符号链接

/var/log/containers/ 目录下的文件是指向 /var/log/pods/ 的符号链接,文件名包含容器名和容器 ID,方便按容器名查找。

日志轮转(Log Rotation)

kubelet 通过以下参数控制日志轮转(配置在 kubelet 的 --container-log-* 参数中):

参数 说明 默认值
--container-log-max-size 单个日志文件最大大小 10Mi
--container-log-max-files 每个容器保留的日志文件数 5

在 KubeletConfiguration 中配置:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
containerLogMaxSize: "50Mi"
containerLogMaxFiles: 10

[!warning] 节点磁盘空间 日志量大的应用(如高并发网关)可能快速占满磁盘。务必设置合理的轮转策略,并配合监控节点磁盘使用率(node_filesystem_avail_bytes 指标)。

集群级方案

方案对比

维度 EFK(Elasticsearch + Fluentd + Kibana) Loki + Promtail + Grafana
存储模型 全文索引,存储成本高 标签索引 + 压缩块存储,成本低
查询语言 KQL / Lucene LogQL(类 PromQL 语法)
全文搜索 原生支持,性能好 不支持原生全文搜索,需配合标签
资源消耗 高(JVM 内存、磁盘 IO) 低(Go 二进制,无全文索引)
与 Metrics 集成 需额外对接 Prometheus 天然与 Prometheus/Grafana 生态融合
适用场景 大规模日志分析、全文检索需求强 资源敏感、与 Prometheus 混合部署
社区趋势 成熟但逐渐被替代 CNCF 毕业项目,增长迅速

EFK 架构

flowchart LR
    A[Pod stdout/stderr] --> B[节点日志文件]
    B --> C[Fluentd DaemonSet]
    C --> D[Elasticsearch]
    D --> E[Kibana]
    E --> F[用户查询]

Fluentd 以 DaemonSet 形式部署在每个节点上,tail 节点日志文件并推送到 Elasticsearch。

Loki + Promtail 架构

flowchart LR
    A[Pod stdout/stderr] --> B[节点日志文件]
    B --> C[Promtail DaemonSet]
    C --> D[Loki]
    D --> E[Grafana]
    E --> F[用户查询]

Promtail 同样以 DaemonSet 部署,但它只提取标签(namespace、pod、container)和日志行,发送给 Loki 进行存储。Loki 的核心设计理念是「like Prometheus, but for logs」——只对标签建索引,日志内容以压缩块存储。

结构化日志

非结构化日志(纯文本)在大规模集群中难以高效查询。结构化日志以 JSON 格式输出,便于日志系统自动解析和过滤。

JSON 日志输出示例(Go / zap)

import "go.uber.org/zap"

logger, _ := zap.NewProduction()
defer logger.Sync()

logger.Info("request processed",
    zap.String("method", "GET"),
    zap.String("path", "/api/v1/users"),
    zap.Int("status", 200),
    zap.Duration("latency", 150*time.Millisecond),
    zap.String("request_id", "abc-123"),
)

输出:

{"level":"info","ts":1720340880.123,"msg":"request processed","method":"GET","path":"/api/v1/users","status":200,"latency":0.15,"request_id":"abc-123"}

结构化日志最佳实践

  1. 统一字段命名:团队内约定日志字段名(如 request_id 而非 reqId / rid),便于跨服务聚合。
  2. 必须包含的字段:时间戳(ts)、日志级别(level)、消息(msg)、请求 ID(request_id)、服务名(service)。
  3. 避免在日志中输出敏感信息:密码、Token、身份证号等必须脱敏。
  4. 日志级别控制:生产环境默认 info,可通过环境变量动态调整为 debug 以便排查问题。

[!tip] kubectl 查看结构化日志 使用 kubectl logs 结合 jq 可以快速过滤 JSON 日志:

kubectl logs my-pod -c my-container | jq 'select(.level == "error")'
kubectl logs my-pod -c my-container | jq 'select(.latency > 0.5)'

指标监控

Metrics Server

Metrics Server 是 Kubernetes 内置的轻量级指标采集组件,提供 CPU 和内存使用数据,是 kubectl top 和 HPA(基于资源指标)的基础设施。

工作原理

flowchart LR
    A[kubelet] -->|Summary API| B[Metrics Server]
    B -->|Metrics API| C[kubectl top]
    B -->|Metrics API| D[HPA Controller]
  • 每个 kubelet 暴露 Summary API(/stats/summary),包含该节点上所有 Pod 的 CPU/内存使用数据。
  • Metrics Server 定期(默认 15 秒)从所有 kubelet 拉取数据,聚合后通过 Metrics API(metrics.k8s.io)暴露。

安装

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

验证:

kubectl top nodes
kubectl top pods -n default

局限性

  • 只提供 CPU 和内存指标,不支持磁盘、网络等。
  • 数据只保留短期窗口(约 1 分钟),不支持历史查询。
  • 不支持自定义指标——需要 Prometheus 等外部系统。

[!info] Metrics Server vs Prometheus Metrics Server 是 K8s 内置的「最小可用」指标方案,适合 kubectl top 和基于资源的 HPA。如果你需要更丰富的指标、历史数据和告警,必须部署 Prometheus。

Prometheus + Grafana

Prometheus 是 CNCF 毕业项目,是 Kubernetes 生态中最主流的监控方案。Grafana 作为可视化层,提供丰富的仪表盘。

架构概览

flowchart LR
    subgraph K8s 集群
        A[应用 /export_metrics] -->|pull| B[Prometheus Server]
        C[kube-state-metrics] -->|pull| B
        D[Node Exporter] -->|pull| B
        E[ServiceMonitor CRD] -.->|配置发现| B
    end

    B --> F[Grafana]
    B --> G[Alertmanager]
    G --> H[邮件 / Slack / 钉钉]
    B --> I[长期存储<br>Thanos / VictoriaMetrics]

核心组件:

  • Prometheus Server:时序数据库 + 采集引擎,定期从 targets 拉取(pull)指标。
  • kube-state-metrics:将 K8s 对象状态(Pod、Deployment、Node 等)转换为 Prometheus 指标。
  • Node Exporter:采集节点级系统指标(CPU、内存、磁盘、网络)。
  • ServiceMonitor CRD:由 Prometheus Operator 提供,以声明式方式配置采集目标。
  • Alertmanager:告警路由、去重、分组、静默。

ServiceMonitor 示例

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-app
  namespace: monitoring
  labels:
    release: prometheus    # 匹配 Prometheus 实例的标签选择器
spec:
  namespaceSelector:
    matchNames:
      - default
  selector:
    matchLabels:
      app: my-app          # 匹配目标 Service 的标签
  endpoints:
    - port: metrics        # Service 中定义的端口名
      path: /metrics
      interval: 15s

[!info] ServiceMonitor 的工作方式 Prometheus Operator 会 watch ServiceMonitor 资源的变化,自动更新 Prometheus 的采集配置。运维人员不再需要手写 prometheus.yml 的 scrape 配置,实现了采集配置的 GitOps 管理。

常见 K8s 监控指标

指标名称 含义 告警建议
container_cpu_usage_seconds_total 容器累计 CPU 使用时间 持续 > 80% requests 告警
container_memory_working_set_bytes 容器内存工作集 接近 limits 时告警
kube_pod_restart_total Pod 重启次数 短时间内重启 > 3 次告警
kube_pod_status_phase Pod 状态(Pending/Running/Failed) Pending > 5 分钟告警
kube_node_status_condition 节点状态(Ready/NotReady) NotReady 告警
kube_deployment_status_replicas_available Deployment 可用副本数 小于期望副本数告警
node_filesystem_avail_bytes 节点可用磁盘空间 < 10% 剩余告警

HPA 自定义指标

HPA 默认支持基于 CPU/Memory 的扩缩容,但实际场景中,基于请求速率(QPS)、队列长度、延迟等自定义指标更有意义。

前置条件

  1. 安装 Prometheus Adapter(将 Prometheus 指标转换为 K8s Custom Metrics API):

    helm install prometheus-adapter prometheus-community/prometheus-adapter \
      --namespace monitoring \
      --set prometheus.url=http://prometheus-server.monitoring.svc
    
  2. 确认 Custom Metrics API 可用:

    kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .
    

HPA 配置示例(基于 HTTP 请求速率)

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 2
  maxReplicas: 20
  metrics:
    # 自定义指标:每 Pod 每秒请求数
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "100"    # 每个 Pod 目标 100 QPS
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300    # 缩容冷却 5 分钟,避免抖动
      policies:
        - type: Percent
          value: 25
          periodSeconds: 60

[!tip] 扩缩容行为配置 behavior 字段(K8s 1.18+)允许精细控制扩缩容速度。建议扩容激进(快速应对流量高峰)、缩容保守(stabilizationWindowSeconds 设大,避免频繁抖动导致连接中断)。


事件(Events)

Kubernetes Events 是集群中发生的重大状态变更记录,是排查问题的第一手信息。

查看事件

# 当前 namespace 的事件,按时间排序
kubectl get events --sort-by='.lastTimestamp'

# 所有 namespace 的事件
kubectl get events -A

# 只看 Warning 级别事件
kubectl get events --field-selector type=Warning

# 查看特定 Pod 的事件
kubectl describe pod my-pod

常见事件类型

Reason 类型 含义
Scheduled Normal Pod 成功调度到节点
Pulling / Pulled Normal 正在拉取 / 已拉取镜像
Created / Started Normal 容器已创建 / 已启动
Unhealthy Warning 健康检查失败
BackOff Warning 重启退避(CrashLoopBackOff)
FailedScheduling Warning 调度失败(资源不足、亲和性冲突等)
OOMKilling Warning 容器因内存不足被杀死
Evicted Warning Pod 被驱逐(磁盘压力、内存压力等)

事件 TTL 与持久化

[!warning] Events 会被自动清理 默认情况下,Events 的 TTL(Time To Live)为 1 小时。这意味着如果一个 Pod 在 2 小时前 OOMKilled,你通过 kubectl describe pod 看不到相关事件。

解决方法:

  1. 调整 TTL(K8s 1.19+):

    # 在 kube-apiserver 中设置
    --event-ttl=24h
    
  2. 持久化到日志系统:将 Events 采集到 Elasticsearch/Loki 等系统中长期保存。

    • 使用 kube-state-metrics 的 kube_event_* 指标采集到 Prometheus。
    • 使用专门的 Events 采集工具(如 event-exporter)推送到日志系统。
  3. event-exporter 示例:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: event-exporter
      namespace: monitoring
    spec:
      template:
        spec:
          containers:
            - name: event-exporter
              image: ghcr.io/resmoio/kubernetes-event-exporter:latest
              args:
                - -conf=/data/config.yaml
              volumeMounts:
                - name: config
                  mountPath: /data
          volumes:
            - name: config
              configMap:
                name: event-exporter-config
    

常见陷阱与最佳实践

陷阱一:日志量爆炸

问题:高并发服务的 debug 日志未关闭,一天产生几十 GB 日志,磁盘告警或 Elasticsearch 索引膨胀。

解决方案:

  • 生产环境默认 info 级别,通过环境变量或配置中心动态调整。
  • 使用 Fluentd/Promtail 的 filter 功能丢弃无用日志(如 kube-system 的健康检查日志)。
  • 设置 Elasticsearch 的 ILM(Index Lifecycle Management)策略,自动过期删除旧索引。
# Fluentd filter 示例:丢弃 kube-system 命名空间的日志
<filter kubernetes.**>
  @type grep
  <exclude>
    key $.kubernetes.namespace_name
    pattern /^kube-system$/
  </exclude>
</filter>

陷阱二:探针超时与应用启动时间不匹配

问题:Java 应用启动需要 60 秒,但 livenessProbe 的 initialDelaySeconds 只设了 30 秒,导致每次重启都进入 CrashLoopBackOff。

解决方案:使用 startupProbe 代替盲目增大 initialDelaySeconds(详见探针设计原则一)。

陷阱三:监控指标标签爆炸

问题:在 Prometheus 指标中使用 user_id、request_path 等高基数(high cardinality)标签,导致时间序列数暴涨,Prometheus 内存 OOM。

解决方案:

  • 高基数字段(如 request_id、user_id)放到日志中,不作为指标标签。
  • request_path 应归一化(如 /api/v1/users/123 → /api/v1/users/:id)。
  • 使用 Prometheus 的 metric_relabel_configs 丢弃不需要的标签。
# Prometheus 配置:限制标签基数
metric_relabel_configs:
  - source_labels: [__name__]
    regex: "container_.*"
    action: keep
  - regex: "pod_name"
    action: labeldrop

陷阱四:readinessProbe 失败但 livenessProbe 正常

问题:readinessProbe 依赖 Redis,Redis 短暂不可达,Pod 被从 Endpoints 摘除。但 livenessProbe 正常,Pod 不会被重启。流量被摘除后 Pod 中积压的请求超时,用户报错。

解决方案:

  • readinessProbe 的依赖检查要设置合理的超时(如 2 秒内无响应即判定失败)。
  • 在应用内部实现熔断机制,不要完全依赖 K8s 探针。
  • 确保 readinessProbe 恢复时,Pod 能快速重新加入 Endpoints(successThreshold: 1)。

陷阱五:kubectl top 数据不准

问题:kubectl top pods 显示的 CPU 使用率为 0 或与实际不符。

原因:

  • Metrics Server 启动后需要约 1-2 分钟才能采集到数据。
  • 容器的 CPU request 未设置,百分比无法计算。
  • Metrics Server 的 --kubelet-insecure-tls 未配置导致与 kubelet 通信失败。

解决方案:

# 检查 Metrics Server 是否正常运行
kubectl get deployment metrics-server -n kube-system

# 检查是否有错误日志
kubectl logs -n kube-system deployment/metrics-server

# 使用 --resource 选项查看原始数值而非百分比
kubectl top pods --containers

最佳实践汇总

领域 实践
日志 stdout 输出、JSON 结构化、统一字段命名、合理轮转
探针 startupProbe 兜底慢启动、liveness 不查依赖、readiness 做适度依赖检查
监控 Prometheus + Grafana 标准栈、ServiceMonitor 声明式配置
告警 分级告警(Warning/Critical)、去重分组、值班轮转
指标 避免高基数标签、区分 RED(Rate/Error/Duration)指标与 USE(Utilization/Saturation/Errors)指标
事件 配置持久化、重要事件告警

延伸阅读