PLFM_RADAR这个项目代号第一次出现在我们运维群里时,很多人误以为是一套新的雷达硬件设备。实际上它是我们团队内部对“平台可观测性监控系统”的统称:PLFM是Platform的缩写,RADAR在这里既包含了雷达“扫描发现目标”的意象,也暗指这套系统的核心职责是Real-time Alert Detection And Response——实时告警检测与响应。这套系统解决的问题非常具体:微服务拆分之后,一个订单抖动会牵动十几个应用,没有统一雷达,你只能靠业务方投诉才发现故障,而PLFM_RADAR做的事情就是把日志、指标、链路、资源四类数据汇到一张图上,故障还没酿成大祸就被扫描出来。这篇文章我会从设计思路、核心模块、告警引擎、落地实操到排障实录,把这套平台监控体系从头到尾拆一遍,适合正在搭建或重构监控体系的运维、SRE和后端开发同学参考。
1. 项目整体设计与思路拆解
1.1 为什么叫PLFM_RADAR而不是“监控系统”
我最早接触这个项目时,部门里已经有不下三套监控工具:一套看服务器CPU,一套看应用日志,还有一套负责发告警邮件。问题是这些工具各管一段,互相之间没有任何联动。数据库CPU飙高,监控平台报了警,但没人知道是哪个业务在打慢查询;日志里一堆ERROR刷屏,告警规则阈值设得太粗,半夜三点把值班同学叫起来一看是误报。
PLFM_RADAR的设计初衷就是结束这种“碎片化监控”的状态。我们给它的定位不是又一个监控工具,而是一个平台级的统一观测视图。名字里特别保留RADAR这个词,强调它应该像雷达一样持续旋转扫描:每一个服务的请求量、响应时间、错误数、依赖资源、宿主机状态,都通过一条条“波束”周期性地扫过,一旦发现信号异常就触发告警并联动定位。这个视角和传统监控有本质区别——传统监控是被动的,等阈值越界才报警,而雷达式监控强调主动扫描、持续观测、快速锁定异常范围。
这个命名的另一个好处是团队沟通成本显著降低。排查故障时大家说“雷达扫一下”,所有人都明白是去看PLFM_RADAR大盘,而不是问“是看Grafana还是看日志平台”。一个清晰的项目代号,看着是小事,实际对协作效率的提升非常明显。
1.2 整体架构选型思路
架构上我们没有选择直接采购商业APM全家桶,原因有两个:一是成本,商业APM按节点收费,几百个服务跑下来账单很吓人;二是定制化,我们既要监控标准技术指标,也要埋业务指标(比如订单量、支付成功率),商业产品很难完全贴合。
最终选型以Prometheus为核心指标底座,Grafana做可视化,Loki做日志聚合,Jaeger/Tempo做链路追踪,Alertmanager负责告警分发。这套组合的好处是全部开源、社区活跃、生态丰富,且每一层都可以替换、可以自研扩展。我们称它为“三源一底座”:日志、链路、指标三个数据源,统一汇入一个时序存储底座。这个架构里Prometheus的PromQL是整个体系的“共通语言”,指标告警、日志告警、甚至是链路数据的统计,最终都能归一到时序指标上来表达。
用“雷达波束”来比喻的话,日志源负责看“发生了什么”,链路源负责看“调用怎么走的”,指标源负责看“系统状态如何”,三束波扫描出来的信号最终都要显示在同一个屏幕上。这个“统一屏”就是Grafana上的PLFM_RADAR主面板。
1.3 三个贯穿始终的设计原则
落地过程中我们沉淀了三条设计原则,对整个项目的成败影响最大。
第一条是低耦合采集。所有采集端都独立部署、独立配置,任何一个采集模块挂了都不能影响业务进程。我们用sidecar模式和exporter模式混布,应用只负责暴露/metrics端点,采集任务全部由外部抓取,业务代码零侵入。
第二条是高覆盖低噪音。监控指标要覆盖基础设施、中间件、应用、业务四个层级,但告警规则宁可少而精,不可多而滥。我们踩过“指标疯狂接入但告警无人响应”的坑,后来强制规定:接入一个指标,必须配套一条告警规则和一位负责人的“三件套”流程,从源头杜绝僵尸监控。
第三条是快定位。告警消息里必须包含“哪个服务、哪个实例、什么指标、当前值、持续多久、疑似影响范围”六要素,值班同学拿到告警不需要再翻三四个系统才能开始排查。后面所有告警模板设计都围绕这个原则。
2. 核心模块与数据链路实现
2.1 波束一:基础设施资源监控
PLFM_RADAR的第一层扫描对象是基础设施,也就是运行所有服务的物理机、虚机和容器。
这层我们使用node_exporter覆盖CPU、内存、磁盘、网络、文件系统五个大类。CPU不只采集usage,还拆解出user、system、iowait、steal等明细状态;磁盘除了空间使用率,还额外加了inode使用率和磁盘读写延迟两个指标。
这里有非常多的实操细节。比如磁盘监控,只盯空间使用率是不够的,经常出现磁盘明明还有30%空间,但文件系统因为inode耗尽而无法创建文件的情况。我们有一条专门针对inode使用率的告警规则,inode使用率超过85%就提前预警,这救过我们好几次。
还有文件句柄数。Java应用和Nginx在高并发下最常见的问题就是“Too many open files”,这个指标在监控里必须提前暴露。我们在node_exporter之外,还从应用进程层面采集fd数量,和系统层的limits做对比,一旦超限比例接近80%就告警。基础设施指标是整个雷达的地基,地基不稳,上层应用的告警分析全是假象。
2.2 波束二:应用性能与业务指标
第二层扫描的是应用本身的运行状态。
应用层指标分为标准指标和业务指标两类。标准指标包括QPS、响应时间、错误率、活跃线程数、JVM内存等,这些通过Micrometer或者Prometheus客户端库暴露,接入了Spring Boot、Go、Python各类服务。业务指标是我们单独埋点统计的,比如订单金额、支付成功率、购物车转化率、任务处理积压数等,以业务字典的方式统一规范命名。
这里特别想说一下指标命名与标签规范。我们用命名的层级结构区分指标来源:metric_prefix代表类型,比如server_、client_、job_;metric_name代表语义,比如http_requests_total、db_query_duration_seconds。标签设计上有一个血泪教训:标签基数必须受控。凡是可能无界增长的标签值,比如用户ID、订单ID、IP地址,一律禁止作为指标标签,否则Prometheus的时序数量会爆炸。我们曾经因为把一个HTTP状态码标签和一个自定义的业务标签一起使用,一个月内时序数量从50万涨到8000万,内存直接被打爆。这事的教训我放在后面“常见问题”里细说。
应用层的告警指标我们重点盯三个方向:流量突增、延迟突刺、错误暴涨。流量突增不一定就是故障,但流量骤降或者突增成十倍,背后一定有问题,尤其要注意秒杀活动或版本发布前后的基线对比。
2.3 波束三:日志异常识别
指标只能告诉你“系统出问题了”,但问题具体是什么,往往要去日志里找答案。日志是PLFM_RADAR里最接地气也最容易失控的一层。
日志采集我们一开始用的ELK方案,后来因为资源占用太高切到了Loki。Loki的优势是不建立全文索引,只索引标签,日志内容本身直接压缩存储,成本比ES低得多,特别适合“查错定位”这种场景,而不适合做复杂的全文统计分析。如果团队需要大量日志聚合分析,ES还是更强,但只做监控排障,Loki完全够用。
日志异常识别我们做了三个层次。第一层是关键词匹配,ERROR、Exception、Timeout这些关键词出现频率超过一定基数就预警;第二层是日志量突增检测,某个服务的日志行数在5分钟内翻了三倍,基本可以断定有异常逻辑在刷日志,这往往是死循环或者重试风暴;第三层是日志与调用链关联,从日志里提取traceId,跳到链路系统看这一条请求到底走了哪些服务。
日志告警最常见的坑是内容噪音。“业务上正常预期内会出现的WARN级别日志”如果被当成ERROR告警,值班同学会在第一个晚上就关掉告警通道。我们最后建了一个“忽略词列表”,把Spring框架启动时的常见非致命异常、预期内的超时重试日志全部过滤掉,告警准确率显著提升。
2.4 波束四:链路追踪与依赖分析
微服务架构下最痛苦的排查场景是:一个请求经过A、B、C、D四个服务,耗时2秒,但每个服务单独看RT都正常,你怎么知道慢在哪个环节?
链路追踪就是解决这个问题的。我们通过OpenTelemetry实现全链路埋点,将一次请求从网关到各微服务到数据库的完整调用链串联起来,数据上报到Tempo存储,然后在Grafana上做链路查询和依赖分析。
不过这层实现起来比指标要复杂得多。最大的痛点是采样率策略。全量采集链路数据在中等流量下存储成本都非常惊人,我们最终采用动态采样:正常服务按10%采样,出现错误码的请求100%采样,关键核心交易链路100%采样。这个策略让我们在成本和排障能力之间找到了平衡。
用链路数据我们做了一张“服务依赖图”,直观展示服务间的调用关系和调用量。图上任何一个节点变红,都能马上看到它的下游依赖是否超时、上游调用方是否在重试。“雷达”扫描到的信号不再是孤立点,而是一张完整的连接网络,排查效率提升了不止一个量级。
3. 告警引擎与扫描周期设计
3.1 告警规则的编写方法论
告警规则是整个PLFM_RADAR系统的“雷达灵敏度旋钮”,写得太松扫不出问题,写得太紧天天误报。
我们总结了一套三层规则模板。基础设施层用固定阈值,例如CPU使用率持续5分钟超过90%告警;应用层用动态基线,例如响应时间超过过去7天同一时段均值的3倍才告警;业务层用环比突变,例如订单量同比昨天同一时段下降30%触发预警。
PromQL是写规则的主要语言,几个最常用的模式可以分享给大家。CPU告警的完整表达式是:
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90这里必须用rate函数包裹counter类型指标,取5分钟的平均变化率,才能消除瞬时抖动的影响。错误率告警的表达式是这样的:
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service) / sum(rate(http_requests_total[5m])) by (service) > 0.05响应时间的P95告警用的表达式是:
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service) ) > 1写告警规则时一个反直觉的经验是:不要对单独的实例写阈值,尽可能用by聚合,或者至少对多个实例同时异常才触发。“单个实例偶发抖动”和“多个实例同时异常”是完全不同的故障级别,前者可能只是网络抖动,后者一定意味着集群级问题。
3.2 动态阈值:告别一览无余的死阈值
最早期我们所有告警都是固定阈值。固定阈值的问题很明显:白天高峰期CPU跑50%是正常,同一台机器凌晨三点CPU跑50%就很可疑;秒杀活动持续一小时,业务指标短时翻倍是预期行为,用固定阈值就一定会误报。
后来我们在关键业务指标上引入了动态基线能力,核心思路是周期性的滑动基线对比。具体做法是用过去14天每一时刻的同类数据做样本,计算均值和标准差,当实时指标偏离均值超过3个标准差时判定为异常。
下面这个Python片段是我们用在告警引擎里的核心检测函数,简化后可以作为参考:
import numpy as np def detect_anomaly(current_value, history_values, k=3): mean = np.mean(history_values) std = np.std(history_values) if std == 0: return False z_score = (current_value - mean) / std return z_score > k or z_score < -k就是这样一个简单的三西格玛判断,让误报率降了至少60%。不过它也有代价:业务发生真实趋势性变化时,基线会“慢慢接受”新状态,所以我们在动态检测之外保留了一个“人工基线覆盖”功能,大促活动前运维提前锁定一个基准线,活动期间不触发动态告警。
3.3 告警分级、路由与抑制去重
规则写好了,另一个关键问题是怎么发、发给谁。我们参考业界惯例将告警分为P1到P4四级:P1是核心服务不可用,需要立即拉群处理;P2是功能受损但服务可用,15分钟内响应;P3是资源预警,当天处理即可;P4是信息类告警,只记录不打扰。
Alertmanager在这一层承担了路由、抑制和去重的任务。路由规则按告警标签分发到不同渠道:P1和P2同时发电话、短信和企业微信,P3只发企业微信,P4静默归档。每次告警都必须携带“服务名、实例、指标、当前值、持续时间、负责人”六要素,这个模板我们在Alertmanager的自定义消息里实现。
告警抑制是最能体现“老司机”经验的配置。最典型的场景是:数据库挂了,依赖它的几十个服务全部报错,这时候如果几十条告警同时发出来,值班手机能炸掉。我们用Alertmanager的inhibit_rules配置,设置“数据库故障”告警发生时,抑制所有由它引发的下游服务告警,只保留根因告警。这个规则上线后,告警数量直接降了一个数量级。
inhibit_rules: - source_matchers: - severity = "P1" - alertname = "DatabaseDown" target_matchers: - severity = "P1" equal: - cluster3.4 告警生命周期:从发现到恢复的闭环
系统上线初期告警发出去就结束了,后来发现这是很大的管理漏洞。现在我们把告警当成一个有生命周期的对象来管理:触发、认领、处理、恢复、复盘五步闭环。
触发后,告警带着唯一ID进入待处理列表。值班同学收到消息后需要在5分钟内在平台上“认领”这条告警,认领会自动在值班群通知“这个问题张三在处理,其他人不用重复跟”。处理过程中可以更新备注,记录排查思路和当前进展。指标恢复正常后,告警自动标记为“已恢复”,系统附带推送一段“本次故障持续时长和关键指标变化区间”的摘要。每周我们导出一次告警复盘报表,统计各服务的MTTA和MTTR,持续优化响应流程。
这个闭环看起来是管理流程,但我们要通过技术手段去保障:所有认领和备注操作都通过Webhook回写到告警系统,对应的数据也存入ClickHouse,供后续统计SLO和告警质量用。
4. 从0到1搭建PLFM_RADAR的实操过程
4.1 环境准备与一键部署
如果你也想复现这套体系,最快路径是用Docker Compose先搭建一套基础环境。下面是一个精简版的docker-compose配置,包含了Prometheus、Alertmanager、Grafana和Loki四项核心服务。我是建议先在测试环境完整跑通,再上生产。
version: '3.8' services: prometheus: image: prom/prometheus:v2.50.1 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - ./rules:/etc/prometheus/rules ports: - "9090:9090" alertmanager: image: prom/alertmanager:v0.26.0 volumes: - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml ports: - "9093:9093" grafana: image: grafana/grafana:10.3.1 environment: - GF_SECURITY_ADMIN_PASSWORD=strong-password ports: - "3000:3000" volumes: - grafana-data:/var/lib/grafana loki: image: grafana/loki:2.9.6 ports: - "3100:3100" volumes: grafana-data:这套基础环境对服务器要求不高,4核8G的内存就能稳稳跑起来。如果你是监控已有的Kubernetes集群,可以直接用Prometheus Operator的Helm Chart方式部署,组件间的配置管理和服务发现都更方便,但理解上会多一层抽象,建议先在原生Docker环境把PromQL和告警原理搞明白。
4.2 指标采集与告警配置实战
接下来是采集配置。下面的Prometheus配置片段展示了三个核心采集任务:一类是采集宿主机指标的node_exporter,一类是采集应用指标的业务exporter,还有一类是通过服务发现自动发现的Kubernetes Pod。
scrape_configs: - job_name: 'node-exporter' static_configs: - targets: ['192.168.1.10:9100', '192.168.1.11:9100'] - job_name: 'business-app' metrics_path: '/metrics' static_configs: - targets: ['192.168.1.20:8080'] - job_name: 'kubernetes-pods' kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_label_app] target_label: app采集间隔我们设置的是15秒。为什么不是5秒或者1分钟?5秒的数据精度高,但存储和查询压力成倍增加;1分钟则会漏掉很多短时间内的异常波形。15秒是一个平衡点,能捕捉到大部分故障的节奏变化。
告警规则文件建议每类一个独立文件:infra.yml放基础设施规则,app.yml放应用规则,business.yml放业务规则。下一条示例是业务规则里的订单量环比突降告警,采用PromQL对相同时间段做对比判断。
groups: - name: business-alerts rules: - alert: OrderVolumeDrop expr: | sum(rate(order_create_total[10m])) < (sum(rate(order_create_total[10m])) offset 24h) * 0.7 for: 5m labels: severity: P2 annotations: summary: "订单量同比昨日同时段下降超过30%"配置完成后启动所有组件,在Prometheus的Status页面确认所有target都是UP状态,再到Grafana创建第一个Dashboard,整个雷达的“屏幕”就开始出图像了。
4.3 可视化面板与值班看板设计
告警再多,没有一个好的总览面板,值班同学的效率还是会很低。我们把Grafana面板分成三层来设计:顶层是总览面板、中间层是服务详情面板、底层是值班大屏。
总览面板有一个核心指标叫“雷达健康分”,按权重汇总了基础设施异常数、应用错误率、日志错误频率、链路超时率四项综合得分。90分以上绿灯,75到90分黄色预警,75分以下红色故障。这个健康分不是花架子,值班同学扫一眼就知道当前系统整体状态。
服务详情面板按服务维度下拉,一行一个服务,展示每个服务的请求量、延迟、错误率和饱和度四类黄金信号。这里特别推荐使用Grafana的“表格+时间序列”混合布局,既能看实时数值,又能看到趋势曲线。
值班大屏放在团队工位正前方,用大尺寸显示器循环播放。展示的内容不是所有指标,而是三块:当前活跃告警列表、故障服务雷达图、最近24小时告警分布热力图。故障服务雷达图我们使用了Grafana的polar panel插件,以服务为圆周分布,圆心到服务点的距离表示健康度,一旦有服务变红,在远处就能看到。
4.4 与CI/CD和值班流程的集成
监控系统建好之后,要让它真正长在运维流程里,而不是变成又一个“没人看的饼”级系统。我们做了两个关键集成。
第一个集成是告警和变更记录的联动。很多故障都是版本发布触发的,我们在告警消息的注解里自动带上最近一次相关服务的发布记录,如果告警时间和上线时间间隔小于30分钟,会自动加一行“疑似与本次发布有关”。这个字段通过查询发布平台的API动态填充,排查时省掉了大量对时间轴、翻日期的操作。
第二个集成是建立on-call轮换值班机制。Alertmanager的webhook告警到企业微信后,由平台的排班模块自动把消息@给当天值班的同学,同时在第二天早上自动生成告警日报,发到运维管理群和研发管理群。这一步做出来后,主管对系统稳定性的感知才真正建立起来,监控从一个技术工具变成了一个管理抓手。
5. 常见问题与排查技巧实录
5.1 监控数据突然断档,告警一片“NoData”
一个很常见但很容易误判的现象:Grafana面板突然一片空,或者大量告警从“异常”变成“NoData”。大多数人的第一反应是业务真的挂了,但排查下来经常是Prometheus本身出了问题。
我自己遇到过两次。第一次是Prometheus数据目录所在磁盘满了,TSDB写入失败导致最近一段时间没有数据。第二次是采集器和Prometheus之间的网络出现短暂中断,重连后因为WAL回放太慢,数据迟迟追不上当前时间。
这类问题的排查顺序是固定的:先看Prometheus的TSDB状态,再看target的up状态,最后看采集器的日志。这里的一个重要经验是:给Prometheus数据盘单独挂一块大容量磁盘,并配置磁盘使用率告警,而不是共享系统盘。TSDB的数据量是随时间线性增长的,不提前规划容量,总有一天会悄悄打满。
5.2 高基数标签把Prometheus内存打爆
前面提到过标签基数问题,这里展开说说。Prometheus内存消耗与时序数量成正比,时序数量又由标签组合决定。假设一个指标有5个标签,每个标签有100个不同取值,理论上就会产生100的5次方,也就是100亿个时序,再大的机器也扛不住。
我们踩过的真实案例是在HTTP请求指标上加了request_id标签,用来追踪单次请求。这个标签值天然无上限,上线才两天,Prometheus的内存直接从3G涨到25G,查询接口开始超时。
最终处理是把这类标签从指标里去掉,改为在日志和链路数据中关联,因为单请求级别的关联属于日志和链路系统的范畴,而不是指标系统的范畴。这里给大家一个参考:单个指标的所有标签组合数量,也就是基数,控制在10万以内,超过这个量级就要重新审视标签设计。
5.3 一个故障刷出几百条告警的“告警风暴”
告警风暴几乎是监控系统上线后的必然经历,差别只是严重程度。我们经历过最严重的一次是缓存集群的主节点宕机,结果依赖缓存的几十个应用服务全部超时,同时触发了一大堆告警,值班手机一分钟响了三四十次。
这个问题的解决要靠前面说的抑制规则和分组规则配合。抑制规则确保下游告警不重复发,分组规则确保相同服务、相同告警类型的消息合并成一条。还有一个隐蔽的技巧是给Alertmanager设置repeat_interval,比如P3告警4小时重复通知一次就可以,P4直接关闭重复通知,这能显著降低告警疲劳。
告警风暴过后还要做的一个动作是“复盘追责”:每条告警都要反问一句“这条告警发出后有人第一时间处理了吗?处理动作是什么?”。如果一条告警发出两周都没有人点开看过,那这条告警就没有存在价值,就把它关掉,不要让它继续制造噪音。
5.4 明明服务已不可用,告警却不触发
比告警风暴更头疼的是该告警的时候没告警。最典型的一个PromQL坑是rate函数与irate函数的误用。
rate计算的是指定时间范围内的平均变化率,适合用于告警判断,因为它平滑了瞬时抖动。irate计算的是区间最后两点的瞬时变化率,曲线毛刺很多,适合用于短时间窗口的图表展示,但不适合做告警规则,因为它容易因为瞬时尖刺触发误报。
另一个常见的坑是for子句的用法。for: 0m表示立即触发,1m表示指标持续异常1分钟才触发。排查一个服务周期性抖动的案例时我们发现,抖动周期是30秒,很快又恢复,for设成0m时每天误报几十次,设成5m后误报为零,但真正持续故障的发现时间也慢了5分钟。这个时间要按业务容忍度来调,不能照搬别人的配置。
5.5 常见告警误报、漏报问题速查表
最后把实操中遇到过的典型场景整理一个速查表,方便大家直接参照排查。
| 现象 | 可能原因 | 排查路径 | 解决思路 |
|---|---|---|---|
| 磁盘使用率告警但df正常 | inode耗尽 | 执行df -i查看inode使用率 | 清理小文件,补充inode告警规则 |
| 内存指标持续高位但未触发OOM | page cache未回收 | 查看cached与buffers占比 | 区分真实内存使用量,用available指标 |
| 多个服务同时告警数据库超时 | 数据库连接池打满 | 查看数据库活跃连接数和慢查询 | 优先处理根因告警,抑制下游告警 |
| 告警发了但没收到企业微信 | Webhook地址失效或限流 | 测试Webhook连通性,看Alertmanager日志 | 配置备用Webhook,加自动重试 |
| 新服务上线后没有指标 | 采集任务未服务发现 | 确认服务是否暴露/metrics端点 | 标准化应用接入模板,自动注册 |
| P95延迟告警但看曲线正常 | histogram bucket设置不合理 | 检查bucket上界覆盖范围 | 调整bucket为上界的1.5倍到2倍 |
这套排查表的价值在于,很多告警问题本质上是配置问题而不是业务问题,提前建立“先查监控自身再查业务”的排查顺序,能节省大量时间。
写在最后的体会
从PLFM_RADAR立项到现在,我们团队经历了过去“告警没人看”到如今“告警有人抢着认领”的转变,中间没有捷径,靠的就是持续打磨规则和流程。我个人最大的体会是:监控系统最难的不是技术,而是让团队对告警建立起信任——误报多了没人看,漏报多了没人信,只有通过动态阈值、抑制去重、认领闭环这些细节一点点把误报率降下来,大家才会在凌晨收到告警时立刻从床上爬起来而不是翻个身继续睡。如果你也在搭建类似体系,建议先别急着堆功能,把告警准确率这一个指标做到95%以上,再考虑扩展其他能力。另外再分享一个小技巧:每次故障复盘后,都应把这次排查路径固化成一条“告警处理笔记”存到平台里,下次同类告警出现时,值班同学能在30秒内调出上次的处理思路,这才是监控系统持续积累价值的正确方式。