我用Kubernetes和Prometheus搭建了一套7×24小时不宕机的监控体系

从半夜被电话叫醒说起

去年冬天凌晨三点,我的手机响了。运维群里炸了锅——线上服务大面积超时,用户投诉已经涌进客服系统。我揉着眼睛爬起来,打开电脑,发现连个像样的监控面板都没有,只能挨个 SSH 上去 tail 日志。那次事故排查花了两个多小时,事后复盘的时候我痛下决心:必须搞一套靠谱的监控体系,让系统自己告诉我哪里出了问题,而不是等用户来骂街。

折腾了大概三个月,我用 Kubernetes + Prometheus + Grafana + Alertmanager + Chaos Mesh + ArgoCD 把整套可观测性和稳定性工程体系搭了起来。到现在运行了大半年,P0 事故从平均每月两三起降到了几乎为零,平均故障恢复时间(MTTR)从两小时压缩到十五分钟以内。今天我把这套实践从头到尾讲一遍,希望能帮到同样在深夜被电话折磨的同行们。

先说整体架构:为什么选这套技术栈

我们的业务跑在 Kubernetes 集群上,生产环境三个节点池,总共 48 个工作节点,跑着大概 200 多个微服务 Pod。选型的时候我考虑过好几套方案,最终定了这个组合:

  • Prometheus:时序指标采集和存储,云原生监控事实标准,跟 K8s 天然集成
  • Grafana:可视化面板,给开发、运维、管理层各出不同视角的 Dashboard
  • Alertmanager:告警路由和分级,P0/P1/P2 三级 severity 对应不同通知渠道
  • Chaos Mesh混沌工程,主动注入故障验证系统韧性
  • ArgoCD:GitOps 模式的持续交付,配置即代码,声明式部署
  • Docker:容器标准化构建,多阶段构建 + 镜像瘦身

整个数据流大概是这样的:各服务通过 Prometheus Exporter 暴露指标,Prometheus Server 定时抓取,告警规则匹配后推给 Alertmanager,Alertmanager 按严重级别路由到企业微信、钉钉、短信、电话语音。Grafana 直连 Prometheus 做可视化展示。

Prometheus 部署与指标采集

我用 Helm 在 K8s 集群里装的 kube-prometheus-stack,这个 Chart 把 Prometheus、Grafana、Alertmanager、Node Exporter 打包在一起,省了不少事。先加仓库:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-prom prometheus-community/kube-prometheus-stack \\
  --namespace monitoring --create-namespace \\
  --set prometheus.prometheusSpec.retention=30d \\
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName=fast-ssd \\
  --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=200Gi

这里有个坑我想提醒大家:默认 Prometheus 用 emptyDir 存储,重启后数据全丢。生产环境一定要配持久化存储,我用的是 200G 的 SSD StorageClass,保留 30 天数据。另外如果指标量特别大,建议开 TSDB 压缩和分片,我们集群每秒采集约 15 万个数据点,单实例完全扛得住。

服务暴露指标这块,我们的 Spring Boot 应用统一集成了 Micrometer,自动暴露 /actuator/prometheus 端点。K8s 里用 ServiceMonitor 来管理抓取配置:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: order-service
  namespace: production
  labels:
    release: kube-prom
spec:
  selector:
    matchLabels:
      app: order-service
  endpoints:
  - port: metrics
    interval: 15s
    path: /actuator/prometheus
    relabelings:
    - sourceLabels: [__meta_kubernetes_pod_name]
      targetLabel: pod
    - sourceLabels: [__meta_kubernetes_namespace]
      targetLabel: namespace

有个细节我踩过坑:ServiceMonitor 的 labels 必须跟 Prometheus 的 serviceMonitorSelector 匹配,不然 Prometheus 根本不会去抓。装 kube-prometheus-stack 的时候默认 selector 是 release: kube-prom,所以你的 ServiceMonitor 上也得加这个 label。

Grafana 面板设计:不同角色看不同东西

Grafana 我做了三套 Dashboard,分别面向不同角色。开发团队看的是服务级面板,重点展示 RED 指标(Rate、Error、Duration)——请求量、错误率、延迟分布。运维团队看的是集群级面板,节点 CPU/内存/磁盘 IO、Pod 资源使用、网络流量。管理层看的是 SLO 面板,服务可用性百分比、错误预算消耗趋势、MTTR 变化。

我个人最常用的一个面板是”黄金信号”面板,把 Latency、Traffic、Errors、Saturation 四个维度放在一屏,用 PromQL 写的核心查询长这样:

# P99 延迟(按服务分组)
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket{namespace="production"}[5m])) by (le, service)
)

# 错误率(5xx 占比)
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service) /
sum(rate(http_requests_total[5m])) by (service)

# Pod CPU 使用率
sum(rate(container_cpu_usage_seconds_total{container!="POD"}[5m])) by (pod) /
sum(kube_pod_container_resource_limits{resource="cpu"}) by (pod) * 100

# 可用性(SLO 计算基础)
1 - (
  sum(rate(http_requests_total{status=~"5.."}[30m])) /
  sum(rate(http_requests_total[30m]))
)

说个真实体验:有次上线后 P99 延迟从 200ms 飙到 1.8s,但错误率没涨。多亏 Grafana 面板一眼看到延迟突增,赶紧回滚,排查后发现是新版本里有个 N+1 查询问题。如果光看错误率,等用户报障黄花菜都凉了。所以监控不能只盯着错误,延迟信号往往更早暴露问题。

告警分级:P0/P1/P2 三级响应机制

告警体系是我花心思最多的地方。之前用的 Nagios 告警一大堆,大家都麻木了,真正的严重告警淹没在噪声里。这次我做了严格的分级:

  • P0(紧急):核心服务完全不可用、数据丢失风险。触发电话语音 + 短信 + 企业微信,要求 5 分钟内响应。
  • P1(高):核心服务性能严重降级、部分功能不可用。触发企业微信 + 短信,要求 15 分钟内响应。
  • P2(中):非核心服务异常、资源水位预警。触发企业微信通知,工作时间处理。

Alertmanager 的路由配置如下:

route:
  receiver: default
  group_by: ['alertname', 'cluster', 'severity']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
  - matchers:
    - severity = "P0"
    receiver: phone-sms-wechat
    group_wait: 0s
    repeat_interval: 30m
  - matchers:
    - severity = "P1"
    receiver: sms-wechat
    group_wait: 30s
    repeat_interval: 1h
  - matchers:
    - severity = "P2"
    receiver: wechat-only
    group_wait: 5m
    repeat_interval: 4h

receivers:
- name: phone-sms-wechat
  webhook_configs:
  - url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=p0-webhook-key'
  - url: 'http://alert-gateway/sms/send?level=P0'
  - url: 'http://alert-gateway/voice/call?oncall=true'
- name: sms-wechat
  webhook_configs:
  - url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=p1-webhook-key'
  - url: 'http://alert-gateway/sms/send?level=P1'
- name: wechat-only
  webhook_configs:
  - url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=p2-webhook-key'

P0 告警的电话语音是我自己写的一个小服务,调的云厂商语音 API,直接打电话给当值 oncall 同事,电话里会语音播报:”P0 告警,订单服务在 production 命名空间全部 Pod 不可用,请立即处理。”这个效果非常好,没有人在凌晨能睡过电话铃声。

告警规则方面,我定义了几条核心规则,贴几条比较关键的:

groups:
- name: critical-service
  rules:
  # P0: 核心服务全部 Pod 不可用
  - alert: ServiceDown
    expr: up{job="order-service", namespace="production"} == 0
    for: 1m
    labels:
      severity: P0
    annotations:
      summary: "订单服务完全不可用"
      description: "{{ $labels.instance }} 已离线超过 1 分钟"

  # P0: 5xx 错误率超过 10%
  - alert: HighErrorRate
    expr: |
      sum(rate(http_requests_total{namespace="production",status=~"5.."}[5m])) by (service)
      / sum(rate(http_requests_total{namespace="production"}[5m])) by (service) > 0.1
    for: 2m
    labels:
      severity: P0
    annotations:
      summary: "{{ $labels.service }} 错误率超过 10%"

  # P1: P99 延迟超过 1 秒
  - alert: HighLatency
    expr: |
      histogram_quantile(0.99,
        sum(rate(http_request_duration_seconds_bucket{namespace="production"}[5m])) by (le, service)
      ) > 1
    for: 5m
    labels:
      severity: P1
    annotations:
      summary: "{{ $labels.service }} P99 延迟超过 1 秒"

  # P2: 节点磁盘使用率超过 80%
  - alert: DiskSpaceWarning
    expr: |
      (node_filesystem_size_bytes - node_filesystem_avail_bytes)
      / node_filesystem_size_bytes * 100 > 80
    for: 10m
    labels:
      severity: P2
    annotations:
      summary: "节点 {{ $labels.instance }} 磁盘使用率超过 80%"

关于告警有一个重要的经验:for 字段一定要设。不加 for 的告警会在指标瞬间抖动时疯狂触发,加了之后只有持续异常才告警。P0 我设 1-2 分钟,P1 设 5 分钟,P2 设 10 分钟。这个值需要根据业务特点调,调到既能及时告警又不至于误报。

混沌工程:用 Chaos Mesh 主动制造故障

监控搭好了,但你怎么知道告警真的会触发?故障真的能自动恢复?答案是混沌工程。我在集群里装了 Chaos Mesh,定期搞”消防演习”,主动注入故障来验证系统的韧性。

# 安装 Chaos Mesh
curl -sSL https://mirrors.chaos-mesh.org/v2.5.0/install.sh | bash

# 模拟订单服务 Pod 网络延迟 500ms,持续 3 分钟
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
  name: order-service-latency
  namespace: chaos-testing
spec:
  action: delay
  mode: all
  selector:
    namespaces:
    - production
    labelSelectors:
      app: order-service
  delay:
    latency: "500ms"
    correlation: "100"
    jitter: "50ms"
  duration: "3m"
  scheduler:
    cron: "@every 24h"

第一次跑混沌实验的时候特别刺激——我故意在订单服务上注入了 500ms 的网络延迟,结果发现 P1 延迟告警 5 分钟后准时触发,但自动扩缩容(HPA)没来得及反应,因为我的 HPA 指标用的 CPU 而不是延迟。后来我把 HPA 改成了基于自定义延迟指标扩缩容,下次再注入同样的延迟,Pod 自动扩了两个副本,延迟恢复到了正常水位。

还有一次我模拟了一个节点掉电(用 PodChaos 杀掉节点上所有 Pod),发现某个服务的 Pod 都调度到了同一个节点上,没有做反亲和性。这个问题如果等真实节点故障才暴露出来,就是一次 P0 事故。混沌工程让我提前发现了至少五六个类似的隐患。

ArgoCD GitOps:让部署也可观测

传统的 CI/CD 流水线是 Push 模式——Jenkins 构建完直接 kubectl apply 推到集群。这种方式有个问题:如果有人手动改了集群里的配置,你的 Git 仓库和实际状态就不同步了,出了问题根本不知道线上跑的是什么版本。我换成了 ArgoCD 的 GitOps Pull 模式,Git 仓库作为唯一事实来源,ArgoCD 持续同步。

# ArgoCD Application 配置
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: order-service
  namespace: argocd
spec:
  project: production
  source:
    repoURL: 'git@github.com:mycompany/k8s-manifests.git'
    targetRevision: main
    path: production/order-service
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true
    - PruneLast=true
  revisionHistoryLimit: 10

selfHeal: true 这个参数特别关键——如果有人手动 kubectl 改了线上配置,ArgoCD 会自动把它纠正回 Git 仓库的状态。有次有个开发同事手动把订单服务的副本数从 4 改成了 2 想省资源,结果 ArgoCD 一分钟内就给改回来了,还触发了一条通知。从那以后再也没人敢手动改集群配置了。

CI 流水线那侧,GitLab CI 构建完镜像推到 Harbor,然后自动修改 k8s-manifests 仓库里的镜像 tag。ArgoCD 检测到仓库变更后自动同步部署。整个链路完全可追溯,每次部署都能在 Git 历史里看到是谁、什么时候、改了什么。

Docker 最佳实践:镜像构建的那些事

容器化是整个 DevOps 体系的基础,镜像构建质量直接影响部署速度和安全性。我们的 Dockerfile 统一采用多阶段构建,最终镜像基于 distroless,体积从最初的 800MB 压到了 120MB 左右:

# 构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src/ ./src/
RUN mvn package -DskipTests -B

# 运行阶段
FROM gcr.io/distroless/java17-debian12:nonroot
COPY --from=builder /build/target/order-service-*.jar /app/app.jar
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"]

几个关键点:第一,RUN mvn dependency:go-offline 先下载依赖再 copy 源码,利用 Docker 层缓存,改代码不会重新下依赖,构建速度提升非常多。第二,用 distroless 而不是完整 OS 镜像,攻击面小很多,而且镜像里没有 shell,被攻破后也没法随便执行命令。第三,USER nonroot 以非 root 用户运行容器,这是 K8s 安全基线要求。第四,-XX:MaxRAMPercentage=75.0 让 JVM 根据容器 cgroup 限制自动设置堆内存,不用手动算 Xmx。

故障应急响应 SOP:从混乱到有序

最后说个容易被忽略但极其重要的东西——故障响应 SOP。我写了一套标准操作流程,所有 oncall 同事都要熟读。流程很简单,分五步:

第一步:确认告警(0-2 分钟)。收到 P0 告警后,先在企业群里回复”收到,开始处理”,然后在 Grafana 上快速确认告警是否属实。偶尔会遇到误报(比如 Prometheus 抓取超时导致的假告警),确认后可以在 Alertmanager 里 silence 掉。

第二步:止血(2-5 分钟)。先止血再排查,不要纠结根因。最常用的止血手段是回滚——ArgoCD 里一键回滚到上一个版本,几秒钟搞定。如果是流量太大扛不住,临时扩容 Pod 数量。如果是依赖的下游服务挂了,触发熔断降级。

第三步:通知干系人(5 分钟内)。P0 事故必须通知到产品负责人和业务方,告知当前影响范围和预计恢复时间。我准备了一套通知模板,填几个变量就能发出去,不用现场组织语言。

第四步:根因排查(止血后)。这时候可以从容一些了。看 Grafana 面板对比故障前后指标变化,看 Loki 里的应用日志,看 Jaeger 里的分布式追踪链路。我有一次就是通过 Jaeger 发现某个下游服务的 gRPC 调用超时,顺藤摸瓜找到是那个服务的数据库连接池配置太小。

第五步:事后复盘(24 小时内)。写 postmortem 文档,记录时间线、根因、影响范围、修复措施、后续 action items。复盘文化很重要—— blameless,对事不对人,重点是改进系统而不是追究责任。

写在最后

这套体系搭完之后,最大的感受是:监控不是装个软件就完事的,它是一套持续的工程实践。Prometheus 采集的指标需要随着业务演进不断调整,告警规则需要根据误报漏报持续优化,混沌实验要定期跑来发现新的风险点。DevOps 和 SRE 的本质不是工具堆叠,而是建立一套让系统自己说话、让故障自愈的机制。

如果你正在搭建监控体系,我的建议是:先从核心服务的 RED 指标开始,先把 P0 告警做起来,保证关键时刻电话能响。然后逐步完善延迟、饱和度、业务指标,再上混沌工程和 GitOps。不要一上来就追求大而全,否则大概率会烂尾。把基础设施一块块夯实,系统自然会越来越稳。

希望这篇文章能帮到正在深夜值班、手忙脚乱排查问题的你。搞技术不容易,少加班、多睡觉,把时间花在刀刃上。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/wo-yong-kubernetes-he-prometheus-da-jian-le-yi-tao-7x24/

(0)
小编小编
上一篇 17小时前
下一篇 17小时前

相关推荐