课程目录(第 21 章 / 共 33 章)
课程/进阶:生产实践

可观测性:指标、日志与告警

21 章 / 共 33·22 分钟·进阶可观测性PrometheusGrafana告警

把「出事了才登机器看」变成「提前发现、快速定位」:搭起指标、日志与告警三条链路,并知道哪些告警必须配。

学完这一章,你将能够

  • 说得出可观测三支柱各自回答什么问题、在 K8s 里由谁提供
  • 分清 metrics-server、cAdvisor、kube-state-metrics、node-exporter 的职责
  • 用 Helm 装起一套指标加日志栈,并在 Grafana 里看到关键看板
  • 写出节点未就绪、Pod 反复重启、PVC 将满等必备告警规则

可观测的三支柱与各自负责人

线上出问题时,你需要回答三类不同的问题:现在有多忙刚才发生了什么这个请求慢在哪一段。它们分别对应指标、日志和链路追踪,也就是常说的可观测三支柱。

支柱回答的问题Kubernetes 里由谁提供
Metrics 指标现在有多忙、容量还剩多少kubelet/cAdvisor、metrics-server、node-exporter、kube-state-metrics、Prometheus
Logs 日志刚才发生了什么、报错内容是什么容器 stdout/stderr、kubectl logs、Fluent Bit 或 Vector、Loki 或 Elasticsearch
Traces 链路一个请求慢在哪个服务、哪个函数应用埋点 OpenTelemetry、Tempo 或 Jaeger

三者的成本差别很大:指标最便宜,适合长期保留;日志量大且贵,通常只留几天到几周;链路追踪需要改代码,通常只在核心服务上开。

还有一个容易被忽略的第四类信号:事件(Events)kubectl get events -A 里记录的是调度失败、拉镜像失败、探针失败这类「集群视角的日记」,排障时往往比日志更快命中原因。

指标链路:cAdvisor、metrics-server 与 Prometheus

Kubernetes 的指标有好几个来源,各管一段,别搞混。先看一张链路图,把「谁采集、谁存储、谁展示」三层对上号:

text
指标从哪来、到哪去(示意)

节点上                                聚合与存储                      使用方
──────                                ──────────                      ──────
┌───────────────────┐
│ kubelet / cAdvisor│──/metrics/cadvisor──┐
├───────────────────┤                     ├──▶ metrics-server ──▶ kubectl top / HPA
│ node-exporter     │──/metrics───────────┤    (只保留最近几分钟)
├───────────────────┤                     │
│ kube-state-metrics│──/metrics───────────┼──▶ Prometheus ──┬──▶ Grafana 看板
├───────────────────┤                     │   或 VictoriaMetrics │
│ 应用 /metrics     │──/metrics───────────┘        │         └──▶ Alertmanager ──▶ 值班 / 群
└───────────────────┘
日志:容器 stdout ──▶ /var/log/containers ──▶ Fluent Bit / Vector ──▶ Loki 或 VictoriaLogs ──▶ Grafana
数据源提供什么部署形态
kubelet /metrics/cadvisor容器与节点的 CPU、内存、网络、文件系统用量节点内建,无需单独部署
node-exporter节点操作系统指标:CPU、内存、磁盘、网卡、文件系统DaemonSet
kube-state-metricsKubernetes 对象状态:期望副本数、Pod 阶段、PVC 状态Deployment
metrics-server聚合后的资源用量,供 kubectl top 与 HPA 使用Deployment
应用 /metrics业务指标:QPS、延迟、错误率随应用暴露

最关键的区分是:cAdvisor 管「用了多少资源」,kube-state-metrics 管「对象是什么状态」。想知道某个 Deployment 有几个副本没起来,靠 kube-state-metrics;想知道某个容器吃了多少内存,靠 cAdvisor。

长期存储与查询要靠 Prometheus 系:它按固定间隔去各个 /metrics 端点拉取数据(pull 模型),存成本地时序数据库再用 PromQL 查询,在 Kubernetes 里通常配合 Prometheus Operator,用 ServiceMonitor、PodMonitor 这类 CRD 声明「要抓谁」。

metrics-server 是最轻的一环,它把 kubelet 的数据聚合起来,通过 metrics.k8s.io 这个聚合接口专供 kubectl top 与 HPA 使用。它的安装、两处 TLS 证书校验以及 --kubelet-insecure-tls 的适用场景和风险,在探针与资源管理里有完整说明。这里只记住它的边界:不能替代 Prometheus——只保留最近几分钟的瞬时值,不能查询历史、不能做告警。

VictoriaMetrics:部署形态与保留期

VictoriaMetrics 是一套兼容 Prometheus 的时序数据库,查询语言 MetricsQL 是 PromQL 的超集。它的两个卖点很实际:存储效率更高(同样数据占用更小),以及单机版一个进程就能跑

维度PrometheusVictoriaMetrics
查询语言PromQLMetricsQL,兼容 PromQL 并扩展
存储效率一般明显更好,适合长期保留
部署复杂度单机简单,高可用要额外组件单机版极简,集群版组件多
适合团队已熟悉 Prometheus 生态指标量大、想省存储与运维成本

选哪种部署形态,判据是时间序列数量和查询并发,不是集群规模:victoria-metrics-single 适合千万级序列以内的集群,victoria-metrics-cluster 把写入(vminsert)、查询(vmselect)、存储(vmstorage)拆成三组可独立扩容的组件,适合序列上亿的场景,victoria-metrics-k8s-stack 则是一套装好采集、规则、看板与告警的全家桶。

单机版的数据落在 PVC 上,最该显式设置的是保留期和存储大小:

bash
helm repo add vm https://victoriametrics.github.io/helm-charts/
helm repo update
helm install vmsingle vm/victoria-metrics-single --namespace monitoring --create-namespace \
  --set server.retentionPeriod=1y \
  --set server.persistentVolume.size=100Gi
kubectl -n monitoring get pods

retentionPeriod 支持 30d1y 这类写法,也可以按月数或磁盘用量设上限。生产上必须显式设置:不设就按默认保留期丢数据,容量规划也无从谈起。集群版还要给 vmstorage 设副本数,它决定能容忍多少存储副本同时挂掉:

bash
helm install vmcluster vm/victoria-metrics-cluster --namespace monitoring \
  --set vmstorage.replicaCount=3 --set vmstorage.persistentVolume.size=200Gi

两套的 Grafana 看板基本通用,迁移成本主要在采集配置和少量 PromQL 差异上——vmagent 同时兼容 Prometheus 抓取配置与 ServiceMonitor 这类 CRD,规则不必重写。

参考:reference/k8s-in-action/o11y/vm/README.md

Grafana 与关键看板

Grafana 只做一件事:连上数据源,把查询画成图。它本身不存数据,所以换后端(Prometheus 或 VictoriaMetrics)对看板影响很小。

装好监控栈后,通常用端口转发先看一眼:

bash
kubectl -n monitoring port-forward svc/kube-prometheus-stack-grafana 3000:80
kubectl -n monitoring get secret kube-prometheus-stack-grafana \
  -o jsonpath='{.data.admin-password}' | base64 -d; echo

浏览器打开 http://localhost:3000,用户名 admin,密码是上面命令的输出。如果用的是 VictoriaMetrics 那套,把服务名换成 vmstack-grafana 即可。建议至少固定看四类看板:

  • 集群总览:节点数、CPU 与内存的分配率、Pod 总数、命名空间资源占用。
  • 节点:CPU、内存、磁盘使用率与 inode、网络吞吐,用来发现「某台机器快满了」。
  • 工作负载:Deployment 期望副本与就绪副本的差值、容器重启次数、OOMKilled 次数。
  • API Server 与 etcd:请求延迟分位、5xx 比例、etcd 的 fsync 延迟与 leader 变更次数。

数据源只加两类:指标(Prometheus 或 VictoriaMetrics)和日志(Loki 或 VictoriaLogs),加完点 Save & test 确认连通。kube-prometheus-stack 与 victoria-metrics-k8s-stack 都自带一批常用看板,在 Dashboards 里按 Kubernetes / Compute Resources 分组找即可,不必从零画。

应用看板用 RED 方法起步

给自己的服务做看板时,先盯三个指标:Rate(QPS)、Errors(错误率)、Duration(延迟分位)。这三个够了之后再考虑更细的维度,不要一上来就堆几十个面板。

日志:stdout、采集与查询

容器里正确的做法是把日志写到标准输出,由运行时收集,而不是写进容器内的文件。kubectl logs 读的就是这些内容,但它有三个硬限制:只能看单个 Pod、默认只看当前容器实例(容器重启后要加 --previous)、Pod 删除后日志随之消失。

bash
kubectl -n demo logs deploy/web --tail=50
kubectl -n demo logs deploy/web --previous
kubectl -n demo logs -l app=web --prefix --tail=20

要在集群级别做搜索和留存,就需要采集器加存储。采集器通常以 DaemonSet 运行,读取节点上的容器日志文件,补上 namespace、pod、label 这些元数据,再发到后端。选采集器看两点:后端认不认它资源开销大不大。Fluent Bit 最省资源,Promtail 专为 Loki 而生(VictoriaLogs 也实现了 Loki 的推送接口),Vector 的转换能力最强但更重。

环节常见选择生产落地要点
采集Fluent Bit、Promtail、VectorDaemonSet 读 /var/log/containers/*.log,必须给采集端加限速,否则一个死循环日志能把后端写爆
后端:LokiPromtail、Fluent Bit只有 single-binary 模式且副本数为 1 时才能用本地盘,simple-scalabledistributed 模式都必须配 S3 之类的对象存储
后端:VictoriaLogsFluent Bit、Promtail单机版一个进程即可,资源占用比 Loki 更小,写入与查询语法更灵活

VictoriaLogs 单机版最省事,装完直接用它的 Loki 兼容入口接收 Promtail 的推送:

bash
helm repo add vm https://victoriametrics.github.io/helm-charts/
helm repo update
helm install vlogs vm/victoria-logs-single --namespace logging --create-namespace
kubectl -n logging get pods

Promtail 的 clients 指向 http://vl-victoria-logs-single-server.logging.svc:9428/insert/loki/api/v1/push?_stream_fields=instance,job,host,app,就能把节点日志写进 VictoriaLogs;换回 Loki 只需把这个地址改成 http://loki-gateway/loki/api/v1/push。采集端配好后在 Grafana 里加数据源:Loki 填 http://loki-gateway.monitoring.svc,VictoriaLogs 填 http://vl-victoria-logs-single-server.logging.svc:9428(VictoriaLogs 要先装 victorialogs-datasource 插件,VictoriaMetrics 的 Helm chart 里用 GF_INSTALL_PLUGINS 声明即可)。之后就能按 namespacepod 过滤,把「刚才发生了什么」和「现在有多忙」对起来看。

参考:reference/k8s-in-action/o11y/logs/readme.mdreference/k8s-in-action/o11y/logs/vl/readme.mdreference/k8s-in-action/o11y/logs/loki/readme.md

事件导出:别让集群的「黑匣子」过期

kubectl get events 的问题不是不好用,而是留不住:默认只保留 1 小时左右,Pod 删掉后相关事件很快消失,第二天复盘时现场已经没了。生产上要把事件当成一类日志来采集。

kubernetes-event-exporter 是一个 watch Event API 的 Deployment,按模板把事件字段映射成日志字段,再通过 webhook 写进 VictoriaLogs 或 Loki:

text
Event API ──watch──▶ kubernetes-event-exporter ──webhook──▶ VictoriaLogs / Loki
                                                             │ classify=k8s-event
                                                             │ type / reason / count

                                        LogsQL:classify:"k8s-event" AND type:"Warning"

配置只有三段:route.routes 决定哪些事件发到哪个 receiver,receivers[].webhook.endpoint 指定后端地址,layout 决定事件字段怎么落到日志字段。`count` 字段最值得留——同一个事件被重复上报的次数越多,说明故障持续得越久,这比单看一条事件更能说明严重程度。大集群里还要把 kubeQPSkubeBurst 调到与 API Server 承载能力匹配,否则 exporter 自己会变成 API Server 的压力源。

装好之后,按 classify:"k8s-event" 就能把所有集群事件捞出来,再按 namespacetype:"Warning"component:"kubelet" 逐层过滤。完整的配置清单和查询示例见排障手册里「用指标与事件定位问题」一节。

参考:reference/k8s-in-action/o11y/logs/collectors/kubernetes-event-exporter/readme.mdreference/k8s-in-action/o11y/logs/readme.md

告警:Alertmanager 路由与必备规则

告警分两步:Prometheus 用规则表达式判断「是否异常」,Alertmanager 决定「发给谁、多久发一次」。Alertmanager 的核心是路由树,加上分组与抑制来对抗告警风暴。

配置作用
route.group_by把同一类告警合并成一条通知,常用 namespacealertname
route.group_wait新告警先等一会,等同类告警凑齐再发
route.repeat_interval未恢复的告警多久提醒一次
routes + matchers按标签分流,比如 critical 走值班,warning 走群消息
inhibit_rules抑制次生告警,比如节点挂了就别再报上面的 Pod 全不可用

下面这份规则覆盖了集群里最该有的五类告警,用 kube-prometheus-stack 时直接 kubectl apply 即可:

yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: kube101-critical
  namespace: monitoring
  labels:
    release: kube-prometheus-stack
spec:
  groups:
    - name: kube101.rules
      rules:
        - alert: NodeNotReady
          expr: kube_node_status_condition{condition="Ready",status="true"} == 0
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "节点 {{ $labels.node }} 未就绪"
            description: "节点 {{ $labels.node }} 已持续 5 分钟 NotReady,检查 kubelet 与节点网络。"
        - alert: PodCrashLooping
          expr: increase(kube_pod_container_status_restarts_total[15m]) > 3
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} 反复重启"
        - alert: PVCWillFillIn
          expr: |
            kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes < 0.15
            and
            predict_linear(kubelet_volume_stats_available_bytes[6h], 4 * 3600) < 0
          for: 30m
          labels:
            severity: warning
          annotations:
            summary: "PVC {{ $labels.persistentvolumeclaim }} 预计 4 小时内写满"
        - alert: APIServerHighErrorRate
          expr: |
            sum(rate(apiserver_request_total{code=~"5.."}[5m]))
            /
            sum(rate(apiserver_request_total[5m])) > 0.01
          for: 10m
          labels:
            severity: critical
          annotations:
            summary: "API Server 5xx 比例超过 1%"
        - alert: CertificateExpiringSoon
          expr: certmanager_certificate_expiration_timestamp_seconds - time() < 7 * 24 * 3600
          for: 1h
          labels:
            severity: warning
          annotations:
            summary: "证书 {{ $labels.name }} 将在 7 天内过期"

几点说明:kube_node_status_conditionkube_pod_container_status_restarts_total 来自 kube-state-metrics;kubelet_volume_stats_* 来自 kubelet,需要 Prometheus 抓取 kubelet;证书这条依赖 cert-manager 的指标,如果证书是手工签发的,就用 kube-prometheus-stack 自带的 KubeClientCertificateExpiration 规则覆盖 apiserver 客户端证书。

接收方配置写进 values:

yaml
alertmanager:
  config:
    route:
      group_by: ["namespace", "alertname"]
      group_wait: 30s
      group_interval: 5m
      repeat_interval: 4h
      receiver: default
      routes:
        - matchers:
            - severity="critical"
          receiver: oncall
          repeat_interval: 1h
    receivers:
      - name: default
        webhook_configs:
          - url: http://alert-webhook.monitoring.svc:8080/
      - name: oncall
        webhook_configs:
          - url: http://oncall-webhook.monitoring.svc:8080/

配好之后可以临时把 for 改小、把阈值改低,观察告警是否真的发到接收端,验证完再改回来。

动手练习:装一套最小监控栈,在 Grafana 里看到一条真实指标

第一步,装 metrics-server。注意 kind 集群的 kubelet 用的是自签证书,不加 --kubelet-insecure-tls 会报 x509 错误:

bash
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm repo update
helm install metrics-server metrics-server/metrics-server --namespace kube-system \
  --set 'args={--kubelet-insecure-tls}'
kubectl -n kube-system rollout status deploy/metrics-server --timeout=180s
kubectl top nodes

kubectl top nodes 能打出 CPU 与内存两列数字就算这一步成功;如果报 metrics not available yet,等 30 秒再试一次。

第二步,装指标栈(这里用 kube-prometheus-stack,换成 VictoriaMetrics 那套命令也可以):

bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace
kubectl -n monitoring get pods

第三步,登录 Grafana 并查一条指标:

bash
kubectl -n monitoring port-forward svc/kube-prometheus-stack-grafana 3000:80
kubectl -n monitoring get secret kube-prometheus-stack-grafana \
  -o jsonpath='{.data.admin-password}' | base64 -d; echo

浏览器打开 http://localhost:3000,用户名 admin,密码是上面命令的输出。进入 Explore,数据源选 Prometheus,依次执行:

text
up
sum(rate(container_cpu_usage_seconds_total{namespace="demo"}[5m])) by (pod)

第一条会返回一串值等于 1 的时间序列(每个采集目标一条),说明「kubelet/cAdvisor → Prometheus → Grafana」这条链路已经通了;第二条要先把 demo 命名空间里的应用跑起来才有数据。能在图上看到一条随时间变化的曲线,这套最小栈就算搭好了。

监控栈本身也要被监控

监控挂了没人知道,是最尴尬的故障。至少给 monitoring 命名空间配一条 Pod 未就绪的告警,或者用外部探针访问 Grafana 的 /api/health。另外,Grafana 的 admin 密码要改掉,别用默认值暴露在公网上。

常见坑与排错

可观测性自己的故障最讽刺:你以为在看监控,其实监控早就瞎了。排查时先确认「采集有没有数据」,再怀疑「看板配错了」。

现象原因怎么确认怎么办
kubectl topmetrics not available yet 或 x509 错误metrics-server 刚启动,或抓不到 kubelet 的自签证书kubectl -n kube-system logs deploy/metrics-serverkind 上补 --set 'args={--kubelet-insecure-tls}',等 30 秒重试
Grafana 看板一片空白数据源选错、时间范围不对、指标名不存在在 Explore 里直接执行该表达式看有没有返回切数据源、放宽时间范围、核对指标名与标签
Prometheus 内存持续上涨甚至 OOM标签基数爆炸prometheus_tsdb_head_series 的数值、按指标名统计序列数去掉 user_id 这类高基数标签,用 metric_relabel_configs 丢弃
节点一挂收到几十条告警没有分组与抑制看 Alertmanager 配置里的 group_byinhibit_rules配分组与抑制,并给告警设置 for 持续时间
告警该响却不响表达式永远为假,或 for 太长在 Prometheus 的 Rules 页面直接执行表达式单独跑一遍表达式确认返回值,再调 for
日志后端磁盘被写满采集端无限速、后端无保留策略df -h、Loki 的 limits_config.retention_period配保留期、给采集器加限速

可观测性里最常见的四类事故

  • 指标基数爆炸:把 user_idrequest_id、带 ID 的 URL 路径当标签,时间序列数量会指数增长,Prometheus 内存飙升甚至 OOM。标签只留低基数的维度。
  • 告警风暴:节点一挂,上面几十个 Pod 的告警同时触发。用 group_by 分组、用 inhibit_rules 抑制,再给关键告警设 for 持续时间。
  • 日志采集把磁盘写满:采集器没有限额、后端用本地盘且没有保留策略,几天就能填满。给 Loki 或 Elasticsearch 配保留期,采集端加限速。
  • 只看平均延迟:平均值会被大量快请求拉低,掩盖长尾。看 p95 与 p99,必要时看 p99.9。

自测题

自测:为什么装了 metrics-server,`kubectl top` 还是报错?(点击展开答案)

最常见的两个原因是证书时间。metrics-server 要从每个节点的 kubelet 拉数据,而 kind 之类的本地集群里 kubelet 用的是自签证书,metrics-server 校验失败就会报 x509 错误,必须加 --kubelet-insecure-tls(生产环境应改为把集群 CA 挂给 metrics-server,而不是关掉校验)。另一个原因是它需要跑满一轮采集周期,刚装完的几十秒内会报 metrics not available yet。此外,如果 kubelet 的 read-only-port 或网络被限制,也会抓不到。判断方法很直接:看 metrics-server 自己的日志,错误类型会明确告诉你卡在哪一步。

自测:为什么 cAdvisor 和 kube-state-metrics 都要有,只留一个不行吗?(点击展开答案)

因为它们回答的是两个不同维度的问题。cAdvisor 由 kubelet 内建,提供的是资源消耗:某个容器用了多少 CPU、多少内存、发了多少网络包。kube-state-metrics 提供的是对象状态:某个 Deployment 期望几个副本、实际几个就绪,某个 PVC 是 Bound 还是 Pending。你无法从 cAdvisor 知道「副本数不够」,也无法从 kube-state-metrics 知道「某个容器吃了 3GB 内存」。告警规则同理:kube_pod_container_status_restarts_total 来自 kube-state-metrics,container_memory_working_set_bytes 来自 cAdvisor,两条都要有。

自测:为什么告警一定要配 `for` 和分组、抑制?(点击展开答案)

for 解决的是抖动:一个 Pod 因为一次 GC 或探针超时瞬间重启,for: 10m 能把它过滤掉,避免半夜被误报叫起来。分组和抑制解决的是风暴:节点宕机时,上面所有 Pod 的告警会在同一秒触发,如果不按 namespacealertname 合并,值班人会收到几十条通知,反而看不到根因。用 inhibit_rules 让「节点 NotReady」抑制「该节点上的 Pod 不可用」,本质上是在告诉系统:根因告警已经发了,次生告警不必再喊。这三件配置决定了告警是「有用的信号」还是「被静音的噪音」。

小结

  • 指标回答「有多忙」,日志回答「发生了什么」,链路回答「慢在哪一段」,三者的成本与用途不同。
  • cAdvisor 给资源用量,kube-state-metrics 给对象状态,node-exporter 给节点 OS 指标,metrics-server 只服务 kubectl top 与 HPA。
  • Prometheus 与 VictoriaMetrics 是两条可互换的指标后端,后者更省存储;VM 单机版按序列量选型,retentionPeriod 必须显式设置。
  • 日志采集用 DaemonSet 上的 Fluent Bit、Promtail 或 Vector,后端选 Loki、VictoriaLogs 或 Elasticsearch;Loki 只有单副本单进程模式能用本地盘。
  • 事件默认只留 1 小时左右,用 kubernetes-event-exporter 导出到日志后端,才能事后复盘集群级异常。
  • 告警靠 Prometheus 规则加 Alertmanager 路由,节点 NotReady、Pod 反复重启、PVC 将满、API Server 5xx、证书过期是必备的五条。
  • 监控栈自己要配告警,否则故障会静默。

练习

  1. 装好 metrics-server 后执行 kubectl top pods -A,再对比 Grafana 里同一个容器的内存曲线,说说两者数据为什么可能略有差异。
  2. NodeNotReadyfor 改成 1m,然后故意在一个 kind 节点上停掉 kubelet,观察告警从 Pending 到 Firing 的过程。
  3. web 应用加一个 /metrics 端点(可以用 nginx-prometheus-exporter),接入采集并在 Grafana 里画出 QPS 与延迟分位。

本章的告警规则里用到了节点、Pod、PVC 的状态,这些资源的入门用法在第 10 章 存储卷与 PV/PVC第 11 章 探针与资源管理;etcd 那几条指标怎么读,回看第 18 章 生产集群部署的磁盘验收部分。指标看到异常之后该从哪里下手,排障手册给了一套固定顺序。

到这里,进阶模块的四章就结束了。回头看看这条路线:集群怎么装、网络怎么通、数据怎么落、状态怎么看——这四件事撑起了「敢在生产上管」的底气。