本文整合了 Prometheus 监控体系的完整实践:从监控原理与架构入手,依次介绍基于 kube-prometheus-stack 的部署、PromQL 查询语言、Exporter 与 Pushgateway 数据采集、服务发现(文件 / ServiceMonitor / PodMonitor / Probe)、告警(AlertManager 规则与企业微信通知)、Grafana 个性化定制,以及常见监控问题的排查方法。
1. 监控概述 1.1 常见的监控数据类型 监控数据主要分为两大类:
应用级别的数据 :了解应用自身的监控指标,例如 CPU 使用率、内存使用率、请求失败率等。
集群级别的数据 :了解集群自身的运行状况,例如节点的 CPU、内存、网络吞吐等指标,及时掌握 kubelet 的负载情况与 Kubernetes 各组件的状态。
此外,运行过程中 Kubernetes 产生的各种 event 数据,以及 Deployment、StatefulSet、DaemonSet、Pod 等对象的状态、资源请求、调用和 API 延迟等指标,也是必须收集的内容。
1.2 常见的监控数据收集工具
cAdvisor :至今仍编译在 kubelet 里,容器级指标经 kubelet 的 /metrics/cadvisor 暴露。v1.12 移除的只是它的独立 Web UI(4194 端口)。
Heapster :通过 cAdvisor 收集汇总各种性能数据,是自动伸缩(Autoscale)所依赖的组件,现已废弃,由 metrics-server 替换。
metrics-server :集群范围内的监控数据聚合工具,可以通过标准的 Kubernetes API 访问这些监控数据。
kube-state-metrics :监听 kube-apiserver 中的数据,生成有关资源对象的状态指标,例如 Deployment、Node、Pod 的状态(这是它与 metrics-server 的最大区别)。
node-exporter :Prometheus 的一个 exporter,用于获取节点级别的监控指标。
Prometheus + Grafana :推荐方案,也是本文的核心。
2. Prometheus 架构与原理
Prometheus 项目工作的核心是 Prometheus Server,它使用 Pull(拉取) 的方式搜集被监控对象的 Metrics 数据(监控指标数据),再把这些数据写进自带的本地 TSDB (--storage.tsdb.path 下的 WAL 加每 2 小时一个 block,默认保留 15 天),以便后续按照时间进行检索。OpenTSDB、InfluxDB 这类是外部远端存储,只能通过 remote_write/remote_read 对接,不是 Prometheus 的内置后端;要长期存储或全局视图,一般再挂 Thanos、VictoriaMetrics。有了这套核心监控机制,Prometheus 的其余组件都是用来配合它运行的。
2.1 核心组件
Pushgateway :允许被监控对象以 Push 的方式向 Prometheus 推送 Metrics 数据。主要用于 Job 类程序——这类程序存活时间较短,不适合采用拉取的方式。
Alertmanager :根据 Metrics 信息灵活地设置报警。
Grafana :前台界面,对外暴露可灵活配置的监控数据可视化界面。
Exporters :用于一些第三方服务,通过转换提供符合 Prometheus 规范的 Metrics 信息,例如 Node Exporter 提供节点相关的信息。
Service Discovery :用于服务发现,获取服务对应的 EndPoint 用于数据采集,默认支持 DNS、Consul、Kubernetes 等多种发现方案,可自动发现新服务并纳入监控,减少手动配置。
Client Library :为各种语言提供的客户端库,提供 HTTP 服务的 Metrics 接口,当 Prometheus Server 拉取时提供实时的 Metrics 数据。
2.2 Metrics 数据类型 通过 /metrics 接口可以查看某个目标暴露的所有指标:
1 curl 10.56.67.105:9090/metrics
Prometheus 的指标分为四种类型:
Counter :只增不减的计数器,例如 http_requests_total、node_cpu。
Gauge :可增可减,例如主机的 CPU、内存、磁盘使用率,当前的并发量。
Histogram :近似的百分比估算数值,用于统计和分析样本的分布情况(例如请求响应时间在 0.5s、0.5~1s、1s 以上各自的占比)。
Summary :分位数由客户端在滑动窗口里算好后直接暴露。关键差别不在”精准还是近似”,而在分位数在哪算、能不能聚合 :Summary 的分位数在 Prometheus 侧没法再聚合 (多个实例的 p99 求平均在数学上没有意义),而且埋点时就定死了分位点,事后改不了。Histogram 暴露的是可累加的 bucket,服务端用 histogram_quantile 估算,精度取决于 bucket 划分,但能跨实例聚合、分位点也能事后调整 。所以多副本服务通常应该优先选 Histogram。
Exporter 提交给 Prometheus 的 key-value 中,value 必须是浮点数或整数。
2.3 kube-aggregator 与聚合 API
kubeadm 默认开启的是聚合层本身 ,但 metrics.k8s.io/v1beta1 这个 APIService 是由 metrics-server 注册的,而 kubeadm 并不会帮你装它。没装时访问下面的地址只会得到 404,先确认 kubectl get apiservices v1beta1.metrics.k8s.io 是 True 再来取指标:
1 http://127.0.0.1:8001/apis/metrics.k8s.io/v1beta1/namespaces/<namespace-name>/pods/<pod-name>
当 Kubernetes 的 API Server 开启了 Aggregator 模式之后,访问 apis/metrics.k8s.io/v1beta1 时,实际访问到的是一个叫作 kube-aggregator 的代理。而 kube-apiserver 正是这个代理的一个后端,Metrics Server 则是另一个后端。在这个机制下,还可以添加更多的后端给 kube-aggregator。所以 kube-aggregator 其实就是一个根据 URL 选择具体 API 后端的代理服务器,通过这种方式可以很方便地扩展 Kubernetes 的 API。
3. 部署 在 Kubernetes 中推荐使用 Prometheus Operator / kube-prometheus-stack 进行部署,它以 Operator + CRD 的方式统一管理 Prometheus、Alertmanager、Grafana 及各类采集配置。
使用 Helm 可以方便地修改配置内容。
3.1 使用 Helm 部署 kube-prometheus-stack 配置思路是安装前基于文件修改 ,可以先用 --dry-run 渲染出最终结果进行检查:
1 2 3 4 /etc/kubeasz/bin/helm upgrade prometheus --install -n monitor \ -f prom-values.yaml \ /etc/kubeasz/roles/cluster-addon/files/kube-prometheus-stack-45.23.0.tgz \ --dry-run > prometheus.yaml
prom-values.yaml 的完整示例如下,涵盖 Alertmanager(含抑制规则与路由)、Grafana、各控制平面组件(apiserver / kubelet / controller-manager / scheduler / etcd / proxy / coreDns)的抓取,以及 Operator 与 Prometheus 的存储配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 kubeTargetVersionOverride: "1.27.2" alertmanager: alertmanagerSpec: image: registry: easzlab.io.local:5000 storage: volumeClaimTemplate: spec: storageClassName: local-path accessModes: ["ReadWriteOnce" ] resources: requests: storage: 10Gi service: nodePort: 30902 type: NodePort config: global: resolve_timeout: 5m inhibit_rules: - equal: - namespace - alertname source_matchers: - severity = critical target_matchers: - severity =~ warning|info - equal: - namespace - alertname source_matchers: - severity = warning target_matchers: - severity = info - equal: - namespace source_matchers: - alertname = InfoInhibitor target_matchers: - severity = info receivers: - name: web.hook webhook_configs: - send_resolved: false url: http://prometheus-webhook-adapter:8060/adapter/wx - name: "null" route: group_by: - namespace group_interval: 5m group_wait: 30s receiver: web.hook repeat_interval: 12h routes: - matchers: - alertname = "InfoInhibitor" receiver: "null" - matchers: - alertname = "Watchdog" receiver: "web.hook" - matchers: - "severity = critical" receiver: "web.hook" templates: - /etc/alertmanager/config/*.tmpl grafana: defaultDashboardsTimezone: CST enabled: true adminUser: admin adminPassword: password image: repository: easzlab.io.local:5000/prometheus/grafana service: nodePort: 30903 type: NodePort sidecar: image: repository: easzlab.io.local:5000/prometheus/k8s-sidecar skipTlsVerify: true datasources: datasources.yaml: apiVersion: 1 datasources: - name: Prometheus type: prometheus uid: prometheus url: http://prometheus-kube-prometheus-prometheus:9090 access: proxy isDefault: true jsonData: httpMethod: POST timeInterval: 30s persistence: type: pvc enabled: true storageClassName: local-path accessModes: - ReadWriteOnce size: 10Gi grafana.ini: paths: data: /var/lib/grafana/ logs: /var/log/grafana plugins: /var/lib/grafana/plugins provisioning: /etc/grafana/provisioning analytics: check_for_updates: true log: mode: console grafana_net: url: https://grafana.net users: default_theme: light security: allow_embedding: true auth.anonymous: enabled: true org_role: Viewer auth.basic: enabled: false server: root_url: http://localhost/test/grafana/ serve_from_sub_path: true kubeApiServer: enabled: true kubelet: enabled: true kubeControllerManager: enabled: true endpoints: - 172.20 .19 .51 - 172.20 .19 .52 - 172.20 .19 .53 service: port: 10257 targetPort: 10257 serviceMonitor: https: true insecureSkipVerify: true serverName: localhost coreDns: enabled: true kubeEtcd: enabled: true endpoints: - 172.20 .19 .51 - 172.20 .19 .52 - 172.20 .19 .53 service: port: 2379 targetPort: 2379 serviceMonitor: scheme: https insecureSkipVerify: true serverName: localhost caFile: /etc/prometheus/secrets/etcd-client-cert/etcd-ca certFile: /etc/prometheus/secrets/etcd-client-cert/etcd-client keyFile: /etc/prometheus/secrets/etcd-client-cert/etcd-client-key kubeScheduler: enabled: true endpoints: - 172.20 .19 .51 - 172.20 .19 .52 - 172.20 .19 .53 service: port: 10259 targetPort: 10259 serviceMonitor: https: true insecureSkipVerify: true kubeProxy: enabled: true endpoints: - 172.20 .19 .51 - 172.20 .19 .52 - 172.20 .19 .53 - 172.20 .19 .54 - 172.20 .19 .55 kubeStateMetrics: enabled: true kube-state-metrics: image: registry: easzlab.io.local:5000 repository: prometheus/kube-state-metrics prometheus-node-exporter: image: registry: easzlab.io.local:5000 repository: prometheus/node-exporter prometheusOperator: enabled: true admissionWebhooks: enabled: true patch: enabled: true image: registry: easzlab.io.local:5000 repository: prometheus/kube-webhook-certgen tag: v1.5.1 image: registry: easzlab.io.local:5000 repository: prometheus/prometheus-operator service: nodePort: 30899 nodePortTls: 30900 type: NodePort prometheusConfigReloader: image: registry: easzlab.io.local:5000 repository: prometheus/prometheus-config-reloader prometheus: enabled: true service: nodePort: 30901 type: NodePort prometheusSpec: image: registry: easzlab.io.local:5000 replicas: 1 secrets: - etcd-client-cert additionalScrapeConfigsSecret: enabled: true name: additional-scrape-configs key: prometheus-additional.yaml storageSpec: volumeClaimTemplate: spec: storageClassName: local-path accessModes: ["ReadWriteOnce" ] resources: requests: storage: 10Gi
确认无误后正式安装(若需要重装可先 uninstall):
1 2 3 4 5 /etc/kubeasz/bin/helm uninstall prometheus -n monitor /etc/kubeasz/bin/helm upgrade prometheus --install -n monitor \ -f /etc/kubeasz/clusters/k8s-01/yml/prom-values.yaml \ /etc/kubeasz/roles/cluster-addon/files/kube-prometheus-stack-45.23.0.tgz
3.2 部署验证 安装完成后,检查 monitoring(或自定义的 monitor)命名空间下的 Service 与 Pod 是否就绪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 [root@test-249 manifests]# kubectl get svc -n monitoring NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE alertmanager-main NodePort 10.56.182.15 <none> 9093:31714/TCP 12d alertmanager-operated ClusterIP None <none> 9093/TCP,9094/TCP,9094/UDP 12d blackbox-exporter ClusterIP 10.56.38.158 <none> 9115/TCP,19115/TCP 12d grafana NodePort 10.56.185.87 <none> 3000:31996/TCP 12d kube-state-metrics ClusterIP None <none> 8443/TCP,9443/TCP 12d node-exporter ClusterIP None <none> 9100/TCP 12d prometheus-adapter ClusterIP 10.56.34.10 <none> 443/TCP 12d prometheus-k8s NodePort 10.56.67.105 <none> 9090:31788/TCP 12d prometheus-operated ClusterIP None <none> 9090/TCP 12d prometheus-operator ClusterIP None <none> 8443/TCP 12d # 下面这段输出来自 kube-prometheus 原始 manifests 版(命名空间 monitoring、资源名 prometheus-k8s / alertmanager-main), # 与上面 Helm 装出来的那一套(release=prometheus、-n monitor、prometheus-kube-prometheus-prometheus)资源名和副本数都不同,仅作对照。 [root@test-249 manifests]# kubectl get pod -n monitoring NAME READY STATUS RESTARTS AGE alertmanager-main-0 2/2 Running 0 11d alertmanager-main-1 2/2 Running 0 11d alertmanager-main-2 2/2 Running 0 11d blackbox-exporter-556d889b47-khgck 3/3 Running 0 12d grafana-665447c488-245vl 1/1 Running 0 43h kube-state-metrics-6f4dfb9ffb-rj2df 3/3 Running 0 12d node-exporter-9tgm9 2/2 Running 0 12d node-exporter-gvjsr 2/2 Running 0 12d node-exporter-klprn 2/2 Running 0 12d node-exporter-rdkvx 2/2 Running 0 12d prometheus-adapter-767f58977c-2w9jn 1/1 Running 0 12d prometheus-k8s-0 2/2 Running 1 11d prometheus-k8s-1 2/2 Running 1 11d prometheus-operator-764cb46c94-8ds59 2/2 Running 0 12d
3.3 安装后的定制入口:Operator CRD 安装后主要通过以下 CRD 进行动态定制(无需重启,Operator 会自动 reload):
1 2 3 4 5 6 7 8 alertmanagerconfigs.monitoring.coreos.com alertmanagers.monitoring.coreos.com podmonitors.monitoring.coreos.com probes.monitoring.coreos.com prometheuses.monitoring.coreos.com prometheusrules.monitoring.coreos.com servicemonitors.monitoring.coreos.com thanosrulers.monitoring.coreos.com
此外还有各种 ConfigMap 和 Secret。以 Alertmanager 为例,动态追加的配置会与最初的文件合并,一起形成新的配置:
1 kubectl edit alertmanagerconfigs.monitoring.coreos.com -n monitor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: monitoring.coreos.com/v1alpha1 kind: AlertmanagerConfig metadata: labels: alertmanagerConfig: example name: config-example namespace: monitor spec: receivers: - name: webhook webhookConfigs: - url: http://example.com/ route: groupBy: - job groupInterval: 5m groupWait: 30s receiver: webhook repeatInterval: 12h
后文的服务发现(ServiceMonitor / PodMonitor / Probe)与告警(PrometheusRule)均通过这些 CRD 完成。注意 :这些自定义资源需要带上 release: prometheus 标签才能被 Prometheus 匹配,其选择器配置如下:
1 kubectl get prometheus -n monitor prometheus-kube-prometheus-prometheus -o yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 spec: podMonitorSelector: matchLabels: release: prometheus probeSelector: matchLabels: release: prometheus ruleSelector: matchLabels: release: prometheus serviceMonitorSelector: matchLabels: release: prometheus
4. PromQL 查询语言 4.1 向量与选择器
瞬时向量 :包含该时间序列中最新的一个样本值。
区间向量 :一段时间范围内的数据。
offset 用于查看多少时间之前的数据,例如 offset 30m。
标签(Labelsets)选择器:
1 2 3 4 5 6 7 8 9 10 11 # 过滤出具有 handler="/login" 的 label 的数据 http_request_total{handler="/login"} # 正则匹配 http_request_total{handler=~".*login.*"} # 剔除某个 label http_request_total{handler!~".*login.*"} # 匹配两个值 http_request_total{handler=~"/login|/password"}
4.2 运算符 数学运算:+ - * / % ^。例如查看主机内存总大小(Mi):
1 2 3 node_memory_MemTotal_bytes / 1024 / 1024 node_memory_MemTotal_bytes / 1024 / 1024 < 3000
集合运算:and、or、unless(排除):
1 2 3 node_memory_MemTotal_bytes / 1024 / 1024 <= 2772 or node_memory_MemTotal_bytes / 1024 / 1024 == 3758.59765625 node_memory_MemTotal_bytes / 1024 / 1024 >= 2772 unless node_memory_MemTotal_bytes / 1024 / 1024 == 3758.59765625
运算符优先级从高到低为:^(右结合)→ * / % atan2 → + - → == != <= < >= > → and unless → or。注意 and/unless 比 or 高,a and b or c 实际是 (a and b) or c。
4.3 聚合操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 求和 sum(node_memory_MemTotal_bytes) / 1024^2 # 根据某个字段进行统计 sum(http_request_total) by (statuscode, handler) # 最小值 / 最大值 / 平均值 min(node_memory_MemTotal_bytes) max(node_memory_MemTotal_bytes) avg(node_memory_MemTotal_bytes) # 标准差 stddev / 标准差异 stdvar stddev(node_memory_MemTotal_bytes) # 计数 count(http_request_total) # 对 value 进行统计计数 count_values("count", node_memory_MemTotal_bytes) # 取前 N 条 / 后 N 条时序 topk(5, sum(http_request_total) by (statuscode, handler)) bottomk(3, sum(http_request_total) by (statuscode, handler)) # 取中位数 quantile(0.5, http_request_total)
4.4 内置函数 指标的增长率相关函数:
1 2 3 # 某指标的增长量 / 增长率 increase(http_request_total{job="grafana"}[1h]) / 3600 rate(http_request_total{job="grafana"}[1h])
rate:区间内的平均增长率,适合观察长期趋势与告警规则;由于取平均,会有“长尾效应”。
irate:瞬时增长率,取最后两个数据点计算,不适合用于长期趋势分析或告警规则。
其他常用函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 预测统计:预测 4 小时后磁盘可用空间是否会小于 0 predict_linear(node_filesystem_avail_bytes{mountpoint="/",fstype!~"tmpfs"}[1h], 4*3600) < 0 # node_filesystem_files_free 里的 files 指的是 inode,拿它预测的是 inode 会不会耗尽,跟空间无关: predict_linear(node_filesystem_files_free{mountpoint="/"}[1d], 4*3600) < 0 # absent():判断数据是否在正常采集。样本不为空返回 no data,为空则返回 1 absent(up{job="node-exporter"}) # 去除小数点:ceil() 向上取整(2.79 -> 3),floor() 向下取整(2.79 -> 2) ceil(node_load1) floor(node_load1) # delta():差值,例如 free 内存差异 delta(node_memory_MemFree_bytes[2h]) # 排序:sort 正序,sort_desc 倒序 sort(node_memory_MemFree_bytes) # label_join:将一个或多个 label 的值拼接后赋值给新 label label_join(node_filesystem_files_free, "new_label", ",", "instance", "mountpoint") # label_replace:根据某个 label 值正则匹配后赋值给新 label label_replace(node_filesystem_files_free, "host", "$2", "instance", "(.*)-(.*)")
5. 数据采集:Exporter 与 Pushgateway 5.1 Exporter Exporter 需要满足以下要求:
是 HTTP 服务器,可以响应外部发来的 HTTP GET 请求。
后台运行,并可定期抓取本地的监控数据。
提供给 Prometheus Server 的内容需要符合 Prometheus 规定的 metrics 类型。
key-value 中 value 必须是浮点数或整数。
选型优先级:优先选择开源的 exporter,其次使用 Pushgateway,最后才考虑自己编写 exporter。
5.2 Pushgateway Pushgateway 适用于生命周期较短的 Job 类程序。下面的脚本统计本机 TCP WAIT 连接数并推送到 Pushgateway:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/bin/bash instance_name=`hostname -f | cut -d'.' -f1` if [ $instance_name == "localhost" ]; then echo "Must FQDN hostname" exit 1 fi label="count_netstat_wait_connections" count_netstat_wait_connections=`netstat -an | grep -i wait | wc -l` echo "$label : $count_netstat_wait_connections " echo "$label $count_netstat_wait_connections " | curl --data-binary @- http://prometheus.server.com:9091/metrics/job/pushgateway/instance/$instance_name
4.5 rate / irate / increase 的三个坑 这三个函数是 counter 类指标的全部日常用法,但它们有几个反直觉的行为,不知道就会怀疑数据出错。
一、increase 的结果不是整数。
1 increase(http_requests_total[5m]) # 可能返回 3.7
请求数怎么会是 3.7 个?因为这三个函数都做外插(extrapolation) :窗口的两个端点几乎不可能正好落在采样点上,Prometheus 会按已有采样点算出斜率,再把结果线性外推到窗口的完整长度。所以 increase 实际是”按这个速率,5 分钟应该涨多少”,而不是”我数到了几个”。
increase(x[5m]) 严格等价于 rate(x[5m]) * 300,只是换了个单位。真要精确计数就别用它,用 x - x offset 5m(不过这个写法遇到 counter reset 会算出负数)。
二、[range] 必须显著大于抓取间隔。
窗口内至少要有 2 个 采样点才能算出斜率,只有 1 个或 0 个就直接 no data。所以按 scrape_interval 的整数倍取窗口是不够的——抓取有抖动、偶尔失败一次,正好卡在 2 个点的边界上就会间歇性出现空洞(图上表现为断断续续的虚线)。
经验规则是 range ≥ 4 × scrape_interval :抓取间隔 15s 就用 [1m] 起步,30s 用 [2m]。留出的余量能容忍一两次抓取失败。
三、irate 不能用在告警里。
取样方式
适合
rate
窗口内所有 采样点做线性回归
告警、趋势图
irate
只取窗口内最后两个 采样点
看瞬时毛刺
irate 只看最后两个点,意味着它对窗口长度几乎不敏感、但对单次抖动极其敏感。配 for: 5m 的告警时,每次 evaluation 都可能正好落在尖峰或低谷上,结果就是告警反复 firing/resolved 抖动,或者真实的持续高负载因为某次 evaluation 落在低谷而被漏掉。
顺带说清一个容易叠错的延迟:从指标越界到收到通知,中间有四段延迟累加 。
1 2 3 4 5 6 7 8 抓取间隔 scrape_interval 指标本身的滞后 + 规则评估间隔 evaluation_interval 默认 30s,Prometheus 多久算一次表达式 + 持续时间 for: 5m 条件必须连续满足这么久才转为 firing + 分组等待 group_wait: 30s AlertManager 攒一批再发 = 端到端可能十分钟才收到第一条通知
同理,恢复通知(resolved)的延迟是 evaluation_interval + resolve_timeout + group_interval——这就是”问题早就好了,恢复通知半天不来”的原因。而 §3.1 里 send_resolved: false 又把恢复通知整个关掉了,两件事放一起看:恢复通知不来,先确认它是不是压根没开 。
6. 服务发现 使用 static_configs 静态定义监控目标不太方便。可以引入一个中间的代理人(服务注册中心),由它掌握当前所有监控目标的访问信息,Prometheus 只需向这个代理人询问有哪些监控目标即可,而不需要重启——这种模式被称为服务发现 。
6.0 先理解 relabel 的执行模型 下面几节全是 YAML,但不理解 relabel 怎么跑,配起来就只能靠抄。
服务发现返回的每个目标,都自带一堆以 __meta_ 开头的元数据标签(__meta_kubernetes_pod_annotation_*、__meta_kubernetes_service_label_* 等,具体有哪些取决于用的哪种 SD)。这些标签只存在于 relabel 阶段 ,抓取一开始就被丢弃,所以指标上是看不到它们的——想留下来就必须在 relabel 里显式复制到一个正常标签上。
还有几个”魔法标签”,它们的值直接决定最终发出的那个 HTTP 请求:
标签
决定什么
默认
__address__
抓取的 host:port
SD 给出的地址
__scheme__
http 还是 https
http
__metrics_path__
请求路径
/metrics
__param_<name>
URL 查询参数
无
所以”改抓取端口”的本质就是 relabel 出一个新的 __address__,”抓 https”就是改 __scheme__——不存在另外的专用配置项。
最容易搞混的是这两个阶段:
relabel_configs
metric_relabel_configs
时机
抓取之前
抓取之后
作用对象
目标(target)
抓回来的每条指标
能做什么
决定抓不抓、抓哪里、带什么标签
改标签、丢弃指标
典型用途
keep/drop 目标、labelmap 注解
治理高基数 :把爆炸的指标丢掉
治高基数只能用后者——目标已经抓回来了,是里面某个 label(比如带 URL 路径或用户 ID 的)取值太多,这时候 relabel_configs 帮不上,得在 metric_relabel_configs 里 drop:
1 2 3 4 5 6 metric_relabel_configs: - source_labels: [__name__ ] regex: 'go_gc_duration_seconds.*' action: drop - regex: 'id|image_id' action: labeldrop
常用 action 的取舍:
keep / drop:白名单 / 黑名单选目标。注解驱动的自动发现全靠 keep (见 6.2)。
replace:改写标签,最常用。
labelmap:批量把 __meta_xxx_(.+) 映射成 $1,一次搬走所有注解或标签。
hashmod + keep:分片抓取 。目标太多单实例扛不住时,起 N 个 Prometheus,每个只抓自己那一份:
1 2 3 4 5 6 7 - source_labels: [__address__ ] modulus: 4 target_label: __tmp_shard action: hashmod - source_labels: [__tmp_shard ] regex: 0 action: keep
最后一步能把 CRD 和原生配置的心智模型彻底打通:ServiceMonitor 最终会被 Operator 渲染成普通的 scrape_configs ,可以直接解出来看:
1 2 kubectl get secret prometheus-<name> -n monitor \ -o jsonpath='{.data.prometheus\.yaml\.gz}' | base64 -d | gunzip | less
看过一次就明白:ServiceMonitor 不是什么新机制,只是一套帮你生成 relabel_configs 的模板。之后遇到”target 为空”这类问题,直接在这里搜自己的 job 名字,看渲染出来的规则哪一条把目标 drop 掉了,比对着 CRD 猜快得多。
6.1 基于文件的服务发现 这种方式适用于非 Kubernetes 环境或独立部署的 Prometheus。编辑 /etc/prometheus/prometheus.yml,设置每两分钟基于文件更新一次目标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 global: scrape_timeout: 10s scrape_interval: 15s evaluation_interval: 15s alerting: alertmanagers: - static_configs: - targets: rule_files: scrape_configs: - job_name: 'prometheus' file_sd_configs: - files: - targets/prometheus-*.yaml refresh_interval: 2m - job_name: 'nodes' file_sd_configs: - files: - targets/nodes-*.yaml refresh_interval: 2m
新建目标文件后,Prometheus 会自动更新,无需重启。
/etc/prometheus/targets/nodes-linux.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 - targets: - 10.0 .10 .192 :9100 - 10.0 .10 .141 :9100 labels: app: node-exporter job: node env: other - targets: - localhost:9100 labels: app: node-exporter job: node env: main
/etc/prometheus/targets/prometheus-server.yaml(文件名必须能被上面的 prometheus-*.yaml 匹配到;叫 prometheus.yaml 是匹配不上的,而 Prometheus 对匹配不到文件的 file_sd 不报错 ,只在 targets 页面空着,很难查):
1 2 3 4 5 - targets: - localhost:9090 labels: app: prometheus job: prometheus
6.2 基于注解的自动发现(不推荐) prometheus.io/scrape 这类注解本身没有任何内建语义 ,Prometheus 不会因为你打了它就自动抓取——要靠下面那份 relabel_configs 显式把注解翻译成抓取规则才生效。但这种方式配置起来不方便,一般更推荐使用 ServiceMonitor 和 PodMonitor。
被发现的 Service 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: v1 kind: Service metadata: annotations: prometheus.io/port: "80" prometheus.io/scrape: "true" labels: app: test-frontend-250 name: test-frontend-250-svc namespace: default spec: ports: - name: test-frontend-250-http nodePort: 31250 port: 80 targetPort: 80 selector: app: test-frontend-250 type: NodePort
对应的额外抓取配置 prometheus-additional.yaml(配合 §3.1 中 additionalScrapeConfigsSecret 使用):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 - job_name: 'kubernetes-service-endpoints' kubernetes_sd_configs: - role: endpoints relabel_configs: - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape ] action: keep regex: true - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme ] action: replace target_label: __scheme__ regex: (https?) - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path ] action: replace target_label: __metrics_path__ regex: (.+) - source_labels: [__address__ , __meta_kubernetes_service_annotation_prometheus_io_port ] action: replace target_label: __address__ regex: ([^:]+)(?::\d+)?;(\d+) replacement: $1:$2 - action: labelmap regex: __meta_kubernetes_service_label_(.+) - source_labels: [__meta_kubernetes_namespace ] action: replace target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_service_name ] action: replace target_label: kubernetes_name
将其创建为 Secret,并让 Prometheus 引用:
1 2 3 kubectl create secret generic additional-scrape-configs --from-file=prometheus-additional.yaml -n monitor kubectl edit prometheus -n monitor
1 2 3 4 spec: additionalScrapeConfigs: name: additional-scrape-configs key: prometheus-additional.yaml
6.3 ServiceMonitor、PodMonitor 与 Probe 在 Operator 环境下,推荐使用以下三种 CRD 声明抓取目标(均需带 release: prometheus 标签,见 §3.3):
ServiceMonitor :针对 Service 发现。
PodMonitor :针对 Pod 发现。
Probe :一般用于自定义/黑盒探测。
ServiceMonitor(以 Redis 为例) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: release: prometheus name: redis namespace: redis spec: endpoints: - interval: 30s port: http-metrics namespaceSelector: matchNames: - redis selector: matchLabels: app.kubernetes.io/component: metrics app.kubernetes.io/instance: redis app.kubernetes.io/name: redis
ServiceMonitor 的 selector 匹配的是 Service 的 metadata.labels(注意不是 spec.selector,那个是给 Service 选 Pod 用的,两回事),endpoints.port 对应 Service 中的端口名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/component: metrics app.kubernetes.io/instance: redis app.kubernetes.io/name: redis name: redis-metrics namespace: redis spec: ports: - name: http-metrics port: 9121 protocol: TCP targetPort: metrics selector: app.kubernetes.io/instance: redis app.kubernetes.io/name: redis
PodMonitor(以 PostgreSQL 主备为例) 当 Pod 标签中带有主备(master/replica)标识时,针对 Pod 发现更方便,还可以通过 relabelings 改写 label 以复用官方的告警与面板规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: name: crunchy-postgres-exporter-master namespace: monitor labels: release: prometheus spec: podMetricsEndpoints: - interval: 30s port: exporter path: /metrics relabelings: - replacement: master targetLabel: role - replacement: postgres-operator:test targetLabel: pg_cluster - replacement: test targetLabel: cluster - sourceLabels: [pod ] targetLabel: deployment regex: (.*?)-\d+ replacement: $1 action: replace - sourceLabels: [__meta_kubernetes_pod_ip ] targetLabel: ip action: replace - sourceLabels: [job ] targetLabel: job regex: monitor/(.*)-(master|replica) replacement: $1 action: replace - sourceLabels: [namespace ] targetLabel: kubernetes_namespace action: replace namespaceSelector: matchNames: - postgres-operator selector: matchExpressions: - key: postgres-operator.crunchydata.com/role operator: In values: [master ] --- apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: name: crunchy-postgres-exporter-replica namespace: monitor labels: release: prometheus spec: podMetricsEndpoints: - interval: 30s port: exporter path: /metrics relabelings: - replacement: replica targetLabel: role - replacement: postgres-operator:test targetLabel: pg_cluster - replacement: test targetLabel: cluster - sourceLabels: [pod ] targetLabel: deployment regex: (.*?)-\d+ replacement: $1 action: replace - sourceLabels: [__meta_kubernetes_pod_ip ] targetLabel: ip action: replace - sourceLabels: [job ] targetLabel: job regex: monitor/(.*)-(master|replica) replacement: $1 action: replace - sourceLabels: [namespace ] targetLabel: kubernetes_namespace action: replace namespaceSelector: matchNames: - postgres-operator selector: matchExpressions: - key: postgres-operator.crunchydata.com/role operator: In values: [replica ]
Probe(黑盒探测) Probe 借助 blackbox-exporter 对目标进行探测,需要先部署 blackbox-exporter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: monitoring.coreos.com/v1 kind: Probe metadata: name: ping namespace: monitor labels: release: prometheus spec: jobName: ping prober: url: blackbox-exporter.monitor:19115 module: icmp targets: staticConfig: static: - www.baidu.com
7. 告警 7.1 告警规则(PrometheusRule) 在 Operator 环境下,告警规则通过 PrometheusRule CRD 定义(同样需要带 release: prometheus 标签)。以下是主机层面的常用规则(主机宕机、CPU、内存、磁盘、TCP 连接数):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 groups: - name: host_status_alert rules: - alert: 主机宕机 annotations: description: 主机{{ $labels.instance }}宕机超过1分钟告警! summary: 主机{{ $labels.instance }}宕机 expr: up{job="node-exporter"} == 0 for: 1m labels: severity: critical - alert: CPU使用过高 annotations: description: 主机{{ $labels.instance }}当前CPU使用率是{{ $value }}%,CPU使用率大于60%告警! summary: 主机{{ $labels.instance }}CPU使用率过高 expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 ) > 60 for: 5m labels: severity: warning - alert: 内存使用过高 annotations: description: 主机{{ $labels.instance }}当前内存使用率是{{ $value }}%,内存使用率大于80%告警! summary: 主机{{ $labels.instance }}内存使用率过高 expr: ((node_memory_MemTotal_bytes - node_memory_MemFree_bytes - node_memory_Buffers_bytes - node_memory_Cached_bytes) / (node_memory_MemTotal_bytes)) * 100 > 80 for: 5m labels: severity: warning - alert: 磁盘空间剩余不足 annotations: description: 主机{{ $labels.instance }}的磁盘{{ $labels.mountpoint }}当前使用率是{{ $value }},磁盘使用率大于80%告警! summary: 主机{{ $labels.instance }}的磁盘{{ $labels.mountpoint }}可用空间不足 expr: (1-(node_filesystem_free_bytes{fstype=~"ext4|xfs|ceph",mountpoint!~"/var.*"} / node_filesystem_size_bytes{fstype=~"ext4|xfs|ceph",mountpoint!~"/var.*"})) * 100 > 80 for: 5m labels: severity: warning - alert: TCP连接数过多 annotations: description: 主机{{ $labels.instance }}当前TCP_ESTABLISHED={{ $value }},TCP连接数大于1000告警! summary: 主机{{ $labels.instance }}TCP连接数过多 expr: node_netstat_Tcp_CurrEstab > 1000 for: 5m labels: severity: page
对于业务组件(如 PostgreSQL),开源项目通常会自带一份 ConfigMap 形式的告警规则,只需将其改写成 PrometheusRule 并 apply 即可:
1 kubectl apply -f prometheusrule-postgres.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: labels: release: prometheus name: postgres-operator.rule namespace: monitor spec: groups: - name: postgres-operator-rule rules: - alert: PGExporterScrapeError expr: pg_exporter_last_scrape_error > 0 for: 60s labels: service: postgresql severity: critical severity_num: 300 annotations: summary: 'Postgres Exporter running on {{ $labels.job }} (instance: {{ $labels.instance }}) is encountering scrape errors processing queries. Error count: ( {{ $value }} )' - alert: ExporterDown expr: avg_over_time(up[5m]) < 0.5 for: 10s labels: service: system severity: critical severity_num: 300 annotations: description: 'Metrics exporter service for {{ $labels.job }} running on {{ $labels.instance }} has been down at least 50% of the time for the last 5 minutes. Service may be flapping or down.' summary: 'Prometheus Exporter Service Down'
7.1.1 别忘了 recording rule PrometheusRule 里能写两种规则,上面只用了 alert,另一种是 record:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule spec: groups: - name: node-recording interval: 30s rules: - record: instance:node_cpu_utilisation:rate5m expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 ) - alert: NodeCPUHigh expr: instance:node_cpu_utilisation:rate5m > 80 for: 10m
为什么值得多这一层:
一、成本。 复杂表达式(多层聚合、大范围 rate、高基数 join)每次被求值都要重算一遍。写成 recording rule 之后只在 interval 里算一次,Grafana 面板、多条告警规则、其他 recording rule 都直接读现成的结果。一个被十个面板引用的重表达式,能省掉九次计算。
二、Grafana 的响应速度。 看一个月的趋势图时,原始表达式要在查询时扫过一个月的原始数据;而 recording rule 产出的是一条已经降过采样的序列,扫描量小一到两个数量级。仪表盘从十几秒变成瞬间打开,多半就是这个差别。
三、表达式只写一遍。 上面那个 CPU 利用率的算法一旦要改(比如把 mode="idle" 换成排除 steal),改一处就够,不用去十个告警规则里逐个找。
命名有个业界约定,level:metric:operations,用冒号分三段:
1 2 3 4 instance:node_cpu_utilisation:rate5m │ │ └─ 做了什么运算 │ └─ 原始指标名 └─ 聚合到哪个层级(instance / job / namespace ...)
冒号在 PromQL 里不允许 出现在原始指标名中,所以看到带冒号的指标名就知道它是 recording rule 产出的——这个约定本身就是一种自文档。
7.2 AlertManager 路由与抑制 AlertManager 的抑制规则(inhibit_rules)、路由(route)与接收器(receivers)已在 §3.1 的 prom-values.yaml 中 alertmanager.config 段落给出:通过 inhibit_rules 让 critical 告警抑制同一 namespace/alertname 下的 warning/info,通过 route 按 namespace 分组并将不同 severity 路由到不同的接收器。如需在运行时按命名空间动态追加路由,可使用 §3.3 中的 AlertmanagerConfig CRD,它会与基础配置合并。
7.3 对接企业微信 / 钉钉机器人 §3.1 中接收器 web.hook 的地址 http://prometheus-webhook-adapter:8060/adapter/wx 指向一个 webhook 适配器,用于把告警转发到企业微信(钉钉同理)。部署适配器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 apiVersion: apps/v1 kind: Deployment metadata: labels: run: prometheus-webhook-adapter name: prometheus-webhook-adapter namespace: monitor spec: selector: matchLabels: run: prometheus-webhook-adapter template: metadata: labels: run: prometheus-webhook-adapter spec: containers: - args: - --adapter=/app/prometheusalert/dingtalk.js=/adapter/dingtalk=https://oapi.dingtalk.com/robot/send?access_token={token}#{secret} - --adapter=/app/prometheusalert/wx.js=/adapter/wx=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxxxxxxxxxxxxxxxxxx image: imwl/prometheus-webhook-adapter:v1 name: prometheus-webhook-adapter ports: - containerPort: 80 protocol: TCP --- apiVersion: v1 kind: Service metadata: labels: run: prometheus-webhook-adapter name: prometheus-webhook-adapter namespace: monitor spec: ports: - port: 8060 protocol: TCP targetPort: 80 selector: run: prometheus-webhook-adapter type: ClusterIP
其中 dingtalk 的 {token}#{secret} 与 wx 的 key=xxxx 需替换为自己机器人的实际值。
8. 个性化定制(Grafana) 8.1 数据源、Dashboard 与运行时修改 Grafana 的数据源、grafana.ini 等已通过 §3.1 的 prom-values.yaml 中 grafana 段落声明。若部署后网页提示 datasource 找不到,可用 admin 账户手动添加数据源:http://prometheus-kube-prometheus-prometheus:9090。
导入 Dashboard :开源组件通常自带 Dashboard 模板(也可从 grafana.com 下载或自行配置)。导入前需要把 JSON 文件里的 "datasource": "PROMETHEUS" 替换为 "datasource": "Prometheus":
1 2 3 4 find . -type f -exec sed -i 's/"datasource": "PROMETHEUS"/"datasource": "Prometheus"/g' {} \; kubectl -n monitor create cm grafana-postgres-overview --from-file=./
然后给 ConfigMap 打上 label,才能被 Grafana 的 sidecar 动态识别(默认 grafana_dashboard=1):
1 kubectl -n monitor label cm grafana-postgres-overview grafana_dashboard=1
sidecar 的 provider 配置如下(决定 Dashboard 的加载路径):
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: 1 providers: - name: 'sidecarProvider' orgId: 1 folder: '' type: file disableDeletion: false allowUiUpdates: false updateIntervalSeconds: 30 options: foldersFromFilesStructure: false path: /tmp/dashboards
运行时修改 :部署完成后也可以直接 kubectl edit cm -n monitor prometheus-grafana 调整(配置项与 prom-values.yaml 中的 grafana.grafana.ini 一致,只是运行时以 INI 格式呈现)。
8.2 开启 OIDC 单点登录 编辑 Grafana 的 grafana.ini(kubectl edit cm -n monitor prometheus-grafana,release 叫 prometheus 时 cm 就是这个名字),配置 auth.generic_oauth。
注意 grafana.ini 改完 sidecar 不会热加载 (它只监听 dashboard 和 datasource 目录),必须 kubectl rollout restart deploy/prometheus-grafana 重启 Pod;而且直接 edit cm 的改动会在下次 helm upgrade 时被覆盖,长期改动应该落到 values 里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [server] protocol = httpdomain = grafana.test.comroot_url = https://grafana.test.com/[auth.generic_oauth] enabled = true allow_sign_up = true auto_login = false client_id = app_xxxxxclient_secret = <YOUR_SECRET_KEY>scopes = openid profile emailauth_url = https://xxxx.aliyunidaas.com/login/app/app_xxxxx/oauth2/authorizetoken_url = https://eiam-api-cn-hangzhou.aliyuncs.com/v2/idaas_xxxx/app_xxxxx/oauth2/tokenapi_url = https://eiam-api-cn-hangzhou.aliyuncs.com/v2/idaas_xxxx/app_xxxxx/oauth2/userinforedirect_uri = https://grafana.test.com/login/generic_oauthemail_attribute_path = emailrole_attribute_path = contains(email, 'admin@' ) && 'Admin' || endsWith(email, '@example.com' ) && 'Editor' || 'Viewer'
说明:client_secret、client_id、auth_url 等均为占位/脱敏值,请替换为自己 IdP 的实际配置;role_attribute_path 用于根据邮箱把用户映射为 Admin / Editor / Viewer 角色。
9. 常见监控问题排查 9.1 CPUThrottlingHigh CPUThrottlingHigh 反映的是最近 5 分钟内超过 25% 的 CPU 执行周期受到限制的 container,一般是 limit 设置得过低引起的。它通过两个指标计算:
container_cpu_cfs_periods_total:container 生命周期中度过的 CPU 周期总数。
container_cpu_cfs_throttled_periods_total:container 生命周期中度过的受限的 CPU 周期总数。
计算表达式:
1 2 sum by(container, pod, namespace) (increase(container_cpu_cfs_throttled_periods_total{container!=""}[5m])) / sum by(container, pod, namespace) (increase(container_cpu_cfs_periods_total[5m])) > (25 / 100)
9.2 kube-scheduler / kube-controller-manager 无监控数据 在 Operator 环境下,控制平面组件的抓取由 §3.1 中的 kubeControllerManager、kubeScheduler 等配置负责(新版集群使用 10257/10259 等 https 端口)。
对于非 Operator 环境或旧版集群(controller-manager 使用 10252 http 端口),若采集不到数据,可以手动创建 Service + Endpoints 把控制平面组件暴露给 Prometheus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 apiVersion: v1 kind: Service metadata: labels: k8s-app: kube-controller-manager name: kube-controller-manage-monitor namespace: kube-system spec: clusterIP: 10.96 .23 .157 ports: - name: http-metrics port: 10252 protocol: TCP targetPort: 10252 type: ClusterIP --- apiVersion: v1 kind: Endpoints metadata: labels: k8s-app: kube-controller-manager name: kube-controller-manage-monitor namespace: kube-system subsets: - addresses: - ip: 192.168 .1 .19 ports: - name: http-metrics port: 10252 protocol: TCP