Day 8:AlertManager 真接入 + PrometheusRule 实战
kube-prometheus-stack 装上之后,最容易停在「Prometheus 在采、Grafana 在画」的中间态 —— rule 是装好的 35 条默认规则,alert 触发了也只在 Alertmanager UI 里堆着,没有一条真的推到通知渠道。这一天把这个链路打通:mock webhook receiver、AlertmanagerConfig CRD、5 条自定义 PrometheusRule,再用集群当下真实存在的 28 个 firing alert 做端到端验证。
整篇按 A → E 5 个阶段走,每阶段先说做什么,再讲一个真坑。
集群当前状态
接 Day 7 装好的 kube-prometheus-stack(helm release kps):35 条内置 rule(kps chart 自带:KubeAPIDown / KubePodCrashLooping / etcd 系列)+ 0 条自定义 + Alertmanager alertmanagerConfigSelector: {}(不接受任何 CR)+ 实时 firing 28 个(多数是几天前 audit policy 改完 cp-2/cp-3 etcd 重启遗留 + Grafana CrashLoop 次生告警)。目标:让这 28 个端到端送达 webhook,JSON 完整。
A. mock webhook receiver:先把「能收」做出来
接生产渠道前先要一个能看到 Alertmanager 真实发什么 JSON 的 receiver。直接接钉钉只能看客户端渲染后的 markdown,看不到原始 payload。用 mendhak/http-https-echo —— 把 POST body + headers 全 echo 到 stdout:
# alert-receiver.yaml
apiVersion: v1
kind: Namespace
metadata: {name: alert-receiver}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: webhook-mock, namespace: alert-receiver}
spec:
replicas: 1
selector: {matchLabels: {app: webhook-mock}}
template:
metadata: {labels: {app: webhook-mock}}
spec:
containers:
- {name: c, image: mendhak/http-https-echo:latest, ports: [{containerPort: 8080}], env: [{name: HTTP_PORT, value: '8080'}]}
---
apiVersion: v1
kind: Service
metadata: {name: webhook-mock, namespace: alert-receiver}
spec:
selector: {app: webhook-mock}
ports: [{port: 80, targetPort: 8080}]
kubectl apply -f alert-receiver.yaml
# svc/webhook-mock ClusterIP 10.103.39.189:80
独立 namespace:AlertmanagerConfig 会被 Operator 自动注入 namespace=<configns> matcher(见 §B),receiver 跟业务混在一起后期绕。不直连钉钉:demo 没凭证 + mock 能看完整 JSON,接生产只换 URL + 加 template。
B. AlertmanagerConfig CRD:Operator-pattern 接 receiver
非 Operator 时代改 Alertmanager 要 helm upgrade 全 chart values。AlertmanagerConfig CRD 把 receiver / route 切成独立 K8s 资源,GitOps 友好。
# alertmanager-config.yaml
apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
name: bootcamp-routes
namespace: monitoring
labels: {alertmanagerConfig: bootcamp} # ← AM Pod 的 selector 抓这个
spec:
route:
receiver: webhook-mock
groupBy: [alertname, severity]
groupWait: 10s
groupInterval: 30s
repeatInterval: 4h # 不要 1h,见 §D 表
routes:
- matchers: [{name: severity, value: critical}]
receiver: webhook-mock
groupWait: 0s # crit 不等 group
- matchers: [{name: alertname, value: Watchdog}]
receiver: webhook-mock # kps 心跳必须接
receivers:
- name: webhook-mock
webhookConfigs:
- url: http://webhook-mock.alert-receiver.svc.cluster.local/alerts
sendResolved: true
kubectl apply -f alertmanager-config.yaml
apply 完什么都不会发生。Alertmanager Pod 默认 alertmanagerConfigSelector: {},不接受任何 CR(Operator 安全默认,避免误装 chart 偷偷改 routing)。patch 打开:
kubectl patch alertmanager kps-kube-prometheus-stack-alertmanager -n monitoring --type=merge \
-p '{"spec":{"alertmanagerConfigSelector":{"matchLabels":{"alertmanagerConfig":"bootcamp"}}}}'
B 真坑:Operator 自动加 namespace matcher,跨 ns alert 全丢
Operator 合并 AlertmanagerConfig 时自动给 route 加 matcher namespace="monitoring"。也就是说写在 monitoring 下的 config 只接收 namespace=monitoring 的 alert,其他 ns(kube-system etcd、longhorn-system volume)走默认 receiver(null —— 直接丢)。
第一次接 webhook 5 分钟没收到 POST,UI 看所有 alert 标 Inhibited By: null receiver 就是这个原因。多租户合理(A/B 团队隔离),单租户需要显式覆盖:
kubectl patch alertmanager kps-kube-prometheus-stack-alertmanager -n monitoring \
--type=merge -p '{"spec":{"alertmanagerConfigMatcherStrategy":{"type":"None"}}}'
生产多团队走另一条路:每个 ns 一个 AlertmanagerConfig + 配 alertmanagerConfigNamespaceSelector,隔离干净。
附带现象:Operator 合并时自动给 receiver name 加 namespace 前缀(webhook-mock → monitoring/bootcamp-routes/webhook-mock),webhook JSON 里看到这个全名是正常的。
C. 5 条自定义 PrometheusRule
35 条内置 rule 覆盖控制面 + 节点,业务相关告警必须自己写。挑 5 条覆盖典型来源:CPU / 内存 / Pod 重启 / 节点 / 存储。
# custom-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: bootcamp-custom-alerts
namespace: monitoring
labels: {release: kps} # ← 必须!见下文真坑
spec:
groups:
- name: bootcamp.cluster
rules:
- alert: NodeCPUHigh
expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 2m
labels: {severity: warning}
annotations: {summary: 'Node CPU > 80% on {{ $labels.instance }}'}
- alert: NodeMemoryHigh
expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 85
for: 5m
labels: {severity: warning}
annotations: {summary: 'Node memory > 85% on {{ $labels.instance }}'}
- alert: KubePodCrashLoopingCustom
expr: rate(kube_pod_container_status_restarts_total[5m]) * 60 * 5 > 3
for: 5m
labels: {severity: warning}
annotations: {summary: 'Pod {{ $labels.namespace }}/{{ $labels.pod }} restarted 3+ in 5m'}
- alert: NodeNotReady
expr: kube_node_status_condition{condition="Ready",status="true"} == 0
for: 3m
labels: {severity: critical}
annotations: {summary: 'Node {{ $labels.node }} not Ready 3m+'}
- alert: PVCFillingUp
expr: (kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) > 0.85
for: 5m
labels: {severity: critical}
annotations: {summary: 'PVC {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} > 85% full'}
三个关键字段:expr PromQL(结果非空即触发)、for 连续满足多久才 pending → firing(防抖,见面试题)、labels.severity 路由依据。
C 真坑:少了 release: kps label,rule 装了但 Prometheus 不加载
kubectl get prometheusrule 能看到资源,但 Prometheus UI 的 Rules 页找不到这 5 条。
根因:kps 给 Prometheus 资源配 ruleSelector: {matchLabels: {release: kps}},Operator 只翻译带这个 label 的 PrometheusRule,少了直接忽略。
# 验 selector
kubectl get prometheus -n monitoring kps-kube-prometheus-stack-prometheus \
-o jsonpath='{.spec.ruleSelector}'
# {"matchLabels":{"release":"kps"}}
# 修
kubectl label prometheusrule bootcamp-custom-alerts -n monitoring release=kps --overwrite
30 秒 reconcile,UI 出现 bootcamp.cluster + 5 条 rule。同样适用 ServiceMonitor / PodMonitor —— Operator 资源 apply 完看不到效果第一反应查 selector。
D. 端到端验证:28 firing alert 真打到 webhook
kubectl port-forward -n monitoring svc/kps-kube-prometheus-stack-alertmanager 9093:9093 &
curl -s http://localhost:9093/api/v2/alerts | jq '[.[] | select(.status.state=="active")] | length'
# 28
跑着的 28 个(节选):etcdMembersDown / etcdInsufficientMembers (cp-2/cp-3 etcd 重启遗留) / KubeControllerManagerInstanceUnreachable x3 / TargetDown x3 / KubePodCrashLooping (Grafana) / Watchdog (kps 永远 firing 的心跳) / 自定义的 NodeCPUHigh 等。
kubectl logs -n alert-receiver -l app=webhook-mock -f 看到的 JSON:
{
"receiver": "monitoring/bootcamp-routes/webhook-mock",
"status": "firing",
"alerts": [{
"labels": {"alertname": "KubePodCrashLooping", "namespace": "monitoring",
"pod": "kps-grafana-...", "severity": "warning"},
"annotations": {"runbook_url": "https://runbooks.prometheus-operator.dev/...",
"summary": "Pod is crash looping."},
"startsAt": "2026-05-26T08:37:44.174Z",
"fingerprint": "7eced4013f5f4ede"
}],
"groupLabels": {"alertname": "KubePodCrashLooping", "severity": "warning"}
}
完整字段含 startsAt / endsAt / fingerprint / runbook_url / generatorURL / labels / annotations,接钉钉/企微只写 template 映射到 markdown 即可。按 groupBy: [alertname, severity] 分组后 28 alert 合并成 4 个 group,每 group 一次 HTTP 200 POST。
生产参数速查
| 参数 | 推荐 | 为什么 |
|---|---|---|
groupBy: [alertname, namespace] | — | 10 pod 同时挂发 1 条不是 10 条 |
groupWait | 10s 普通 / 0s critical | warn 等几秒等同类,critical 不等 |
groupInterval | 30s | 太短刷屏,太长滞后 |
repeatInterval | 4h | 不要 1h,24h 24 次直接被静音;4h = 6 次刚好 |
inhibitRules | crit 静默同 alertname 的 warn | 减噪 |
sendResolved | true | 否则不知道何时自动恢复 |
E. inhibit_rule:critical 静默同 alertname 的 warn
source_matchers 匹配的 alert firing 时,跟它共享 equal 里所有 label 的 target_matchers alert 被静默。典型用法:同一个 alertname 有 warn / crit 两个阈值,crit 触发后压住 warn:
spec:
inhibitRules:
- sourceMatch: [{name: severity, value: critical}]
targetMatch: [{name: severity, value: warning}]
equal: [alertname, namespace]
跨 alertname 的关联(etcdInsufficientMembers 抑制 etcdMembersDown)用 inhibit 不优雅,更适合在 PromQL 里合并表达。
silence / inhibit / group 三者对比:group 是 yaml 静态合并通知(默认就有),inhibit 是 yaml 配的减噪规则(另一条 firing 时压住这条),silence 是人在 UI/API 临时屏蔽(维护窗口专用,不进 git)。生产都需要。
接生产 webhook:换 receiver 就好
mock 验通后,接生产渠道只换 receiver 段。Alertmanager 原生支持 slackConfigs / wechatConfigs / pagerdutyConfigs,钉钉需配套 prometheus-webhook-dingtalk 做 JSON → markdown。webhook URL 必须 Secret 引用,不明文写 CRD:
- name: slack
slackConfigs:
- apiURL: {name: slack-webhook-secret, key: url} # secretRef
channel: '#alerts'
sendResolved: true
title: '{{ .GroupLabels.alertname }}'
text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}'
kubectl -n monitoring create secret generic slack-webhook-secret \
--from-literal=url=https://hooks.slack.com/services/XXX
CRD apply 到 git 时 webhook URL 不泄露。企微 / 钉钉 / PagerDuty 同理。
典型坑速查
| 现象 | 根因 | 修复 |
|---|---|---|
| AlertmanagerConfig apply 完 receiver 不生效 | 默认 alertmanagerConfigSelector: {} | patch Alertmanager 开 selector |
| webhook 不收,UI 看 alert 都是 null receiver | Operator 自动加 namespace=<configns> matcher | 设 alertmanagerConfigMatcherStrategy=None |
| PrometheusRule apply 完 UI 看不到 | 少 release: kps label | 加 label |
| Watchdog 一直 firing | kps 心跳告警,就是要永远 firing | 路由到 Dead Man's Snitch |
| webhook 间歇收不到 | groupWait 太长 group 还在凑 | crit 路由设 groupWait: 0s |
repeat_interval: 1h 值班炸了 | 一天 24 次推送 | 改 4h 起步 |
面试常见题
Q1:PrometheusRule 和 AlertmanagerConfig 是什么关系?
两个 CRD 管告警链路两端:
- PrometheusRule 在 Prometheus 侧,定义「什么 metric 满足什么条件算告警」。Operator 翻译成 rule 文件,Prometheus 每 30s eval 一次 + 把 firing 推 Alertmanager。
- AlertmanagerConfig 在 Alertmanager 侧,定义「收到 alert 怎么路由 / 分组 / 静默 / 发给谁」。Operator 合并进
alertmanager.yaml。
链路:PrometheusRule → Prometheus eval → Alertmanager API → AlertmanagerConfig route → receiver。
实战易错:两个 CRD 都需要对应 Operator 资源(Prometheus / Alertmanager)的 selector 显式匹配,默认 selector 是 {} 或带特定 label,少了 label 等于没装。
Q2:alert 为什么有 pending → firing 过渡?for 字段干什么的?
防抖。NodeCPUHigh: expr > 80 没有 for:CPU 抖到 81% 立刻 firing,回 79% 立刻 resolved,1 分钟内可能切换 10 次刷屏。for: 2m 让 expr 满足后进入 pending,连续满足 2 分钟才转 firing,中途任一次不满足重新计时。
生产 alert 几乎都不能省 for(除了 KubeAPIDown 这种 1 秒就该报的)。经验值:CPU/内存 2-5m、可用性类 1-3m、趋势类(PVCFillingUp)10-30m。
Q3:Alertmanager 的 group / inhibit / silence 三个减噪机制区别?
- group:同类 alert 合并通知(10 pod 同时挂发 1 条不是 10 条)。alert 本身还在,只是通知合并。
- inhibit:另一条 alert firing 时压住这条不发。典型:crit 触发后压住同事件 warn。alert 还在 UI,不推 receiver。
- silence:人在 UI / API 手动加,按 matcher 在指定时间段屏蔽通知。维护窗口专用。
层次:group 默认就有,inhibit 是 yaml 固定规则,silence 是人工临时操作。生产都需要。
Q4:receiver 配多个,alert 怎么路由?
Route 是树形结构:进 root → 按顺序检查子 routes,第一个 matchers 命中的子路由接收。子路由设 continue: true 命中后继续匹配后续子路由(一个 alert 发多个 receiver)。任何子路由都不命中 → 落到当前路由 receiver。
route:
receiver: default-slack # fallback,必须有
routes:
- matchers: [{name: severity, value: critical}]
receiver: pagerduty
continue: true # 同时也发下面的 oncall-slack
- matchers: [{name: severity, value: critical}]
receiver: oncall-slack
- matchers: [{name: team, value: data}]
receiver: data-team-slack
要点:root 必须有 fallback(否则未分类 alert 直接丢),continue 用于多渠道通知关键 alert。
Q5:生产告警分级和值班轮转怎么设计?
按 severity 分级:critical → PagerDuty / 钉钉电话 + Slack(15 分钟 SLA)、warning → Slack 频道(工作时间 1 小时)、info → 邮件归档。
值班:PagerDuty / Grafana OnCall 配 schedule 按周轮主备双人;escalation 主班 15 分钟没 ack 升级到备班,30 分钟升级到经理;Dead Man's Snitch 把 Alertmanager 永远发的 Watchdog alert 接到外部心跳监控服务,几分钟没收到反向报警 —— 防 Alertmanager 自己挂导致以为没事其实全挂。
经验:repeat_interval ≥ 4h、runbook_url 必填 annotation(半夜被叫起来直接点链接)、Grafana dashboard 链接也放 annotation、工作日/周末不同 schedule。
下一步
Day 8 结束集群有完整告警闭环:5 条自定义 + 35 条 kps 内置 PrometheusRule → Prometheus eval → Alertmanager 分组路由 → webhook 接收,28 firing 端到端验通。下一步把 mock 换成钉钉 / Slack,把这 28 个历史 firing 各自的根因(etcd / Grafana / TargetDown)逐个修掉 —— 是真问题,不是 alert 配置问题。
Day 9 接 GPU 节点:k3s 单节点 + NVIDIA GPU Operator + vLLM 跑 Qwen2.5,主 K8s 跨集群 curl GPU NodePort,Hubble 看 L7 流量。