凌晨三点,手机在床头柜上疯狂震动。接起来就听到一句:“支付页面打不开了,用户在群里刷屏了。”我那时候一边开电脑一边想:如果系统能在用户发现之前告诉我,该多好。
这种被动挨打的局面,几乎所有做线上系统的人都经历过。线上故障的发现速度,直接决定了故障影响面。同样是数据库连接池被打满,早十分钟发现可能只是少数请求超时,晚十分钟可能就是核心业务全挂、用户大量流失。所以“如何第一时间发现线上故障”从来不是监控面板上多挂几个图表的事,而是一整套从指标采集、日志监控、链路追踪到告警触达的闭环设计。这篇文章把我这几年搭监控、调告警、处理线上事故的经验沉淀一下,适合正在负责线上系统的运维、后端开发、SRE 同学参考,也适合刚接手一个“什么监控都没有”的项目的朋友抄作业。
1. 先说清楚:什么叫“第一时间发现”
1.1 不要把“第一时间”理解为“最快”
很多人一提到故障发现,第一反应是“告警要快”。其实这里的“快”不是绝对意义上的秒级,而是在用户感知之前完成发现和响应。业内通常用两个指标来衡量这个能力:MTTD(Mean Time To Detect,平均发现时间)和 MTTR(Mean Time To Repair,平均恢复时间)。我见过不少团队,监控工具装了一堆,告警也能在几秒内推送出来,但值班同学看到告警不知道发生了什么、不知道找谁处理,最后 MTTD 是快了,MTTR 反而没变化,甚至因为误报干扰,真正的问题被淹没在告警海洋里。
线上故障的发现路径,本质上只有两个方向:被动发现和主动发现。被动发现就是用户反馈、客服转达、产品同学在群里说“系统好像出了问题”;主动发现则是靠监控告警、日志巡检、定期自检。如果你们的故障大多是被动发现的,那说明监控体系基本是失灵的。我见过一个极端案例:某核心服务的磁盘空间被打满,业务上表现为文件上传全部失败,但因为这个服务平时请求量不高,流量曲线看不出异常,团队愣是等用户投诉了才发现。这其实就是典型的“监控存在但不覆盖关键场景”。
1.2 反推设计:从用户可感知的业务指标倒着搭
踩过几次坑之后,我总结出一个原则:搭监控不能从“我能监控什么”出发,要从“用户会感知到什么”出发,倒着设计。
以最常见的电商下单场景为例,用户能感知到的是:页面能不能打开、商品能不能搜到、下单成不成功、支付流程顺不顺畅、订单状态能不能查到。这些感知落到系统指标上,对应着请求成功率、响应时间、下单接口的 QPS、支付回调的延迟和成功率。我管这套指标叫“用户可感知指标”,它们的优先级永远高于 CPU、内存、磁盘这类基础设施指标。
一套完整的监控体系,我会分层来搭:
- 用户端层:页面可用性、核心接口的成功率和耗时,最好能模拟用户访问做主动探测
- 应用层:服务实例的 QPS、错误率、响应时间、JVM 或进程核心指标
- 组件层:数据库、缓存、消息队列的连接数、慢查询、堆积量、主从延迟
- 基础设施层:CPU、内存、磁盘、网络 IO、带宽
每一层都可能出问题,但出问题时对用户的影响是不同的。基础设施层出问题,通常表现为上层多个指标异常;而用户层指标异常,不一定能通过底层指标解释。所以我的做法是:监控配置从用户层开始,逐层向下覆盖,宁可底层指标少一点,也要保证用户层和核心应用层指标的覆盖率。反过来排查故障时,则是从用户层指标入手,顺着异常逐层下钻定位根因。
2. 监控体系三大支柱:指标、日志、链路
2.1 指标监控:先保证“看得见”
指标监控是监控体系的地基,解决的是“系统现在什么状态”的问题。一个服务到底健不健康,先看指标曲线。Prometheus 加 Grafana 的组合是目前开源社区里用得最多的选择,网络上相关的配置案例也很多。核心思路其实很简单:用 exporter 暴露指标,Prometheus 定期拉取,Grafana 展示和告警。
基础层指标,比如节点 CPU、内存、磁盘、网络,直接用现成的采集组件即可。但应用层和业务层指标,必须自己做埋点。我见过很多团队把 node_exporter 装好就以为监控完成了,真出了故障,面板上只能看到服务器空转,业务代码里的死循环、慢 SQL、下游超时,一个都看不出来。
业务埋点其实不复杂,以支付服务为例,用 Prometheus 客户端在代码里定义几个关键计数器就够了:
from prometheus_client import Counter, Histogram # 支付请求总量和成功量 PAYMENT_TOTAL = Counter('payment_total', 'Total payment requests') PAYMENT_SUCCESS = Counter('payment_success', 'Success payment requests') # 支付耗时分布 PAYMENT_DURATION = Histogram('payment_duration_seconds', 'Payment latency', buckets=[0.1, 0.3, 1, 3, 10]) def process_payment(order): with PAYMENT_DURATION.time(): try: do_payment(order) PAYMENT_SUCCESS.inc() finally: PAYMENT_TOTAL.inc()这样就能在 Grafana 里画出支付成功率、P95 耗时的趋势线。这些指标的异常,才是用户真正感知到的异常。 关于指标监控,有几个容易忽略的细节。一个是数据保留周期,Prometheus 默认的本地存储通常只保留 15 天,如果公司有更长的审计或回溯需求,最好把数据对接长期存储或做定期快照备份。另一个是指标采集频率,默认 15 秒拉取一次对大多数场景足够,不用为了追求“实时”把采集间隔压到秒级以下,那样只会加重监控组件自身的压力,对故障发现几乎没有额外帮助。 ### 2.2 日志监控:把错误“捞”出来 指标会告诉你“系统出问题了”,但通常不会告诉你“为什么出问题”。日志是回答“为什么”的关键。而“第一时间发现线上故障”这件事,日志监控的目标不只是事后翻日志,而是让日志里的异常成为告警来源。 一个比较常见的场景:服务虽然没有宕机,接口也没有大量报错,但日志里持续出现某种特定异常,比如下游超时重试、缓存穿透打到数据库。如果没有日志告警,这类“慢刀子割肉”式的问题可能要过很久才被注意到。所以我一直建议团队把日志监控和指标监控放在同等重要的位置。 日志监控的落地路径,常见的是“采集端 + 搜索端 + 告警”的组合。采集端负责从服务器收集日志,过滤敏感信息后推送;搜索端做索引和查询;告警则基于日志内容的关键字或频率触发。配置一个核心服务的关键字告警,通常长这样: ```yaml # 采集配置示例 filebeat.inputs: - type: filestream enabled: true paths: - /var/log/payment-service/*.log fields: app: payment-service告警规则就围绕关键字来做,比如java.sql.SQLException、Connection pool exhausted、Request timeout。这里有一个我踩过很多次的坑:直接对日志原始内容做关键字匹配,误报率会高到让人崩溃。生产环境里某个异常关键字每分钟出现几次其实是正常波动,直接匹配意味着每天产生几百条无用告警,真正的紧急问题反而被淹没。更合理的做法是结合“出现频率 + 时间窗口”来判断,比如最近 5 分钟内某个关键字出现次数超过 N 次才触发告警,这个 N 需要根据业务历史数据来调,前期先放大,稳定后逐步收紧。
另外一个建议:尽早推动结构化日志。用 JSON 格式输出日志,把错误码、接口名、耗时、traceId 都作为字段,那么告警规则可以用“错误码等于 5001 且耗时大于 3000ms”这类组合条件来写,精确度和可读性都远超在纯文本里搜关键字。
2.3 链路追踪:快速定位“是哪一环坏了”
微服务架构下的线上故障,有一个特别恶心的特点:错误是跨服务传递的。用户请求没成功,可能是网关超时,也可能是订单服务挂了,还可能是下游渠道返回了错误。单看每个服务的指标和日志,每个服务好像都“很正常”——日志里确实有报错,但每个服务都觉得自己是受害者。如果拿不到完整的调用链路,排查故障全靠猜,那就谈不上“第一时间发现”,只能“第一时间开始排查”。
链路追踪解决的就是这个问题。核心思路是给每个请求分配一个全局唯一的 traceId,这个 ID 从入口一直透传到所有下游服务,每个服务处理时记录自己这一段耗时和状态,最终串成一条完整的调用链。当接口报错时,直接通过 traceId 查出整条链路,哪个环节耗时最长、哪个环节返回了错误、哪个服务被调了 N 次,一目了然。
链路追踪的接入要点,首先是采样策略。全量采集在低流量下没问题,但大流量下存储和性能成本很高,通常做法是默认 10% 采样,关键接口单独设置 100% 采样。其次是上下文透传,有些团队只在网关层生成 traceId,下游服务不配合透传,导致链路追踪只能看到一段,这是最典型的落地失败原因。技术上需要所有服务统一接入链路追踪 SDK 或中间件,并约定好透传协议。
实际排查中,链路追踪的价值体现在:一个下单接口超时,打开链路图能看到 80% 的时间花在某个下游渠道调用上,而不是在业务代码里。这个定位过程从原来的几十分钟,缩短到几分钟。所以我会说,指标监控、日志监控、链路追踪三个加在一起,才形成一个完整的“发现—定位”闭环:指标告诉你哪里异常,日志告诉你异常内容,链路告诉你异常路径。
3. 告警体系设计:不是所有警报都值得半夜叫醒你
监控搭完只是第一步。如果告警设计得烂,每天几百条消息,值班同学看都不看就划掉,真出问题时告警形同虚设。我有段时间最怕的不是没告警,而是告警太多,凌晨手机响个不停,打开一看全是无关紧要的“CPU 超过 60%”之类,那时候真的想把手机扔到窗外。告警体系的本质,是用最小的噪音传递最关键的信息。
3.1 告警阈值怎么定才不误报
阈值设置是告警设计里最考验经验的部分。常见问题是两类:一类是阈值设得太宽松,故障发生半天都不触发;一类是设得太敏感,系统稍微抖一下就刷屏。
我一般用“动态视角 + 窗口期”的思路来定阈值。所谓动态视角,是指不要只看某个指标的绝对值,而是看它的变化趋势。比如 CPU 使用率 80%,对一个高流量的核心服务来说可能是正常水位,但对一个平时 20% 都不到的边缘服务来说,就是明显异常。处理办法是按服务分组设置不同阈值,或者用“相对偏差”类告警,比如“当前值比过去 24 小时同时段均值高出 50% 才触发”。
窗口期的作用是过滤瞬时抖动。比如磁盘使用率超过 85% 持续 10 分钟才告警,而不是超过就告警,因为磁盘空间往往是有缓和的,真正危险的场景是持续增长。Prometheus 告警规则里for参数就是干这个的,我之前配置过一个比较经典的规则:
groups: - name: node_alerts rules: # CPU 持续超过 80% 才告警 - alert: HighCPULoad expr: 100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 for: 10m labels: severity: P1 annotations: summary: "服务器CPU持续偏高" description: "实例 {{ $labels.instance }} CPU使用率已超过80%,持续10分钟"for: 10m这个参数非常关键,它避免了 CPU 瞬时尖峰带来的误报。根据我的经验,绝大多数指标都适合先加一个 5 到 10 分钟的窗口期,宁可晚几分钟收到告警,也不要被误报消耗掉注意力。唯一不适合加长窗口期的是“成功率断崖式下跌”这类用户可感知的指标,因为它们直接影响业务,需要秒级响应。
3.2 告警分级与通知路由
告警分级本质上是把“什么时候打扰谁”这个决策明确下来。我习惯分四级:
- P0:核心业务不可用,用户明确受到影响,比如支付成功率骤降、登录接口大面积超时,需要立即电话通知值班人并启动应急响应。
- P1:重要功能受影响但用户感知还不明显,比如某个非核心接口错误率升高、数据库主从延迟变大,通过电话或高优通知推送给负责人,要求分钟级响应。
- P2:系统存在隐患但当前不影响用户,比如磁盘空间超过阈值但还在增长、某服务内存缓慢上涨,通过即时通讯通知,当天处理即可。
- P3:信息类告警,比如某指标出现短期波动、证书即将到期,通过邮件或消息汇总,定期查看即可。
分级标准要写清楚,最好是用一句话定义加一个具体例子,比如“P0 = 核心链路成功率低于 99% 且持续 5 分钟”。有了分级,才能让不同级别对应不同的通知渠道和响应时限。P0 才配得上电话通知,P1 用即时通讯的高优通知,P2 和 P3 推送到普通群里。
之所以强调分级,是因为它直接影响告警是否被认真对待。如果所有人收到的都是同样的手机消息推送,那么重要的告警和无关的信息在感受上没有区别,很快就不想看了。我见过非常成功的实践:值班同学只对 P0 才能拨出电话,收到电话意味着“必须立刻响应”,这种稀缺性的设计极大地提升了告警的严肃性。
另外,每条告警最好带上“首次响应指引”,内容要能回答三个问题:这是什么问题?影响面多大?第一件事先做什么?实际配置中,我会在告警内容里写清当前值和持续时长,再附上排查入口,比如看哪个 Grafana 面板、搜哪个日志关键字。这样即使是一个不熟悉该服务的新人值班,也不会完全懵。
3.3 告警去重与抑制,防止告警风暴
告警风暴是线上故障最大的“二次伤害”。一次数据库主库宕机,可能同时触发:连接池告警、慢查询告警、上下游几十个服务调用失败的告警。如果没有去重抑制机制,值班同学会在几分钟内收到上百条消息,真正该关注的根因反而被淹没了。
处理告警风暴,业内通行的手段是“分组、抑制、静默”三板斧。分组解决的是“同类告警合并”:把按报警名称、主机实例、服务名分组,同一批告警合并成一条,显示受影响实例数量和样例信息。抑制解决的是“根因告警藏掉衍生告警”:比如某台机器宕机,所有针对该机器的告警都可以被抑制,只保留一条“主机宕机”的根因告警。静默则允许运维在已知的变更窗口或维护期间临时关闭某些告警,避免误报。
Alertmanager 的配置里,分组和抑制可以这样体现:
route: # 按告警名和实例分组,合并推送 group_by: ['alertname', 'instance'] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: 'default-im' routes: # P0 告警走电话通道 - matchers: - severity = P0 receiver: 'phone-group' continue: true inhibit_rules: # 主机宕机时,抑制该主机的其他所有告警 - source_matchers: [severity = "P0", alertname = "InstanceDown"] target_matchers: [instance = "同一实例"]group_wait设成 30 秒左右,可以让同一时间窗口内的多个告警攒在一起发出去。repeat_interval控制已推送告警的重复提醒间隔,一般设成 4 到 8 小时,防止同一条告警每隔几分钟就重复轰炸。
我在实际项目中专门做过一次告警治理:花了两周时间,把所有告警规则梳理了一遍,按照“是否影响用户、是否可执行、是否重复”三个标准逐一审视,结果近一半的告警规则被删除或改写。治理完成后,每天的告警量从 200 多条降到了 30 条以内,而且每一条都有明确的处理路径。那之后值班同学才开始真正信任告警,而不是本能地划掉它们。
4. 实战落地:一套最小可用监控方案
如果你接手一个项目,当前的监控是空白或者基本不可用,不要一上来就想搭一个“完美平台”。我建议了先跑通一个最小闭环,让关键服务的故障能被发现、被通知到人,然后再逐步完善。以下是我实际使用过的方案组合,开源工具选型来自社区的通用实践,按这个思路落地,一周内就能看到效果。
4.1 工具选型:自建还是托管
自建方案的核心组合是:Prometheus 采集指标,Grafana 做展示和告警,Alertmanager 做告警路由,日志采集端加搜索存储,链路追踪可以后置。这套组合的优势是灵活、可控、社区资料多,我身边的团队很多都在用。缺点是需要自己维护监控组件本身,磁盘、容量、升级这些都要管。
如果公司已经在用云服务,直接用云厂商自带的可观测产品也是一个合理选择。托管方案最大的好处是省心,数据可靠性、告警通道这些不用自己操心。但要注意两点:一是云监控默认覆盖的往往是基础设施指标,业务指标的埋点还是得你自己做,这点任何方案都躲不开;二是导出能力,如果以后想切换或做跨云分析,被打进厂商锁定会比较麻烦。
我的个人建议是:根据团队维护能力来选。团队里有专人做运维或 SRE,优先自建;两三个人既要写业务又要管运维,那就用托管方案,把精力花在业务埋点和告警调优上。
4.2 关键配置参考
这里分享几段经过实际项目验证的配置,可直接作为初始模板使用。
Prometheus 采集配置,最基础的是抓取 node_exporter 和应用自身暴露的指标端口:
scrape_configs: - job_name: 'node_exporter' static_configs: - targets: - '192.168.1.10:9100' - '192.168.1.11:9100' - job_name: 'app_metrics' metrics_path: '/metrics' static_configs: - targets: - '192.168.1.10:8080' - '192.168.1.11:8080'Grafana 的告警功能可以直接对接 Prometheus 数据源,核心通知渠道是即时通讯和邮件。如果条件允许,给 P0 告警配一个电话通道,这个投入非常值得。实际过程中我发现很多团队把 Prometheus、Grafana、Alertmanager 三个组件分开部署,其实初期完全可以在同一台机器上跑起来,等规模变大再拆分。
日志监控的最小方案是:日志采集端收集应用日志,推送到日志搜索存储中,再用告警规则对关键字告警。初期不要追求太复杂的告警算法,先把采集通道打通,再逐步摸索关键字的合理阈值。
4.3 接入步骤速览
一个最小闭环的接入流程,按顺序大概是这样的:
- 部署 Prometheus 和 Grafana,确认基本功能可用。
- 在核心服务所在节点部署采集组件,获取 CPU、内存、磁盘等基础指标。
- 在核心服务的代码里增加业务指标埋点,暴露
/metrics端口,接入 Prometheus 采集。 - 在 Grafana 里建好四类面板:核心接口成功率、关键业务吞吐、服务实例健康状态、基础设施资源水位。
- 配置第一批告警规则,从最核心的 5 条开始,宁可少不能滥。
- 对所有告警链路做一次“上线前测试”。这条极其重要:我见过太多团队配置了告警规则,但没有测试过实际触发链路,最后告警推送通道早就不通了。测试方法很简单:故意把某个阈值调到必然触发,确认能够收到通知后,再调回正常值。
- 把告警分级、值班表和首次响应指引同步给团队,最好做一次模拟演练。
这个流程一周内能跑完,覆盖核心业务的 80% 故障场景。剩下的时间,用在告警阈值调优和补充日志、链路追踪上。
5. 常见问题与排查技巧实录
5.1 告警没触发:那几个容易被忽略的坑
监控配好了,告警却不响,这种问题比“告警太多”更可怕,因为它是隐性失控。我总结几个实际踩过的坑。
第一个坑:采集目标丢失。Prometheus 的 targets 列表里如果出现红色 Down,那说明目标已经失联,所有的告警规则对这台机器来说都是摆设。造成 Down 的原因可能是 exporter 进程挂了、端口被防火墙拦截、也可能是机器本身已经宕机。我养成了一个习惯:给“采集目标本身 Down”配一条专门的告警,因为“监控不到”本身就是一种异常。
第二个坑:时间不同步。监控组件和服务端机器之间的时钟如果偏差超过一两分钟,像rate()这类基于时间窗口的计算结果就会变得很奇怪,告警可能提前、延迟,甚至根本不触发。排查告警不触发的问题时,第一件事就是检查各机器的时间同步。
第三个坑:进程重启导致指标重置。服务重启后,进程级指标会从零开始,如果告警规则里用了“持续增长”这类趋势判断,往往会产生误报或漏报。应对办法是给重启事件做标记,或者把告警的窗口期避开重启时刻。我处理过一个案例:一个服务每天凌晨做定时重启,每次重启后半小时内 CPU 告警必现,后来发现是进程启动时预热逻辑太慢,导致 CPU 长时间高位。这个告警本身其实是帮了大忙的。
5.2 告警轰炸与告警疲劳
前面说的告警风暴,我判断的标准是:如果值班同学每天处理的告警超过 50 条,那这个告警体系就是在制造问题而不是解决问题。告警疲劳的代价是真实的:我见过有人把手机消息通知关到只剩电话,结果非工作时间的故障全靠浏览器偶然打开 Grafana 才发现。
治理告警疲劳,我会按这个顺序来:先做去重和抑制,把告警量降下来;再做分级和通知改造成,让信息触达更精准;最后才动告警规则本身,删除那些没有实际处理价值的告警。后面这一步最需要业务判断,要通过复盘历史告警来判断:过去一周哪些告警触发了、触发了之后做了什么处理。处理动作是“看看再说”的,直接砍掉;处理动作明确的,保留并完善说明。
一个非常有效的做法是“每新增一条告警规则,必须同时提交一份处理指引”。没有处理指引的告警本质上就是在喊“狼来了”但不告诉你狼在哪、怎么打。这个习惯坚持下来,整个团队的告警质量会有质的提升。
5.3 兜底方案:监控本身挂了怎么办
这是一个容易被忽视的问题:监控系统也是系统,它也会出故障。Prometheus 机器磁盘满了、Alertmanager 宕了、日志采集端全部断连——监控系统自己挂掉时,谁来提醒我们?
兜底方案至少要覆盖两层。第一层是监控组件自身的高可用:Prometheus 在多副本部署时数据一致性复杂,小规模团队可以先做主备部署加定期备份,关键是把 Prometheus 自身的运行状态也纳入另一个独立监控通道。第二层是保留一个最基础的“逃生通道”:即使主监控挂了,也要有一套最基础的外部探测脚本,定期检查核心接口能否访问、首页是否返回 200。这套脚本越简单越好,甚至可以是一个独立机器上的 cron 任务,执行失败就发消息出来,不要依赖主监控的数据源。
最后,一定要做故障演练。不要只在文档里写着“告警应该触发”,要真的找一台测试机器把告警规则触发一遍,把通知、响应、升级的流程完整走一遍。我经历过一次演练:模拟核心数据库宕机,结果告警在 30 秒内到达了值班手机,但值班同学打开告警附带的面板链接,发现面板权限配错了,根本看不了数据。这种细节,不演练永远不会被发现。
我个人的体会是,线上故障的“第一时间发现”不是一个技术指标,而是一个组织习惯。它需要把监控指标、告警规则、值班流程、处理指引串成一个完整的闭环,并且持续用真实故障和演练来校准。这个闭环跑得越顺畅,你在凌晨三点被电话惊醒的概率就越低——即使被惊醒,你也会知道自己该看哪里、先做什么。最后分享一个小技巧:每次故障处理完,花十五分钟把“为什么没有更早发现”写进复盘里,并且一定要产出具体的监控改进事项。积累几次之后你会发现,真正成熟的系统不是不出故障,而是几乎所有故障都能在用户察觉之前被发现和扑灭。