前阵子同事接了一个新项目集群的交接,第一件事就是问我:k8s里监控怎么搭?他说以前在物理机上用的那套Agent采集方案,搬到Kubernetes之后完全失灵了——Pod一重启IP就变,告警目标列表根本维护不过来。
这几乎是每个刚从传统架构转过来的运维都会撞上的墙。K8s里的监控不是把原来的Agent塞进YAML里就行,它需要一套能自动发现目标、能随Pod生命周期动态变化的采集体系。而在社区里沉淀最多、踩坑最少、资料最全的方案,就是Prometheus + Grafana这套组合。
我想借这篇内容把k8s部署prometheus + grafana这件事完整拆开来讲:不是给你一份照抄就结束的安装命令,而是把这个方案在K8s里的核心链路、配置逻辑、以及我实际部署过程中替大家踩过的坑一次讲清楚。不管你是在测试环境先验证,还是准备推到生产,照着这个思路走,能省下不少冤枉时间。
1. 先想清楚:K8s里的监控到底难在哪
1.1 Pod的“短命”让老一套监控直接失效
传统监控的思路是先把机器列表捋清楚,然后在每台机器上装Agent,Agent把CPU、内存、磁盘这些指标持续上报到中心端。这个模式在机器数量稳定、IP固定的环境里非常成熟,维护成本也低。但K8s里的工作负载完全不是这个逻辑:一个Deployment的副本数可以从3个伸缩到30个,Pod随时会被重新调度到另一个节点,Service背后的Endpoints列表每秒钟都可能变化。
更麻烦的是,很多监控指标藏在Pod内部。比如某个业务容器的请求数、某个中间件的连接数,这类指标靠外部Agent很难采集,必须在业务进程内部把数据暴露出来。给一个会随时销毁重建的Pod装Agent,本身就是一个维护噩梦:你没法预先把Agent装到还没创建的Pod里,所以只能在镜像里预置,但又不是所有镜像都适合往里面塞东西。
这里有个很形象的类比:如果把传统配置Agent比作按通讯录打电话,那么K8s里的监控对象更像一个不断换号的群体——你得有办法动态获知“现在该联系谁”,而不是死守一张固定的名单。这也是为什么很多人把Zabbix、Netdata或者其他老方案硬搬到K8s之后,会觉得处处别扭:它们解决问题的前提假设“监控对象相对稳定”在K8s里不成立了。
1.2 Pull模型 + 服务发现:Prometheus解决问题的姿势
Prometheus的处理方式非常朴素:我不注册、不登记,定时过去拉。Prometheus服务器按固定周期(默认30秒)访问一批目标端点,每个端点返回符合OpenMetrics格式的指标文本,这就是Pull模型。相比Agent主动上报的Push模型,Pull模型有几个很实用的好处:被监控端不需要额外安装什么通信组件,只要暴露一个HTTP端口就行;排查问题时可以直接拿curl去看这个端点返回什么;Prometheus作为抓取方,对抓取频率、超时、重试有完全的控制权。
配合Pull模型的是服务发现机制。Prometheus可以对接Kubernetes API,动态获取集群里所有Service、Pod、Node等对象的列表,再根据配置的label selector筛选出要抓取的目标。新扩容一个Pod,只要它暴露了正确的metrics端口,无需人工注册,下一轮抓取就能自动出现在Targets列表里。这种随集群状态自适应的能力,才是K8s监控真正需要的。
K8s生态选择Prometheus作为监控标准不是偶然。正因为这套“自动发现+主动拉取”的架构天然适应Pod的动态调度,再加上CNCF背景下的社区推动,它已经成为K8s监控事实上的默认选项。
2. 选型这步走错了后面全白搭:Helm一键部署与手写清单的取舍
2.1 我为什么最终选了Helm Chart
部署Prometheus到K8s有很多种方式,最省力的是直接用Helm官方Chart。社区维护的kube-prometheus-stack把Prometheus、Grafana、Alertmanager、NodeExporter、kube-state-metrics以及一大堆默认告警规则全部打包成了一张编排清单。装一次就能把能想到的监控组件都带齐,不用自己一个个抠YAML。
我一开始也觉得“全家桶”太重,想自己搞一个精简版。后来在某个内部项目里试过手写一整套,发现光是调通RBAC权限、ServiceMonitor、各种Service关联关系就花了大半天。改用Helm Chart之后,这些问题大部分被Chart自身的默认值处理好了,我只需要关心values.yaml里需要覆盖的少数配置。
实际命令也很简单,先添加仓库再安装:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update kubectl create ns monitoring helm install monitor prometheus-community/kube-prometheus-stack \ -n monitoring \ -f values.yaml注意我创建命名空间用的是kubectl create ns而不是在Helm命令里带--create-namespace,主要是我习惯安装前先确认命名空间存在、并且知道它里面已经有什么东西。这个习惯在维护多套环境时挺重要,避免Helm悄悄建了空间导致后面找资源时漏掉。
2.2 手写YAML适合什么场景
如果环境网络受限、无法访问Helm仓库,或者你只需要Prometheus单组件、不想被Grafana等组件绑定,那手写YAML也是一条路。手写的好处是每个资源对象都看得见摸得着,适合教学、适合离线环境、适合对组件数量有严格控制的项目。
但代价是维护成本高。Prometheus的部署涉及Namespace、ServiceAccount、RBAC、StatefulSet/Deployment、ConfigMap、Service、Ingress等十几种资源,每个组件都是一套。一旦K8s版本升级导致部分API变化,所有清单都要跟着过一遍。我见过有人从网上手工找了一套版本很老的清单直接apply到新版集群,服务起不来才回头排查兼容性问题,那种排查真的很耗时间。
所以我的建议是:除非有明确的隔离或合规要求,否则优先用Chart。手写方案更适合作为学习手段,而不是长期生产维护的方式。
2.3 版本兼容矩阵:这是最容易被忽视的
K8s版本和Chart版本的兼容性是选型时第一个要确认的点。Kubernetes每代版本会调整一部分API,比如某些资源的group从v1beta1升到v1,旧清单在新集群上无法工作。kube-prometheus-stack本身对K8s版本有要求,升级Chart时如果不看兼容区间,很容易出现装了最新Chart、集群却太老的情况。
建议去Chart仓库的README里确认它支持的K8s版本区间,再对照自己的集群版本选择Chart版本。另外要注意,Grafana和Prometheus这两个组件对版本敏感度不太一样,Grafana面板和数据源插件的兼容性通常更宽松,但Prometheus的存储格式和抓取配置在某些大版本升级时会有迁移要求。升级前做好配置备份,比什么都重要。
3. 让Prometheus“看得见”集群:ServiceMonitor与指标抓取链路的拆解
3.1 抓取链路全景:从ServiceMonitor到Exporter
如果你用的是kube-prometheus-stack,会发现Prometheus并不是直接抓某一个Pod的IP,而是通过一个叫ServiceMonitor的自定义资源来声明“我要抓什么”。这个CRD是Prometheus Operator引入的,也是整个K8s监控体系里最核心的一个概念。
完整链路是这样的:业务服务创建一个Service,指向暴露metrics端口的Pod;在同一个命名空间下创建一个ServiceMonitor,通过selector选择刚才那个Service;Prometheus启动时读取自己配置里的serviceMonitorSelector,匹配到这些ServiceMonitor后生成抓取目标列表;Prometheus定期访问Service的地址,由Service转发到后端Pod的metrics端口完成采集。
这个设计把“目标发现”和“目标管理”解耦了。开发团队只需要维护自己的ServiceMonitor,不需要去改Prometheus的全局配置。从平台角度说,你给业务团队开放一个ServiceMonitor的写入权限,他们就能自助接入监控,而不会碰坏全局抓取配置。这也是Operator模式在监控领域最大的价值。
3.2 标签匹配与服务发现的几个勘误
ServiceMonitor的selector是整个环节里最容易出bug的地方。很多人发现Targets页面一直没有新目标出现,问题大多出在标签不一致上。
我举一个实际的例子:Service的标签是app: my-app,而ServiceMonitor的selector写成release: monitor,两者永远匹配不上。正确做法是让ServiceMonitor的selector去匹配Service的标签,而不是自己想当然的标签。写错之后最典型的现象是:Pod已经起来了,metrics端口也通了,但Prometheus就是抓不到。
是命名空间的问题也经常遇到:ServiceMonitor默认只能发现同命名空间内的Service,如果目标服务在别的命名空间,要么在该命名空间也创建一个ServiceMonitor,要么用namespaceSelector的any: true做跨命名空间选择。后者会有一定的权限和资源过滤上的副作用,实际生产里我更喜欢在每个业务命名空间各放一个ServiceMonitor,保持职责清晰。
排查时最有用的三条命令是:
kubectl get servicemonitor -n <namespace> kubectl get svc -n <namespace> --show-labels kubectl describe servicemonitor <name> -n <namespace>然后到Prometheus的Targets页面看每个目标的状态和Discovery标签,基本就能定位是selector不匹配、端口不对还是路径不对。
3.3 NodeExporter与kube-state-metrics各司其职
组件多了以后容易混淆这几个采集器的分工。NodeExporter负责采集节点操作系统层面的指标,比如CPU、内存、磁盘IO、网络;kube-state-metrics则从Kubernetes API角度生成资源对象的状态指标,比如Deployment期望副本数、Pod当前状态、Service的selector情况。
| 采集器 | 数据来源 | 典型指标 | 适合回答的问题 |
|---|---|---|---|
| NodeExporter | 宿主机/Node | node_cpu_seconds_total、node_memory_MemTotal_bytes | 节点资源是否够用 |
| kube-state-metrics | Kubernetes API | kube_deployment_spec_replicas、kube_pod_container_status_restarts_total | 应用是否按预期运行 |
| cAdvisor(kubelet内置) | 容器运行时 | container_cpu_usage_seconds_total、container_memory_usage_bytes | 单个容器到底吃了多少 |
这三个一起配合,才能回答“节点没爆但应用挂了”这类常见问题。比如一个Pod反复重启,你在NodeExporter看到的CPU和内存都很正常,但看kube-state-metrics的Pod重启次数指标就会发现异常;再进一步用cAdvisor的容器指标看单容器资源用量,就能定位是进程内存泄漏还是被OOMKilled。离开任何一层,排查都不完整。
4. Grafana接入:把监控数据变成能看懂的图表
4.1 数据源配置的几个细节
在kube-prometheus-stack里,Grafana的数据源通常已经预置好了,它会自动往Prometheus的Service地址注册一个数据源。但如果你是分开部署的,手动配置时最容易犯的错就是把数据源URL填成localhost。
在K8s集群里,Grafana Pod和Prometheus Pod不在同一进程空间,“localhost”指的是Grafana Pod自己,不是Prometheus。正确写法取决于Service的命名和命名空间,一般是类似这样的集群内地址:
http://<prometheus-service-name>.<namespace>:9090如果Prometheus以StatefulSet方式部署,Service名通常叫prometheus-operated或类似格式。配置数据源还有两个容易被忽略的选项:一个是Access模式要选“Server”,让Grafana服务器去访问Prometheus,而不是浏览器直接访问;另一个是如果Prometheus前面挂了认证或TLS,需要在数据源里对应配置。
4.2 仪表盘导入:别急着找最新版
Grafana生态最大的好处是现成的Dashboard可以导入。社区里常用的几个面板我用下来最顺手:Node Exporter Full这类节点资源看板很直观,K8s集群视图类面板能把集群大致状态一眼扫清楚。导入方式很简单,在Dashboards页面点Import,输入一个数字ID或者上传JSON文件就行。
这里有个经验:不要看到新面板就导入,先看它的数据源是否匹配、依赖的标签名称和你的环境是否一致。很多面板在别人环境里显示正常,导过来一片空,绝大多数是因为标签名不匹配。比如面板里写的是instance,而你的环境实际输出的是host标签,图表自然就是空的。
另一个经验是版本问题。有些Dashboard的JSON是为特定Grafana版本写的,老版本Grafana可能不支持新面板类型,导入时报错。所以如果团队对Grafana版本有强管控,导入面板前最好先确认面板要求的版本范围,不然就是白白花时间调半天。
4.3 自定义一个简单面板的流程
不管现成面板多丰富,总有某些指标需要自己画。以最常见的CPU使用率为例,PromQL可以这样写:
100 - (avg(irate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance) * 100)这个表达式的意思:先求每台节点5分钟内空闲CPU秒数的变化率,按节点聚合后整体里减掉,得到的是各节点的CPU使用率。新建面板时选时间序列图,查询选择刚写的PromQL,单位设置为percent(0-100),图例里显示instance标签,一张节点CPU使用率面板就出来了。
自定义面板这件事做几次之后,你对PromQL的理解会比看十篇教程都深。我个人的做法是先找现成面板里的查询语句,读懂它,再改标签或者时间范围,慢慢就掌握了一套属于自己的查询习惯。
5. 忘掉告警等于白监控:Alertmanager与告警规则落地
5.1 PrometheusRule的语义与热加载
指标有了、图表有了,还差最后一道闭环:告警。Prometheus自己负责计算和触发告警,但真正把消息发出去的是Alertmanager这个组件。
在kube-prometheus-stack里,告警规则同样通过自定义资源来管理,也就是PrometheusRule。一个PrometheusRule里可以声明若干组规则,每组规则的核心字段包括:
- expr:PromQL表达式,满足条件时触发告警;
- for:条件持续多久才触发,避免瞬时抖动造成误报;
- labels:给告警附加标签,后续路由会用到;
- annotations:告警内容文本,包括标题、描述、处理建议。
Prometheus会周期性重新加载规则文件,改完规则不需要重启服务,过几十秒就会自动生效。这在实际值班时很实用,临时调整告警阈值不用走发布流程。
一条规则示例:
apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: name: example-rule namespace: monitoring spec: groups: - name: app.rules rules: - alert: TargetDown expr: up == 0 for: 1m labels: severity: critical annotations: summary: "{{ $labels.job }} 的抓取目标已下线" description: "实例 {{ $labels.instance }} 已超过1分钟无法抓取"5.2 告警路由:让通知找到对的人
Alertmanager的route配置是整个告警体系的调度中枢。它按照标签匹配,把不同告警分发给不同接收者。例如按severity区分:critical的告警走紧急通道,warning的走普通通道;按alertname分组,避免同一类告警刷屏。
route是一个树形结构,顶级路由下面可以有子路由,每级路由都可以定义continue、receiver、group_by等参数。实际场景举例:把基础设施相关告警发给运维值班渠道,把业务应用告警发给业务值班渠道。这个用父路由匹配团队标签、子路由匹配告警级别就能完成。
receiver这块,最常用的是email、webhook两种。Webhook的用途很广,可以接到办公软件的群机器人里,也可以接到内部的工单系统。实际配置时需要注意:如果webhook接口响应慢或者返回错误,Alertmanager会按重试策略不断重发,接收方需要做好幂等处理。
5.3 告警从触发到恢复的完整生命周期
一条告警从产生到恢复,通常会经历Pending、Firing和Resolved三个阶段。Pending表示PromQL条件已经满足,正在等待for的持续时间验证;一旦超过for指定的时间条件仍然成立,告警进入Firing并送入Alertmanager;当表达式不再成立时,Prometheus自动发送Resolved通知表示恢复。
了解这个生命周期有助于设置合理的for值。for设得太短,一个突发的CPU尖峰就会被当成告警轰炸;设得太长,小故障会拖很久才被发现。对大多数业务指标,1到3分钟的for是一个比较合理的起点。告警规则数量多起来之后,还要注意抑制规则(inhibit_rules)的合理配置。比如某个节点宕机,会连带触发一堆Pod级别的告警,这时如果有抑制规则自动屏蔽衍生告警,值班同学的手机就能清净很多。
6. 这些坑我替你们踩过了:存储、资源配额与版本兼容
6.1 默认存储是emptyDir:重启即失忆
kube-prometheus-stack安装完成后,如果没做任何持久化配置,Prometheus和Alertmanager的数据都放在emptyDir里。emptyDir的生命周期跟随Pod,Pod一旦重启或迁移到其他节点,历史数据全部清空。这在测试环境无所谓,生产环境是不能接受的。
生产环境必须配置持久化存储。以StorageClass为例,在values.yaml里指定Prometheus使用PVC:
prometheus: prometheusSpec: storageSpec: volumeClaimTemplate: spec: storageClassName: "local-path" accessModes: ["ReadWriteOnce"] resources: requests: storage: 50Gi这里踩过的坑是:如果集群里的StorageClass没有配置动态供给,PVC会一直停在Pending状态,Prometheus的Pod也起不来。所以配置前先确认集群里有哪些可用的StorageClass,以及它们的供给方式。另外Prometheus官方建议保留15天左右的数据,具体存储容量可以根据你的抓取指标数量和保留周期粗略估算。
6.2 资源配额调不好,Pod起不来
默认的Chart会为Prometheus和Grafana设置requests,但如果你在自定义values.yaml时把limits设置得比实际用量小,最常见的问题就是Pod一直重启,看日志才发现是OOM。
各个组件的资源需求差异很大:Prometheus随着监控目标增加,内存消耗会明显上涨;Grafana在加载复杂面板时CPU波动明显。建议初期不要把limits卡得太死,先让Prometheus和Grafana跑起来,观察一到两周的实际用量,再根据Grafana里的资源监控回头调整。
我之前就踩过一次:给Prometheus的容器设了2Gi内存limits,实际因为采集了太多业务指标,半小时就崩了,排了半天才发现是内存被打满。后来把limits调到合理值并加了PVC,才稳定下来。合理的做法是先用宽松limits跑,再结合容器的实际用量曲线去收紧,而不是上来就拍脑袋定一个数。
6.3 kubelet指标抓取失败的常见原因
集群本身的很多指标依赖kubelet暴露的metrics接口。但在部分托管集群或自建集群里,kubelet的指标端口可能没有对外开放,导致Prometheus的target列表里出现很多down状态。
常见原因有两个。一个是指标地址绑定在回环地址上,只能本机访问,需要通过修改kubelet启动参数或者使用特殊抓取方式解决;另一个是kubelet的安全端口需要认证,而Prometheus没有配置对应的token或者证书。这部分和集群初始化方式强相关,不同发行版的kubelet参数默认值都有差异。
kube-prometheus-stack自带的规则和抓取配置已经处理了大多数场景,但仍建议安装后先打开Targets页面过一遍,确认所有目标都在up状态再继续加业务采集。一个很实用的排障路径:在目标列表里点开一个down状态的目标,查看它返回的error信息,大多数时候错误信息已经把原因说得很直白了。
| 现象 | 可能原因 | 排查/解决方案 |
|---|---|---|
| Prometheus Pod一直重启 | 内存limits过小或PVC未绑定 | 查看Prometheus日志,调整资源或存储 |
| Grafana数据源连不上 | URL填了localhost | 改为Prometheus Service的集群内地址 |
| Target列表有大片down | kubelet端口未开放或证书异常 | 逐项看Down的原因,重点看错误信息和认证配置 |
| 新添加的Service采集不到 | ServiceMonitor标签不匹配 | 核对selector标签与Service标签一致 |
最后说一点个人体会:Prometheus+Grafana这套方案在K8s里的成熟度确实高,但难度从来不在安装,而在把链路跑通之后怎么持续维护。我自己的习惯是每接一个新集群,先让Targets页面保持全绿,再谈后面的事情。监控系统的价值不在于面板多华丽、告警多密集,而在于当你真正需要它的时候,它足够可靠、足够准确。希望这篇文章能帮你少走点弯路,把基础打扎实。