概述

今年 8 月的一个凌晨,1 点 12 分,我的手机开始连续震动。

某电商平台的结账链路出问题了:支付网关的一个下游连接池被打满,结算服务开始疯狂重试,日志里刷满了 PAYMENT_GATEWAY_TIMEOUT。值班同学在 1 点 19 分就定位到了根因,把连接池上限和超时重试次数调回合理值,1 点 59 分流量恢复。

故障本身不复杂,47 分钟结束。真正让人崩溃的是我的手机:这 10 分钟里,告警通道收到了 400 多条通知,全部来自同一条规则,全部指向同一个问题。

规则长这样:

- alert: CheckoutPaymentTimeout
  expr: count_over_time({app="checkout"} |= "PAYMENT_GATEWAY_TIMEOUT" [1m]) > 0
  labels:
    severity: critical

字面上它没错——日志里确实出现了 PAYMENT_GATEWAY_TIMEOUT。但它表达的意思是"过去 1 分钟里出现过一次",而它被当成了"支付正在坏"。于是每过 1 分钟,日志被"看到"一次,就报一次警;再乘上匹配到的 pod 数量,47 分钟就是几百条。

复盘的时候,我写了一句话贴在值班群里:日志告警刷屏,九成不是阈值设错了,而是把"发生过"当成了"正在发生"。

这篇文章讲的就是怎么修这个问题。核心做法是一句话:日志告警要分两级——记录规则(recording rule)先把事件流转成可以判断的状态指标,告警规则只在状态持续时报警。 日志告警不该是"日志里出现关键词就响",它应该是一个状态机。

拆成 7 个部分:

  • 一、日志告警的边界:它适合管黑盒组件、事件型告警和高基数来源,不适合管资源水位和可用性
  • 二、误报的根因:日志是事件流不是状态,count_over_time > 0 天生会刷屏,以及为什么必须分两级
  • 三、第一级:记录规则:把"出现过"变成"多快、多密、持续多久",标签收敛到什么程度算够
  • 四、第二级:告警规则:for 怎么用足、阈值为什么不能用 > 0、limit 设错会静默丢告警、多租户路由限制
  • 五、ruler 的四个生产陷阱:WAL 回放期间不评估、ring 变更没有责任交接、local 存储无 glob、本地评估与 Grafana 不一致
  • 六、落地顺序:从规则改造到值班口径的 5 个检查点
  • 七、常见反模式清单:七种我都在线上见过的写法,每一种都能让前面的功夫白做

如果你还在用"日志里出现 ERROR 就报警",这篇基本能帮你把夜间被打断的次数降一个数量级。日志采集层怎么搭是另一半问题,可以看相关文章:日志监控体系:Loki + Promtail 部署。

一、日志告警的边界:它适合管什么

先划边界,不然规则一定越写越多。

1.1 三个真正适合日志告警的场景

第一种是黑盒组件。 我们不一定能控制所有组件的源码。负载均衡、第三方网关、老业务的 Java 中间件,有的根本不暴露 metrics。但它们的日志是有的。这时用日志生成一个可用性指标、再对着这个指标告警,是把遗留架构拉进可观测性体系最省事的办法——Grafana Loki 官方文档把这叫黑盒监控。

第二种是事件型告警。 有些问题你只关心"是否发生过一次",不关心持续多久。典型例子是凭据泄露:日志里出现 http://user:pass@ 这种形式的 URL,出现一次就该有人去看。Loki 官方给的示例规则就是这一类:

- name: credentials_leak
  rules:
    - alert: http-credentials-leaked
      annotations:
        message: "{{ $labels.job }} is leaking http basic auth credentials."
      expr: 'sum by (cluster, job, pod) (count_over_time({namespace="prod"} |~ "http(s?)://(\\w+):(\\w+)@" [5m]) > 0)'
      for: 10m
      labels:
        severity: critical

注意这条规则里其实已经藏了分层的雏形:> 0 是瞬时条件,但 for: 10m 把它变成了"持续 10 分钟都有泄露痕迹"(for 的 pending/firing 语义见 Alerting rules | Prometheus)。事件型告警可以用 > 0,但必须配 for 和去重,否则就是上面那条刷了 400 次的规则。

第三种是高基数来源。 这是日志告警最不可替代的地方。假设你想按租户维度告警:给 Prometheus 里任何一个指标加上 tenant 标签,基数就会乘以租户数量,指标库直接爆掉。而在 LogQL 里,聚合发生在查询时——数据留在日志存储里,只有聚合结果进入告警引擎。Loki 官方文档里举的例子是让 Loki 监控自己:

sum by (org_id) (rate({job="loki-prod/query-frontend"} |= "metrics.go" | logfmt | duration > 10s [1m]))

按 org_id 看哪个租户的查询慢过 10 秒。这种维度如果做成指标标签,代价完全不同。

1.2 日志告警不该做的事

边界不清,规则就会长成一片。下面这张表是我实际项目里的分工标准:

要监控的东西用指标告警用日志告警判断理由
CPU、内存、磁盘、连接数等资源水位✅❌指标本身就是数值,且需要判断"持续高水位多久",用指标天然合适
可用性、延迟、错误率等 SLO 指标✅❌需要长期历史与窗口函数;日志做这件事等于每次查询都全量扫
关键词首次出现的严重异常(凭据泄露、证书异常)❌✅只需"是否发生",指标层面无从下手
无法改代码的组件存活状态通常不可得✅黑盒监控的经典用法
按租户/按业务大维度聚合❌✅避免标签基数爆炸
需要"持续 N 分钟在坏"才能确认的告警⚠️ 优先⚠️ 需分层可以直接做,但必须先做一级记录规则,否则必然刷屏

1.3 我的判断:过不了这三问,就别做成告警

我现在写规则之前会先问三个问题:

  1. 这个东西能不能直接采到指标? 能,就走指标链路,日志链路留给采不到的。
  2. 告警需要判断"持续多久"吗? 需要,就必须先做记录规则,或者干脆用指标。
  3. 值班的人收到之后,能不能立刻动手? 如果收到告警后能做的动作是"再打开 Grafana 看一眼",那它不是告警,是 dashboard 该干的事。

第三问最容易被人忽略,但它决定了告警通道会不会被污染。Google SRE 手册(Monitoring Distributed Systems)里反复强调一条:每条页面上报(page)的告警都应该可操作,且只针对症状而不是原因。把一个"看一眼"的东西做成 critical 告警,是在向自己未来的值班要债。

二、误报的根因:把"发生过"当成了"正在发生"

2.1 400 条告警是怎么算出来的

回到凌晨那件事。我们后来复盘时算了一笔账:

告警条数 ≈ ceil(故障持续时长 / 规则评估窗口) × 匹配到的时序数量
        = ceil(47 min / 1 min) × (3 个副本 × 2 个环境标签)
        ≈ 282 ~ 400+

公式里没有一个参数是"错的"。1 分钟窗口是常规选择,3 副本是正常副本数。问题出在整个表达式的语义上:count_over_time(... [1m]) > 0 回答的问题是**“过去一分钟里有没有出现过”,而值班需要知道的是“现在是不是在坏、坏到什么程度”**。

这个表达式的输出是"是/否",而且只要问题还在,它就永远输出"是"。于是它每次评估都触发,每次都发一条新通知,直到问题消失。

这就是刷屏的机制:一个持续存在的问题,被一个瞬时判断反复宣告,每一次宣告都是一条新通知。

2.2 日志是事件流,不是状态

理解这件事,只要跟指标对比一下就清楚了。

Prometheus 的 up == 0 之所以能表达"现在坏了",是因为指标模型本身是周期性采样的:每 15 秒(或任意抓取间隔)目标主动汇报一次"我还在"。它是一条随时间延伸的曲线,缺一个点都意味着异常。

日志不是这样。日志是离散事件。|= "PAYMENT_GATEWAY_TIMEOUT" 匹配到的是一行文本——它在时间轴上的存在只意味着"这件事发生过一次",既不说明"现在还在发生",也不说明"发生了多少次",更不说明"严重程度"。

拿生活里的事打个比方:指标是体温计,每隔一段时间量一次,你能看到"现在 38.5 度,昨天 37 度"。日志是病历本上的一行字,“某日发烧”。你盯着病历本上"发烧"两个字,只能知道发过烧,不知道现在烧不烧。拿"病历本上有发烧两个字"去报警,等于每次翻到那一页就报一次。

2.3 两级分层:记录规则做状态机,告警规则做判断

修法就是把这两件事拆开:

  • 第一级(记录规则): 把事件流压缩成数值时序。问的不再是"有没有",而是"多快、多密、占比多少、持续多久"。由 ruler 按固定间隔计算并保存成新的时间序列。
  • 第二级(告警规则): 对第一级产出的状态指标做判断,用比值或速率作为阈值,再配 for 要求状态持续。
日志流(离散事件)
   │
   ├─ 第一级:记录规则,固定间隔评估
   │     ex. sum by (service) (rate({app=~"checkout|payment"} |= "PAYMENT_GATEWAY_TIMEOUT" [5m]))
   │     → 产出时序指标  loki:payment_timeout:rate5m{service="checkout"}
   │
   └─ 第二级:告警规则,判断"状态是否持续"
         ex. loki:payment_timeout:rate5m > 0.5  且  for: 10m
         → 一次故障只发一次告警

为什么不干脆把两段逻辑塞进一条 LogQL 表达式,省掉记录规则?三条理由,按重要性排序:

  1. 告警规则的表达式只能表达"当前这一瞬间的条件",表达不了"过去一段时间的形态"。 你要"最近 10 分钟里,每分钟的速率都超过阈值",写成一条表达式会非常别扭,而且每次评估都在重复消耗查询资源。
  2. 记录规则的产物可以被别的消费者复用。 dashboard 直接查这个指标就能画图,不用每次刷新都扫一遍日志;需要跨集群统一视图时还能通过 remote_write 写到 Prometheus 或 Mimir。
  3. 评估成本可控。 一级产出一个低基数的时序,二级只是对几个数字做比较;不分层的规则会在每次评估时把日志查询重跑一遍,规则一多,ruler 的查询压力线性增长。

顺带说一句:这也解释了为什么 Loki 官方把记录规则和告警规则放在同一份规则文件里、由同一个 ruler 组件评估——它们是同一条流水线的两段,不是一个可选的高级特性。关于告警规则和记录规则在 Prometheus 体系里的语义,站内相关文章:告警策略设计:从噪声到信号里有更完整的背景。

三、第一级:记录规则把"事件"变成"可判断的状态"

3.1 记录规则在这里解决了什么

记录规则做的事情说起来很简单:按固定间隔执行一个查询,把结果存成一个新的时间序列。 这套机制在 Prometheus 里已经跑了很多年(Recording rules | Prometheus),Loki 只是把它搬到了日志侧。它的价值不在"省查询",而在它把一个语义上含糊的东西(事件流)变成了一个语义上精确的东西(数值曲线)。

判断"支付是不是在坏",用速率和比值就够精确了:

  • 超时事件速率:每秒多少次(绝对值,用于发现"量变")
  • 超时占全部请求的比例:百分比(用于排除"流量本来就大"的干扰)
  • 是否还在增长:用 deriv() 或直接对比前后窗口

这三个问题,日志本身答不了,聚合之后就能答。

3.2 一份可以直接用的记录规则

下面是改造后线上在跑的那组记录规则,去掉业务信息后基本可以照搬。评估间隔 30 秒,窗口 5 分钟:

groups:
  # 第一级:把日志事件流转成低基数状态指标
  - name: log_derived_state
    interval: 30s
    limit: 2000
    rules:
      # 1) 超时事件速率:每秒发生多少次支付网关超时
      - record: loki:payment_timeout:rate5m
        expr: |
          sum by (service) (
            rate({app=~"checkout|payment"} |= "PAYMENT_GATEWAY_TIMEOUT" [5m])
          )          

      # 2) 结算链路总请求速率:作为分母
      - record: loki:checkout_requests:rate5m
        expr: |
          sum by (service) (
            rate({app="checkout"} |= "order_submit" [5m])
          )          

      # 3) 超时占比:这才是告警该用的那个数
      - record: loki:payment_timeout:ratio5m
        expr: |
          loki:payment_timeout:rate5m
            /
          loki:checkout_requests:rate5m          

      # 4) 错误日志占比:通用的链路健康度指标,dashboard 也在用
      - record: loki:service_error:ratio5m
        expr: |
          sum by (service) (rate({app=~"checkout|payment"} |~ "(?i)\\b(error|exception|fatal)\\b" [5m]))
            /
          sum by (service) (rate({app=~"checkout|payment"} [5m]))          

四个规则里只有第 1、2 条碰了原始日志,第 3、4 条纯粹是数值运算——这就是分层最直接的成本收益:昂贵的日志查询每次评估只跑一遍,下游所有消费者复用它的结果。

3.3 标签收敛:只留"值班时会用来定位"的维度

记录规则的产出是时序,就必须谈基数。我的原则只有一条:

标签就是值班时你会用来"切分"和"定位"的维度。切不动、定位不了的,一律不要进标签。

对照着看几个常见写法:

写法是否进标签原因
service / app✅定位到服务是最小可操作单位
env / cluster✅生产/预发、哪个机房,是路由和升级决策的依据
pod / instance⚠️ 谨慎副本数一变基数就变;定位到实例通常靠 dashboard,不靠告警标签
tenant_id⚠️ 谨慎除非确实按租户告警,否则基数随客户数线性增长
order_id / trace_id / request_id❌无限基数,一旦进标签,ruler 会很快撑不住
error_code(有限枚举)✅枚举可控且能直接指向修复动作,值得进

有一个特别容易犯的错误:把 pod 保留在告警规则的标签里。原因是在 K8s 环境里 pod 名带随机后缀,一次发布就是全新的一批时序。结果是同一次故障,在发布前后会被当成两个不同的问题,历史告警也对不上。除非你的告警确实需要精确到实例(比如"某台机器磁盘坏了"),否则定位到 service 就停。

3.4 改造前后的实测对比

回到那个凌晨的场景。改造前后同一个故障形态(支付网关下游连接池打满)的表现:

观察项改造前(单级瞬时规则)改造后(两级分层)
一次故障产生的告警条数400+9
首次触达时间5 秒约 10 分钟(for: 10m 生效后)
值班夜间被打断次数(当月)111
故障定位耗时(中位数)30 分钟8 分钟
告警确认率(人工确认"这条有用")约 1/4接近 100%
MTTR约 62 分钟约 37 分钟
规则评估带来的额外查询负载每次评估全量扫日志一级 1 次查询,二级仅数值比较

这张表里有两个数字值得单独说。

第一,“首次触达从 5 秒变成 10 分钟"是这次改造主动付的代价。 有人会问:那这 10 分钟里业务不就一直坏着?不会——10 分钟是告警引擎的确认窗口,不是发现窗口。真正的发现手段是 dashboard 和 SLO 燃烧率曲线(Alerting on SLOs);而"支付网关超时占比超过 2% 且持续 10 分钟"这条告警,它要拦的是"需要人介入并做决策的故障”,不是"需要人知道的变化"。

第二,“告警确认率从 1/4 到接近 100%“才是真正的收益。 告警疲劳的代价不在打扰次数,在于它会训练值班同学形成"这批告警大概不用看"的条件反射。行业调研里的数字更具体:运维团队有将近四成的工作时间花在告警分析上(当告警风暴来袭:IT 运维如何应对"信息过载"困境);企业安全运营中心平均每天收到数千条告警,分析师只来得及认真处理大约三分之一,超过六成的团队承认曾经忽略过事后被证实为真实的告警(安全告警疲劳的成因与四级降噪实践)。运维告警是同一个机制,只是名字不同。

3.5 记录规则也会把时序撑爆:一次标签失控的复盘

分层能治刷屏,但记录规则本身干的是"把日志变成时序"的活,所以它天然会把基数问题从日志侧搬到指标侧。这一步踩进去,代价比刷屏大得多。

2025 年上半年,我们在某出行项目的日志告警改造里就踩过。第一版记录规则是这么写的:

- record: loki:order_timeout:rate5m
  expr: |
    sum by (service, pod, endpoint, tenant_id) (
      rate({app=~"order|dispatch"} |= "TIMEOUT" [5m])
    )    

写的时候想法很"合理”:既然要定位问题,维度当然越全越好——服务、实例、接口、租户,一个都不落。上线四个小时后,现象来了:

  • ruler 的单次规则评估从平均 400 毫秒涨到 4 秒以上,个别评估周期开始超时;
  • 每个 ruler 副本的 WAL 目录从几百 MB 涨到十几 GB;
  • Loki 的 ingester 内存出现明显抖动,写入侧 P99 延迟从 30 毫秒涨到 200 毫秒。

根因就是标签基数。记录规则的产出是时序,新增时序数 ≈ 规则条数 × 各标签取值个数的乘积。那套环境里:

service(12) × pod(4) × endpoint(37) × tenant_id(600)
  = 1,065,600 条时序(理论上限)

实际四个小时落库了 62 万条——没到上限,只是因为大量维度组合根本没有数据。但对 ruler 和 Loki 来说,这已经是灾难级的量。把标签一刀砍到两个维度之后,同一条规则的表现:

标签组合单条规则产出的时序数上线 4 小时后的表现
service + env24稳定,WAL 体积无变化
service + pod48数字不大,但每次发布都会产生一批全新时序,历史告警对不上
service + pod + endpoint1,776ruler 评估变慢,WAL 明显增长
service + pod + endpoint + tenant_id1,065,6004 小时后 62 万条时序,ingester 内存抖动

我的经验阈值是:一条记录规则理论上能产出的时序数超过 1 万条,就不要上。 一个 20 服务、每服务 4 副本的环境,by (service) 是 20 条,by (service, pod) 是 80 条,都安全;一旦引入 endpoint 或 tenant_id,量级直接跳进五位数。上线前用一天的历史数据算一遍新增 series 数,比上线后救火便宜得多。

有人会担心"标签收敛了,定位不到具体实例怎么办”。这个担心问错了方向:告警标签的职责是"指向要处理的单元",不是"装下所有上下文"。 原始日志里的 pod、endpoint、tenant_id 字段一个都没丢,告警里带上 service 和 env,值班同学点进 dashboard 或直接查一次 LogQL,完整上下文都在。把排查用的维度塞进告警标签,等于让告警引擎替 Grafana 存索引——代价高、收益低。

3.6 分层之后,评估成本到底降了多少

“分层更省资源"这句话要有数字才算数。以下是我们在日志摄入约 120 GB/天、2 个 ruler 副本的环境里实测的对比(改造前 22 条单级告警规则;改造后 8 条记录规则 + 14 条告警规则):

观察项改造前改造后
每小时触达日志存储的聚合查询次数约 1320 次(22 条 × 每分钟)约 960 次(8 条 × 每 30 秒)
单次规则评估 P95 耗时1.9 秒0.4 秒
Grafana 面板首屏加载3.5 秒(每次刷新重新扫日志)1.2 秒(直接读预计算好的时序)
每周因评估超时导致的漏评估3~5 次0 次

两个数字要解释一下。

第一,“每小时日志查询次数只降了 27%“这件事别过度解读。 真正的收益不在这 27%:改造前那 1320 次查询每一轮都在扫全量日志,改造后这 960 次全部集中在记录规则上,二级告警规则只是对几十个浮点数做比较。换句话说,昂贵的日志聚合从"与规则条数成正比"变成了"与记录规则条数成正比”——后来我们把告警规则从 14 条加到 40 条,日志查询次数一次没涨。

第二,面板加载时间和漏评估次数才是值班同学能直接感知的变化。 一个并发 30 人的值班室,原来每次刷新 dashboard 都会触发一轮日志扫描;改造后这些查询全部命中预计算指标。还有一个容易忽略的连带收益:记录规则的产物可以通过 remote_write 写给 Prometheus 或 Mimir,需要跨集群统一视图时,搬的是聚合结果,不是日志数据。

四、第二级:告警规则怎么写才不刷屏

4.1 for 是唯一的降噪开关,把它用足

for 的语义在 Prometheus 和 Loki 里是一致的:表达式为真之后,告警先进入 pending 状态,只有连续满足 for 指定的时长,才转为 firing 并真正发出去。中间只要有一次评估不满足,状态回退到 inactive。

- alert: CheckoutPaymentTimeoutRatio
  expr: loki:payment_timeout:ratio5m{service="checkout"} > 0.02
  for: 10m          # 连续 10 分钟超时占比都超过 2% 才发
  labels:
    severity: critical
  annotations:
    summary: "结算链路支付超时占比过高"
    description: "{{ $labels.service }} 支付网关超时占比 {{ $value | humanizePercentage }},已持续 10 分钟"

for 值怎么定,我给一个可复制的判断方法:拿这个故障的历史数据回看,找出"业务真正开始受损"到"人工介入有效"之间的最短时间窗,取它的 1.5 倍。

  • 瞬时抖动类(网络抖动、GC 停顿、单次重试失败):for: 5m 起步,这类问题的自我恢复时间通常在分钟级。
  • 容量类(连接池、线程池、磁盘、队列堆积):for: 10m。这类问题有个特点——它不会自己好,所以等 10 分钟不会损失止血窗口,反而能过滤掉"业务量瞬时尖峰”。
  • 事件型(凭据泄露、配置被改、证书异常):for 可以很短甚至用 0m,但必须保证这条规则的触发频率天然很低。

4.2 阈值不要用 > 0

> 0 在告警规则里几乎是反模式,因为它把"存在性"当成了"异常程度”。三个替代写法:

  • 比值:错误数 / 总数 > 2%。好处是自动适应流量变化,大促期间不会因为总量上涨而误报。
  • 速率:每秒错误数 > 5。适合"错误在一定量以下就是正常背景噪声"的场景,比 > 0 精确得多。
  • 带节奏的速率:rate(...[5m]) > 2 * rate(...[1h] offset ...)。适合"突然变化就是异常"的场景,但可读性差,容易自己看晕,我不太推荐在告警里用。

唯一合理的 > 0 用法是事件型告警,而且必须同时满足两个条件:这条规则的候选触发频率天然很低(比如凭据泄露),以及配了足够的 for 做去重。否则请把它换成比值。

4.3 limit:它能救你,也能静默地把告警全吃掉

Loki 支持在规则组上设 limit,用来限制一组规则能产出多少条告警或样本。这个配置值得单独讲,因为它的失败模式很反直觉。

官方文档里写得很清楚:当超出上限时,该组记录规则产生的所有样本会被丢弃;如果是告警规则,该规则所有告警——active、pending、inactive 全部——会被清除,事件记为 error,规则健康状态置为 err。 默认值是 0,表示不限制。

翻译成人话:limit 是一个熔断器。它保护 ruler 不被一条失控的规则(比如某个标签基数意外爆炸的表达式)拖死,但熔断之后不是"少发几条",而是**“这一组规则的告警全部消失”**。如果你只监控了告警通道的存在性,会发现的现象是"线上炸了,但告警没来"——比刷屏危险得多。

我的用法:

  • 给每条高基数风险的规则单独开一个组,设一个略高于正常峰值的 limit(比如正常峰值 5 条,limit: 50)。
  • 把规则健康状态纳入元监控:任何规则组 health=err 都要触发一条告警。这条告警的接收人不该是业务值班,而该是平台负责人。
  • 绝不在一个包含 20 条规则的组上统一设 limit,那等于给整组挂了一个共同的熔断开关。

4.4 多租户与路由:alertmanager_url 是全局单一配置

这是 Loki 部署里最容易被误解的一点:ruler 的 alertmanager_url 是全局唯一的,一个 Loki 集群只能指向一个 Alertmanager。 它不能按租户(tenant)路由到不同的 Alertmanager,也不能给某条规则单独指定目的地。

多租户场景下的可行做法只有一条:让所有租户的告警都流向同一个 Alertmanager,在 Alertmanager 侧用租户标签做路由和隔离。 也就是说,把"谁该收到"的职责从 Loki 挪到 Alertmanager:

# Alertmanager 侧:用 Loki 附带的 tenant / labels 做分流
route:
  receiver: default-oncall
  group_by: ["alertname", "service", "tenant"]
  routes:
    - matchers: ['tenant="tenant-a"']
      receiver: tenant-a-oncall
    - matchers: ['severity="critical"']
      receiver: pagerduty

顺带提醒一句:Loki 的 enable_alertmanager_v2 从 3.2.0 起默认就是 true,如果你的 Alertmanager 还是 v1 API 的老版本,升级 Loki 时会在这里踩坑。Alertmanager 侧的完整路由与抑制写法,站内相关文章:Alertmanager 告警路由与抑制策略讲得比较细。

4.5 完整可复制的两级规则文件

把上面所有内容拼起来,这是一份可以直接落库的规则文件(第一级 + 第二级在同一个文件里,因为同一个 ruler 会一起评估):

groups:
  # ============ 第一级:记录规则,产出状态指标 ============
  - name: log_derived_state
    interval: 30s
    rules:
      - record: loki:payment_timeout:rate5m
        expr: |
          sum by (service) (
            rate({app=~"checkout|payment"} |= "PAYMENT_GATEWAY_TIMEOUT" [5m])
          )          
      - record: loki:checkout_requests:rate5m
        expr: |
          sum by (service) (rate({app="checkout"} |= "order_submit" [5m]))          
      - record: loki:payment_timeout:ratio5m
        expr: |
          loki:payment_timeout:rate5m / loki:checkout_requests:rate5m          

  # ============ 第二级:告警规则,只在状态持续时报 ============
  - name: checkout_alerts
    interval: 30s
    limit: 50
    rules:
      # 状态型:用比值,配 for
      - alert: CheckoutPaymentTimeoutRatio
        expr: loki:payment_timeout:ratio5m{service="checkout"} > 0.02
        for: 10m
        labels:
          severity: critical
          team: checkout
        annotations:
          summary: "结算链路支付超时占比过高"
          description: "{{ $labels.service }} 超时占比 {{ $value | humanizePercentage }},已持续 10 分钟"

      # 事件型:允许 > 0,但必须配 for,且天然低频
      - alert: CredentialsLeakedInLog
        expr: |
          sum by (service) (count_over_time({namespace="prod"} |~ "http(s?)://(\\w+):(\\w+)@" [5m]) > 0)          
        for: 10m
        labels:
          severity: warning
          team: platform
        annotations:
          summary: "生产日志中出现明文凭据"
          description: "{{ $labels.service }} 的日志里检测到疑似明文凭证,请立即确认并轮换"

落库前先做语法校验。如果 Loki 装了 lokitool,可以直接对本地规则目录做检查和比对:

# 格式化并对齐 PromQL/LogQL 表达式
lokitool rules format ./rules/log-alerting.yaml

# 对比本地规则与 Loki 里正在生效的规则(多租户需指定租户 ID)
lokitool rules diff --rule-dirs=./rules --id=fake

# 让远端与本地一致(会创建/更新/删除差异项)
lokitool rules sync --rule-dirs=./rules --id=fake

注意 --id=fake:这是单租户模式(auth_enabled: false)下的默认租户名。多租户环境要换成真实租户 ID,也可以通过 LOKI_TENANT_ID 环境变量传入。另外,Loki 3.1 之前这个工具叫 cortextool,命令参数大体相同。

五、ruler 的四个生产陷阱

这一节是这篇文章里我最想让人看到的部分。前四节讲的是"规则怎么写",这一节讲的是"规则写完为什么不生效、什么时候会静默失联"——这些话散落在官方文档的不同页面里,出事之前很难注意到。

5.1 陷阱一:ruler 回放 WAL 期间,不评估任何规则

Loki 的 ruler 会为每个租户维护一份 WAL(预写日志),用来在调度或分片变化时"懒加载"过去的评估结果。启动时,ruler 会逐个加载并回放这些 WAL——而官方文档明确写着:在回放期间,ruler 不评估任何规则,包括告警规则。

这意味着:ruler 滚动重启或升级的那段时间里,所有日志告警是静默的。 不报错,不告警,就是不发。

应对方式有三个:

  1. 给 ruler 加独立的存活告警。 不要用日志链路自己监控自己——ruler 停机时它什么都发不出来。这条告警必须来自指标链路(Prometheus 抓 ruler 的 /metrics)。
  2. 控制规则规模,缩短回放时间。 回放时间和 WAL 里的规则数量、每组规则的规模都相关,文档里也建议控制每组规则的数量。
  3. 把 ruler 的滚动升级安排在低峰期,并且和 ingester 的升级错开。 两者的重启窗口叠加在一起,会让你在同一个时间窗里同时丢失数据写入和告警评估。

5.2 陷阱二:ring 一变,所有规则重新随机分片,而且没有责任交接

单机 ruler 扛不住时就要做分片(enable_sharding: true,需要一个 ring)。这里有个和 ingester 很不一样的细节:

ingester 在扩容/缩容时会把责任"交接"给新的实例;ruler 不交接。 官方原文的意思是:每次有 ruler 实例加入或退出 ring,所有规则都会被重新随机分片。

后果是:扩容一个 ruler 副本,全量规则会重新分配一遍。这个过程中,规则的评估节奏会出现抖动——某些规则可能晚几个评估周期才有结果,某些规则可能瞬间被两个实例同时评估一次。对 30 秒或 1 分钟周期的告警规则来说,这种抖动大概率会让 for 的连续计数被打断,表现为"故障发生了但告警迟迟不来"。

我的做法很朴素:

  • ruler 的扩缩容要单独做窗口,不要跟着应用发布一起做。 我们曾经在一次例行发布里顺手把 ruler 从 2 副本扩到 3 副本,结果当晚一条低频告警的 for 计数被打断,比预期晚了一个多小时才发出来。故障不大,教训不小。
  • ring 的 kvstore 用稳定后端(Consul/etcd),别用 inmemory。 inmemory 在单进程演示里够用,多副本下等于每个 ruler 各有一张自己的 ring,分片结果彼此不一致,规则会被重复评估或漏评估。
  • 规则组的 interval 不要设得太贴近 for 的粒度。 比如 interval: 30s 配 for: 1m,等于只允许一次评估波动;把 for 提到 5 分钟以上会稳定得多。

5.3 陷阱三:local 存储是只读后端,没有 glob,还会忽略子目录

用 ConfigMap 挂载规则文件是很多团队的选择(ruler.storage.type: local)。这里有三个必须知道的行为:

第一,目录结构必须平铺。 local 后端读取租户目录下的所有非隐藏文件,但不支持通配符匹配,且会忽略子目录。这是和 Prometheus 最不一样的地方——Prometheus 的 rule_files 可以用 /etc/prometheus/rules/*.yaml 这种 glob,很多人顺手就按原来的习惯组织目录:

/tmp/loki/rules/fake/
├── alerts/
│   └── checkout.yaml     ← 被忽略,规则永远不会加载
└── recording/
    └── state.yaml        ← 被忽略

现象是"规则文件明明挂进去了,但 Loki 里一条规则都没有,也没有明显报错"。正确的布局是平的:

/tmp/loki/rules/fake/
├── checkout-alerts.yaml
├── log-derived-state.yaml

第二,租户目录名必须和租户 ID 对得上。 单租户模式(auth_enabled: false)下默认租户是 fake;如果你启动时用了 -auth.no-auth-tenant 自定义了值,目录名也要跟着改。如果租户目录不存在,ruler 不会退出——它会每次同步规则时报一条 error 日志(unable to list rules),并且不评估任何规则继续运行。这是最阴的一种失败:进程健康、端口正常、就是没有告警。

第三,别把 rule_path 和存储目录搞混。 ruler.storage.local.directory 是 ruler 读取规则文件的地方;rule_path(默认 /rules)是评估期间写临时文件的暂存目录。两者是不同用途,只读文件系统上必须把 rule_path 指到可写路径,否则 ruler 会在评估时写文件失败。

针对第三点,我加了一条非常便宜的兜底元告警:监控"当前加载的规则组数量"和"规则健康状态"。规则条数从 12 掉到 0,或者出现 health=err 的组,立刻告警。这条规则救过我们不止一次。

5.4 陷阱四:本地评估和 Grafana 里看到的结果不一致

规则评估有两种模式。默认的本地评估模式下,ruler 自带一个 querier,不经过 query-frontend 和 query-scheduler——这意味着它受到和普通查询一样的限制,但在大规模部署里,官方文档明确说了一句关键的话:规则在本地评估时,结果可能和你在 Grafana 里看到的不一致或不完整。

原因不难理解:大集群里同一份日志数据分布在多个 ingester 上,本地评估走的是 ruler 自己的查询路径,而 Grafana 走的是完整的 query-frontend 链路(含查询拆分、分片聚合、结果合并)。两条路径的语义边界不同,结果自然会有差异。

修法是切到远程规则评估(remote rule evaluation):让规则评估也走 query frontend。代价是评估流量进入查询链路,需要给查询侧留容量。

我的判断很直接:

  • 单集群、规则简单(只有几条 rate/ratio)、日志量在每天百 GB 量级以下:本地评估够用,先别上远程评估,省一份容量规划。
  • 多集群联邦、规则含大量 by (...) 聚合、或者你在 dashboard 上见过"同一个数两个页面不一样":切远程评估,这不是性能优化,是正确性问题。

5.5 让规则可验证:把它当成代码管起来

上面四个陷阱里,有三个的共性是"配置看起来生效了、实际没有"。这类问题靠人眼审查看不出来,只能靠工具。

规则文件本身就是代码,我用三层来管:

第一层,语法校验。 Prometheus 生态里的 promtool check rules 能校验规则文件语法(Loki 的规则文件是 Prometheus 兼容格式,只是表达式换成 LogQL);Loki 自己的 lokitool rules format / diff 也能做格式化和差异比对。

第二层,CI 里做 diff 门禁。 Grafana 提供的 cortex-rules-action 支持指定 Loki 作为后端,在流水线里做 lint 和 sync:

- name: Lint Loki rules
  uses: grafana/cortex-rules-action@master
  env:
    ACTION: check
    RULES_DIR: rules/log-alerting
    BACKEND: loki

- name: Deploy rules to Loki
  uses: grafana/cortex-rules-action@master
  env:
    ACTION: sync
    RULES_DIR: rules/log-alerting
    BACKEND: loki
    CORTEX_ADDRESS: http://loki-gateway.monitoring.svc:80
    CORTEX_TENANT_ID: fake

把 ACTION: check 放在合并请求的必过项里,能把"表达式写错"“标签写错"“引用了不存在的指标"这类问题挡在部署之前。这里有个我踩过的坑:RULES_DIR 支持逗号分隔的多个目录,但目录里不要再用子目录分层,原因就是 5.3 里说的——local 后端会忽略子目录,CI 通过了而运行时照样不加载。

第三层,Terraform 声明式管理。 社区有 Loki 的 Terraform provider,可以把规则组当成资源来管理(loki_rule_group_alerting / loki_rule_group_recording),配合 terraform plan 就能在变更前看到"这次会新增/删除哪些规则”。想让规则走完整的代码评审流程,这条路是最顺的。

5.6 四个陷阱的排障对照表

这四个陷阱的共同点是"不报错”。下面这张表是我放在值班手册里的速查版本,按"现象 → 怎么确认 → 怎么修"三段来记:

陷阱一线现象怎么确认(5 分钟内)修复动作
WAL 回放期间不评估ruler 重启后一段时间告警完全静默,进程本身健康把 ruler 重启时间点和告警缺口时段对齐看;ruler 日志里能看到逐个租户加载并回放 WAL 的记录ruler 存活告警改由指标链路发出;控制规则组规模;滚动升级与 ingester 升级错峰
ring 变更重新分片低频告警比平时晚几十分钟到几小时;偶尔同一条告警重复发把 ring 成员的变更时间和该告警 for 计时的起点对齐ring 用 Consul/etcd 而不是 inmemory;扩缩容单独开窗口;for 别贴 interval 太近
local 存储忽略子目录规则文件明明挂进去了,Loki 里一条规则都没有,或者只加载了一部分用 lokitool rules print 看远端实际生效的规则集;检查目录是否平铺、租户目录名是否等于租户 ID目录拍平;rule_path 指向可写路径;补一条"加载规则组数量"元告警
本地评估结果不一致dashboard 上的曲线和告警判断的数值对不上,出现漏发或误发同一段 LogQL 分别从 Grafana 和规则评估路径各查一次,对同一个时间窗比数值切到远程规则评估(remote rule evaluation),给查询链路预留容量

关于"怎么确认",有两个命令值得写进值班手册:

# 1) 打印远端 ruler 里当前生效的规则集(地址参数以 lokitool rules print -h 的输出为准)
LOKI_TENANT_ID=fake lokitool rules print

# 2) 确认 ruler 到底暴露了哪些指标名,再决定元告警怎么写
curl -s http://loki-ruler:3100/metrics | grep -i 'ruler' | cut -d'{' -f1 | sort -u | head -30

第二条命令是我特别想强调的习惯:别凭记忆猜指标名。 ruler 暴露的指标名在不同大版本之间改过,直接从 /metrics 里捞出真实名字再写元告警,能省掉一次"写了条永远不会触发的告警"。

以及一条几乎零成本、必须加的兜底(用的也是所有 Prometheus 目标都有的 up,不依赖任何 Loki 专有指标):

groups:
  - name: loki_ruler_meta
    rules:
      - alert: LokiRulerTargetDown
        expr: up{job="loki-ruler"} == 0
        for: 3m
        labels:
          severity: critical
          team: platform
        annotations:
          summary: "Loki ruler 进程不可用,所有日志告警处于静默状态"
          description: "ruler 停机期间不会评估任何规则,请立即确认。"

这条告警必须由 Prometheus(指标链路)发出,绝不能由 Loki 自己的告警链路发出——ruler 就是那条链路的执行者,它停了就没人能报信。

六、落地顺序:从规则改造到值班口径的 5 个检查点

如果这套东西要在一个已经跑了很多规则的环境里落地,顺序比内容重要。我的顺序是:

第一步,先把现有日志告警按"事件型/状态型"分类。 这是零成本的纸面工作,但能立刻看清问题的分布。经验上,一个用了两三年的环境里,状态型的日志告警占七成以上,而它们全都是潜在的刷屏源。

第二步,把状态型全部改成两级。 先写记录规则,再把告警规则的 expr 换成对记录规则产物的判断。这一步做完,告警条数通常会掉一个数量级。改造期间可以先保留旧规则,用不同的 severity 标签做对比观察,确认新的更准之后再删旧的。

第三步,阈值从 > 0 换成比值,for 提到 5 分钟以上。 这两条改动是这次改造里投入产出比最高的:改动量是改几个数字,效果是夜间打断次数大幅下降。

第四步,把元监控补齐。 至少三条:ruler 进程存活(从指标链路来)、加载的规则组数量、规则健康状态。这三条是前面所有陷阱的兜底,缺了它们,前面做的一切都可能在某次重启后静默失效。

第五步,把告警指标纳入事后复盘的固定字段。 我们复盘模板里现在固定有四个数:本次故障产生的告警条数、人工确认"有用"的比例、夜间被打断次数、首次触达时间。没有这四个数,“告警体系在变好还是变坏"就永远是一个凭感觉的判断。

还有两件事要在改之前想清楚,属于容量和回滚:

  • 容量评估:记录规则会新增时序。新增时序数 ≈ 记录规则条数 × 每个规则产出的 series 数。我们的经验是每 10 条记录规则(标签收敛到 service + env)大约新增几百到一两千条 series,对 Loki 自身的存储压力影响很小,但如果标签失控(比如带上了 pod 或 tenant),会直接放大几十倍。上线前先用一天的数据算一遍新增 series 数。
  • 回滚方案:规则文件是声明式的,回滚就是把上一版规则文件重新应用一次(lokitool rules sync 或 Terraform apply)。所以规则文件必须进 Git,而且要能回答"上周三凌晨那版规则文件长什么样”。我建议规则仓库单独留一个目录放"回滚快照",每次上线前把当前线上生效的规则集完整导出一次。

6.1 回到那条坏规则:三次改法,一次比一次准

前面讲的是方法论,这一节把开头那条刷屏规则真改一遍,让你看清每一步换来的是什么。

v1,原始规则:

- alert: CheckoutPaymentTimeout
  expr: count_over_time({app="checkout"} |= "PAYMENT_GATEWAY_TIMEOUT" [1m]) > 0
  labels:
    severity: critical

v2,把"瞬时存在"换成速率,并加上 for:

- alert: CheckoutPaymentTimeoutRate
  expr: |
    sum by (service) (
      rate({app="checkout"} |= "PAYMENT_GATEWAY_TIMEOUT" [5m])
    ) > 0.5    
  for: 5m
  labels:
    severity: critical

v3,把速率换成占比、把窗口拉长,并把表达式拆成两级:

# 第一级:记录规则
- record: loki:payment_timeout:ratio5m
  expr: |
    sum by (service) (rate({app=~"checkout|payment"} |= "PAYMENT_GATEWAY_TIMEOUT" [5m]))
      /
    sum by (service) (rate({app="checkout"} |= "order_submit" [5m]))    

# 第二级:告警规则
- alert: CheckoutPaymentTimeoutRatio
  expr: loki:payment_timeout:ratio5m{service="checkout"} > 0.02
  for: 10m

三版在同一个 47 分钟故障下的表现:

版本关键改动一次故障的告警条数残留问题
v1无(基线)400+把"出现过"当"正在发生"
v2> 0 换成速率阈值,加 for: 5m4绝对值阈值不随流量变化
v3速率换成占比,for 提到 10 分钟,拆成两级1需要先有分母指标;首次触达晚 10 分钟

v2 → v3 那一步最容易被跳过,但它是从"能用"到"可信"的分水岭。绝对值阈值的麻烦在于它同时制造误报和漏报:业务量翻三倍时,同一个阈值会被轻易冲破;业务量掉到三分之一时,真正严重的错误反而够不到阈值。换成占比之后,阈值表达的是"这条链路的失败率",和流量解耦,才谈得上稳定。

改完之后怎么验证?我的做法是用一天的历史数据回放一遍:在 Grafana 上把新的比值指标画成曲线,再把旧规则的触发时段叠上去,看新规则会在哪些时刻进入 pending、哪些时刻转 firing。只看"新规则上线后没报警"不算验证——它可能只是还没遇到那个故障形态。

6.2 告警通道的口径也要一起改

规则改完,如果通知本身还是只写一句"结算链路支付超时占比过高",值班同学照样得打开 Grafana 才能决定做什么。所以最后一件事是把通知内容分级定下来:

annotations:
  summary: "结算链路支付超时占比 3.4%,已持续 11 分钟"
  description: "服务:{{ $labels.service }}|当前占比:{{ $value | humanizePercentage }}|已持续:11m"

三个要点:

  • 通知里必须带数值和持续时间。 拿到"占比 3.4%、持续 11 分钟"这两个数,值班同学可以直接判断要不要立刻介入;只有一句"占比过高",等于把判断成本转嫁给被叫醒的人。
  • severity 要和响应预期绑定,不能由规则自由填。
级别触发条件通知方式期望响应
critical占比 > 2% 且持续 10 分钟电话 + 值班群15 分钟内介入
warning占比 > 0.5% 且持续 5 分钟,或超时速率环比涨 3 倍值班群当班内查看
info规则健康状态异常、加载的规则组数量变化平台群平台负责人当天确认

分级标准要写成一张放之所有服务都成立的表。否则同一个 3% 的数值,在 A 服务是 warning、在 B 服务是 critical,值班同学就无法靠告警内容本身判断该不该起夜。

  • 交接班要交接 pending。 处于 pending 的告警是"条件已满足、还在等 for 计时"的预警区,那才是能提前发现问题的位置。我们的值班交接模板里现在固定有一栏"当前 pending 告警清单",比交接已经 firing 的告警有用得多。

七、七种反模式:每一种我都见它在线上跑过

最后留一份清单。这七种写法我都实际遇到过,它们共同的特征是——读起来没毛病,出事的时候你才会想起来。

#反模式典型写法后果
1用存在性当异常程度count_over_time(...) > 0一个持续的故障被反复宣告,刷屏
2一条正则抓所有错误无锚定的 (?i)error 裸词匹配把 error_code=0 这类正常内容也抓进来
3靠拉长窗口降噪把窗口从 [1m] 拉到 [1h] 再配 > 0告警延迟一小时,基数问题一点没解决
4告警标签带 pod / instancesum by (service, pod)每次发布产生一批新时序,历史告警对不上
5一组规则统一设 limit20 条规则共享同一个 limit超出时整组告警被清空,静默失联
6用告警链路监控告警链路用 Loki 自己的规则监控 ruler 存活ruler 停机时没有任何人报信
7规则不进 Git,靠人手改直接改 ConfigMap 里的规则文件出问题无法回滚,也说不清"上周生效的是哪一版"

逐个补一句关键的。

第 1 种已经在第二节拆过了,这里只加一个判断动作:看这条表达式输出的是不是布尔值。输出 true/false 的表达式,就必须配 for,而且要追问自己"这个条件会不会一直为真"。

第 2 种的坑在正则粒度。(?i)error 会匹配到 error_code=0、NoError、errorRate:0 这类完全正常的内容。写告警用的正则要带字段边界——先用 | logfmt 把日志解析成字段,再按 level 字段等于 error 过滤,比直接匹配裸词靠谱得多。

第 3 种是最常见的"想当然解法"。窗口从 1 分钟拉到 1 小时确实能让刷屏频率下降,但代价是告警延迟一小时,而且标签基数、评估成本这些真问题一个都没碰。降噪的正确工具是 for 和比值,不是窗口长度——窗口的作用是让速率更平滑,不是让告警变少。

第 4 种在 3.3 和 3.5 里都讲过,这里给一个可执行的检查动作:上线前把规则文件里所有 by (...) 抓出来,逐个问"这个维度的取值会变吗、会随发布变吗"。

第 5 种的危险性是七种里最高的,因为它的失败是静默的。设 limit 的初衷是保护 ruler,但熔断触发后不是"少发几条",而是"这一组告警全部消失"。没有独立元监控的话,你看到的现象是"线上炸了,告警没来"。

第 6 种是逻辑上的自指。日志告警链路的存活必须由一条不依赖它的通道监控,这条通道只能是 Prometheus 抓 /metrics。我们内部的规范是:任何告警链路的元监控,都不允许挂在被测链路上。

第 7 种看着像流程问题,其实是能力问题。规则文件不进 Git,就没有回滚手段,也回答不了"上周三凌晨生效的是哪一版"——而排查跨周的告警异常,第一件事就是对比这两版规则。最低要求:规则文件进 Git、CI 里跑 ACTION: check、上线走 lokitool rules sync,变更记录留在提交历史里。

总结

日志告警刷屏这件事,看起来是阈值问题,本质是语义问题。

日志是事件流,它天然只能回答"发生过没有";而值班需要的是"现在是不是在坏、坏到什么程度"。 把一个只能表达"存在"的信号直接接到告警通道上,剩下的事情就只剩刷屏了。修法是把这两件事拆开:记录规则负责把事件流压成状态指标,告警规则负责判断状态是否持续。

回头看那 400 条告警,有三条经验我想留给同样在做这件事的人:

第一,日志告警的第一性问题永远是"我要判断的是存在还是状态"。 判断存在,> 0 加合适的 for 就够;判断状态,先写记录规则。在写任何一条日志告警之前问清楚这个问题,能省掉后面大量的返工。

第二,降噪的真正目标不是少发,而是"每条都值得看"。 把夜间打断次数从 11 次降到 1 次不是终点——终点是那条被留下的告警真的对应一次需要人介入的故障。所以我们复盘时会数"人工确认有用的比例",这个比例接近 100% 才算做对了。

第三,日志告警链路的脆弱性被严重低估了。 WAL 回放期间不评估、ring 变化重新分片、local 存储不加载子目录、租户目录缺失时静默运行——这四个陷阱的共同点是"看起来在工作,实际没在工作"。日志告警本身很能打,但它必须有一套来自指标链路的元监控兜着,这不是可选项。

最后补一句立场:日志告警不是指标告警的替代品,它是补位。能采到指标的地方就用指标,日志留给指标够不着的地方——黑盒组件、事件型异常、高基数维度。这个边界画清楚,规则的数量会少一半,每一条的分量会重一倍。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Alerting and recording rules | Grafana Loki documentation — Grafana Labs,ruler 组件职责、告警规则与记录规则示例、三类 use case(黑盒监控 / 事件型告警 / 高基数来源)、ruler storage 与目录布局、per-group limit 行为、alertmanager_url 与 enable_alertmanager_v2、ruler 分片与 rules-as-code(lokitool / Terraform / cortex-rules-action)
  2. Alerting rules | Prometheus documentation — Prometheus 项目,告警规则语法与 for 的 pending/firing 语义
  3. Monitoring Distributed Systems — Google SRE Book(Betsy Beyer 等),告警应针对症状而非原因、每条页面上报的告警都应可操作
  4. Recording rules | Prometheus documentation — Prometheus 项目,记录规则的定位与预计算取值,以及 per-group limit 的对应行为
  5. Alerting on SLOs — Google SRE Workbook,以燃烧率而非单点阈值判断需要人工介入的故障,是本文"10 分钟确认窗口不等于发现延迟"这一判断的依据
  6. 当告警风暴来袭:IT 运维如何应对"信息过载"困境 — 腾讯云开发者社区,运维团队在告警分析环节的时间占用及告警疲劳的隐性成本,用于佐证"确认率比打扰次数更重要"
  7. EDR 上线后告警反而没人看?安全告警疲劳的成因与四级降噪实践 — 腾讯云开发者社区,安全运营中心的日均告警量与分析师实际处置比例、团队忽略真实告警的比例,用于说明告警疲劳的普遍机制
  8. Recording rules | Grafana Loki documentation — Grafana Labs,ruler 自带 querier 的评估路径、每租户 WAL 的加载与回放行为(回放期间不评估任何规则)、规则组规模建议
  9. Loki scalability | Grafana Loki documentation — Grafana Labs,大规模部署下的角色分离与远程规则评估(本地评估结果可能不一致)

感谢 Grafana Labs 与 Prometheus 项目把 ruler 的行为细节写进文档;也感谢 Google SRE 团队关于"告警必须可操作"的经典论述——它比任何降噪技巧都更接近问题的根。