← 返回 Day 182 正文

Day 182 · 图解

Day 182 — 原理图解

六张图,从「数据怎么流」到「为什么平台重启后会漏掉恢复」。


图 1 · 四层链路:一份数据怎么变成一次通知

┌────────────────────────────── 05-platform-demo.py(把四段串起来)──────────────────────────────┐
│                                                                                            │
│  ┌────────────────┐   快照 JSON    ┌────────────────┐   事件    ┌──────────────┐   动作      │
│  │ 01 采集         │ ─────────────▶ │ 02 判定         │ ────────▶ │ 04 去重       │ ────────┐  │
│  │                │                │                │           │              │         │  │
│  │ /proc/stat     │                │ consecutive=3  │           │ fingerprint  │         ▼  │
│  │ /proc/meminfo  │                │ release=80     │           │ 状态表        │   ┌────────────┐
│  │ psutil         │                │ flap_window    │           │ notify/       │   │ 03 路由     │
│  │ HTTP 探活       │                │ =300s          │           │ suppress/     │   │            │
│  └────────────────┘                └────────────────┘           │ renotify/     │   │ critical→wh │
│        ▲                                  ▲                     │ recover/drop  │   │ warn→file   │
│        │                                  │                     └──────────────┘   │ 抖动→file    │
│        │                                  │                                        └─────┬──────┘
│        │                                  │                                              │
│   真机/假快照                        事件只在「状态变化」时产生                      投递报告(delivered_by)
│                                                                                            │
└────────────────────────────────────────────────────────────────────────────────────────────┘
                                                                                             ▼
                                                              webhook(HTTP POST)/ file(JSONL)/ console

关键:只有 firing / resolved 两种事件能走到 04。 pending(越界但没到连续次数)和 dropped(毛刺被过滤)在第 02/05 层就被吃掉了—— 它们的存在只是为了让你会诊「为什么不报警」,不该变成通知。

对应 mermaid(同样的链路):

flowchart LR A["01 采集<br/>/proc + psutil + HTTP 探活"] -->|快照 JSON| B["02 判定<br/>连续次数 / 迟滞 / 抖动"] B -->|"pending / dropped(截断)"| X[不通知] B -->|"firing / resolved"| C["04 去重<br/>指纹 + 状态表"] C -->|"notify / renotify / recover"| D["03 路由<br/>分级 + 重试 + 降级"] C -->|"suppress / drop"| Y[只计数] D -->|"delivered_by=webhook"| E[值班 webhook] D -->|"delivered_by=file"| F[落盘 file] D -->|"delivered_by=None"| G[投递失败,如实上报]
flowchart LR
    A["01 采集<br/>/proc + psutil + HTTP 探活"] -->|快照 JSON| B["02 判定<br/>连续次数 / 迟滞 / 抖动"]
    B -->|"pending / dropped(截断)"| X[不通知]
    B -->|"firing / resolved"| C["04 去重<br/>指纹 + 状态表"]
    C -->|"notify / renotify / recover"| D["03 路由<br/>分级 + 重试 + 降级"]
    C -->|"suppress / drop"| Y[只计数]
    D -->|"delivered_by=webhook"| E[值班 webhook]
    D -->|"delivered_by=file"| F[落盘 file]
    D -->|"delivered_by=None"| G[投递失败,如实上报]

图 2 · 一条告警的状态机(本日 02 + 04 合起来)

                          ┌──────────────── 样本流(每 2s 一条)────────────────┐
                          ▼                                                    │
   ┌──────┐  越界 1 次   ┌─────────┐  累计到 N 次   ┌────────┐  同指纹再来   ┌──────────┐
   │  ok  │ ───────────▶ │ pending │ ────────────▶ │ firing │ ───────────▶ │ suppress │
   └──────┘              └─────────┘               └────────┘   (计数+1)  └──────────┘
      ▲                       │                        │                        │
      │  未达 N 次就回落        │                        │ 未到恢复线(迟滞)        │ 超过 resend
      │  = dropped(毛刺)      │                        │ → 保持 firing,无事件     │ → renotify
      └───────────────────────┘                        │                        │
                                                       │ 跌破 release
                                                       ▼
                                                   ┌────────┐
                                                   │   ok   │ ──▶ recover(恢复通知)+ 状态表删除
                                                   └────────┘

mermaid 版(状态转移带条件):

stateDiagram-v2 [*] --> ok ok --> pending: 越界(breach) pending --> pending: 越界,累计 < N pending --> ok: 未达 N 就回落(dropped,毛刺被过滤) pending --> firing: 累计到第 N 次(notify) firing --> firing: 仍越界 / 未跌破 release(迟滞,无事件) firing --> firing: 同指纹重复上报(suppress,只计数) firing --> firing: 超过 resend_interval(renotify) firing --> ok: 跌破 release(recover) ok --> [*]
stateDiagram-v2
    [*] --> ok
    ok --> pending: 越界(breach)
    pending --> pending: 越界,累计 < N
    pending --> ok: 未达 N 就回落(dropped,毛刺被过滤)
    pending --> firing: 累计到第 N 次(notify)
    firing --> firing: 仍越界 / 未跌破 release(迟滞,无事件)
    firing --> firing: 同指纹重复上报(suppress,只计数)
    firing --> firing: 超过 resend_interval(renotify)
    firing --> ok: 跌破 release(recover)
    ok --> [*]

注意 firing -> firing 的三条自环: 状态没变,但动作不同(无事件 / 计数 / 再提醒)。这就是为什么把「状态」和「动作」分成两个概念 写代码会清爽很多——本日 02 只管状态,04 只管动作。


图 3 · 迟滞带:进去难,出来也难

   指标值
     ▲
 100 │                        ┌──┐        ← 抖动点都在这附近(89.9 / 90.1)
  95 │                    ┌───┘  └───┐
  90 ├══════════════════════════════════════════  越界线 threshold = 90
     │                        │
  85 │                        │              ← 这段(80, 90] 是「迟滞带」
  80 ├──────────────────────────────────────────  恢复线 release = 80
     │                        │
  75 │                        │
     └──────────────────────────────────────────▶ 时间
              越界开始                     跌破 80 才恢复

  没有迟滞:每次穿越 90 都翻转一次 → 89.9/90.1 抖动 = 刷屏
  有迟滞:  必须一路跌到 80 以下 → 0.2 个点的抖动完全被吸收

实测证据(实验 2c,真机输出):值从 95 一路降到 79 的过程中, 88 → 86 → 84 → 82 → 81 全部是「迟滞保持」,一个通知都没发,直到 79 才 resolved。

3      95.0     firing    firing    升级为告警
4      88.0     firing    -         迟滞保持(未到恢复线 80.0)
...
9      79.0     ok        resolved  恢复通知

图 4 · 抖动判定:时间窗口,而不是总次数

 采样   1    2    3    4    5    6    7    8
 值    95   60   95   60   95   60   95   60
 状态  F    R    F    R    F    R    F    R        F=firing R=resolved
        ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑
 翻转  #1   #2   #3   #4

             flap_window = 300s 内翻转 >= 4 次  ⇒  flapping = True
             ┌──────────────────────────────┐
             │  窗口内的翻转全部被算进来       │
             └──────────────────────────────┘

 关键对比(本日自检实测):
   窗口 100s、间隔 1s  → 翻转 4 次 → flapping = True   (真抖动)
   窗口 2s、间隔 10s   → 同样 7 次翻转 → 不是抖动      (每次翻转之间平静了 10s)

第二条是本日自检专门钉住的:同样的翻转次数,间隔 10 秒就算不上抖动。 用「总次数」判定的话,一个跑了 30 天、每天抖 1 次的系统会被误判成抖动。


图 5 · 指纹去重:60 次采样怎么变成 1 条通知

  采样流(每 2s 一条,同一条时间线)
  ┌─────────────────────────────────────────────────────────────┐
  │ cpu=95  cpu=96  cpu=97  cpu=95  cpu=98  ...  cpu=94         │   ← 30 条
  └─────────────────────────────────────────────────────────────┘
        │
        │  每个样本算指纹(只取「身份」字段)
        │  sha256(name + severity + labels)  =  f05360f106d8
        ▼
  ┌─────────────── 状态表(fp -> 记录)─────────────────┐
  │ f05360f106d8 → {count: 30, first: t0, last: t29}    │
  └─────────────────────────────────────────────────────┘
        │
        ├─ 第 1 次:表里没有 → notify      (★真正打扰人的那一条)
        └─ 第 2..30 次:表里有 → suppress  (count 累加,不通知)

  指纹**不能**包含的字段:
    value   → 每次值都不同 → 每次都算新指纹 → 去重失效
    status  → firing / resolved 指纹不同 → 恢复通知匹配不上(★本日踩过)
    ts      → 同 value 的后果
  指纹**应该**包含的字段:
    name / severity / instance / job / 挂载点 …(故障的「身份」)

  实测:10 条同指纹告警 → 1 条 notify + 9 条 suppress

图 6 · 降级链:顺序尝试,失败必须可见

  告警(severity=critical, flapping=False)
        │
        ▼
  选路由:critical → primary=webhook, fallback=[file, console]
        │
        ├─▶ ① webhook  尝试 1 次  ✗ URLError: Connection refused
        │              尝试 2 次  ✗ URLError: Connection refused     ← attempts=2
        │        (重试是「同一通道」,不是「换通道」)
        │
        ├─▶ ② file     写入 JSONL  ✓  → delivered_by = "file"        ← 降级成功
        │        (到此结束,不会再去发 console)
        │
        └─  ③ console  (不会被执行:链是顺序的,不是广播的)

  如果全部失败:
        delivered_by = None   ← ★绝不假装成功,上层必须看得见

  实测投递报告(实验 5):
     路由=critical      链=webhook->file->console       投递=file /tmp/day182/live/fallback.jsonl
     路由=warning       链=file->console                投递=file   (压根没碰 webhook)
     路由=flap-silenced 链=file                         投递=file   (抖动不叫人)

mermaid 时序版:

sequenceDiagram participant P as 05 平台 participant R as 03 路由 participant W as webhook :19090 participant F as file P->>R: deliver(alert) R->>W: POST #1 W--xR: URLError (refused) R->>W: POST #2(重试) W--xR: URLError (refused) R->>F: 写入 JSONL F-->>R: ok R-->>P: delivered_by="file", errors=[2 条]
sequenceDiagram
    participant P as 05 平台
    participant R as 03 路由
    participant W as webhook :19090
    participant F as file
    P->>R: deliver(alert)
    R->>W: POST #1
    W--xR: URLError (refused)
    R->>W: POST #2(重试)
    W--xR: URLError (refused)
    R->>F: 写入 JSONL
    F-->>R: ok
    R-->>P: delivered_by="file", errors=[2 条]

附:本日四层的「谁负责什么」一页纸

问题 由哪一层回答 本日实现 典型的「答错」后果
现在值是多少 01 采集 psutil / /proc / HTTP 探活 阈值配错口径(38.3% vs 36.3%)
这算不算事 02 判定 连续次数 + 迟滞 + 抖动窗口 抖动风暴
这事喊过没有 04 去重 指纹 + 状态表 + 恢复 刷屏 / 恢复通知丢失
喊给谁、怎么喊 03 路由 分级路由 + 重试 + 降级链 半夜叫醒人 / 告警悄悄丢掉
四段拼起来对不对 05 串联 假时钟自检 + 真机模式 每层都「对」,合起来漏告警