Files
pangolin/docs/runbook-scheduler.md
wangjia 5d4b484646 feat(alert): 统一告警出口 TG bot + runbook [tsk_9YMHMTfWJyNB]
新增 server/internal/alert 包(15G):
- 定义 Notifier 接口及 7 种 EventType(判封确认/补新失败/水位低/熔断/
  探针失联/心跳缺失/故障态)
- TGNotifier:Bot API 发送,Critical 事件不去重,Warning/Info 事件
  10min SETNX 去重窗口,失败重试 ≤2 次后降级至 LogNotifier
- LogNotifier:slog 结构化降级实现
- 单测:7 种事件模板 + runbook 锚点正确性;去重窗口内第二条被抑制;
  TG 5xx 重试后 fallback 且 Notify() 返回 nil;
  runbook 文件锚点与枚举一致性

接入 scheduler(替换旧的 NotifyFault 桩):
- detect/engine.go:故障态(Rule 5)→ EventTypeFault;
  判封确认(Rule 3)→ EventTypeBlockConfirmed
- orchestrate/deps.go:Notifier 类型别名指向 alert.Notifier
- orchestrate/replacer.go:补新失败 → EventTypeReplenishFailed;
  熔断触发 → EventTypeBreakerTripped
- probe/prober_agent.go:failCount ≥3 → EventTypeProbeAgentLost
- probe/store.go:新增 CheckHeartbeats() 供 15H 检测心跳缺失>90s

新增 docs/runbook-scheduler.md:7 节各含含义/先查什么/处置/升级条件,
锚点与代码枚举对应。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 00:52:26 +08:00

403 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pangolin Scheduler Runbook
> **适用范围**:本文档面向 Pangolin 内部运维人员,描述 scheduler 七种告警事件的排查与处置流程。
> **保密提示**:告警消息含内部节点 ID,不得转发至外部渠道。节点 ID 是不透明内部标识符,不含域名。
---
## 目录
1. [判封确认 (block_confirmed)](#block-confirmed)
2. [补新连续失败≥3 (replenish_failed)](#replenish-failed)
3. [水位<70% (watermark_low)](#watermark-low)
4. [熔断触发 (breaker_tripped)](#breaker-tripped)
5. [探针失联 (probe_agent_lost)](#probe-agent-lost)
6. [心跳缺失>90s (heartbeat_missing)](#heartbeat-missing)
7. [故障态 (fault)](#node-fault)
---
<a id="block-confirmed"></a>
## 1. 判封确认 (block_confirmed)
### 含义
15D(检测引擎)确认某节点被 GFW 封锁:该节点在 `blocked_suspect` 状态下连续经历 ≥6 个探测周期(约 30 分钟),国内三大运营商中 ≥2/3 探测失败而境外探测正常,判定为确认封锁(`blocked_confirmed`)。封锁确认后节点立即转入 `down` 状态,并进入 15E 补充队列。
**正常处置**:15E 自动补充新节点,通常无需人工干预。若随后出现 [`replenish_failed`](#replenish-failed) 告警则升级处理。
### 先查什么
```sql
-- 查看 node_events 最近的封锁确认事件(需替换 <node_id>
SELECT created_at, actor, action, detail
FROM node_events
WHERE target = 'node:<node_id>'
AND action IN ('transition', 'block_confirmed')
ORDER BY created_at DESC
LIMIT 10;
```
```bash
# 查看 15E 补充记录(在 ec2 上)
redis-cli GET sched:replace:<replacement_uuid>
redis-cli SMEMBERS sched:replace:index
```
```bash
# 查看该节点最新探针快照
redis-cli KEYS "probe:<node_id>:*"
redis-cli GET "probe:<node_id>:CN::3rd-ChinaTelecom"
```
### 处置步骤
1. **确认补充进行中**:检查 `sched:replace:index` 是否有该节点的记录,以及记录的 `phase` 字段(应为 `pending` / `creating` / `probing` 之一)。若存在则等待 15E 自动完成。
2. **若补充卡住**:参见 [`replenish_failed`](#replenish-failed)。
3. **记录 IP**:从节点数据库查取该节点旧 IP,记录到封锁 IP 备案。
4. **可选 SNI 轮换**:如池内同 SNI 多节点均被封,通知基础设施团队轮换 REALITY SNI。
### 升级条件
- 同一池(tier/region)内 30 分钟内出现 ≥3 次 `block_confirmed` → 触发 [`breaker_tripped`](#breaker-tripped)。
- 补充失败 → 升级至 [`replenish_failed`](#replenish-failed)。
---
<a id="replenish-failed"></a>
## 2. 补新连续失败≥3 (replenish_failed)
### 含义
15E(编排引擎)在为某个已封锁节点补充新节点时,连续尝试 3 次(默认 `MaxAttempts = 3`)均失败(探针验证超时 15 min 或 Provider API 报错),替换记录进入 `failed` 状态。旧节点仍处于 `down` 状态,**池容量已减少**。此为 Critical 级别告警,需立即人工介入。
### 先查什么
```bash
# 读取失败的替换记录
redis-cli GET sched:replace:<replacement_uuid>
# 字段说明:attempts(尝试次数)、providerTried(已试过的 Provider
```
```bash
# 查看目标 Pool 当前容量
redis-cli SMEMBERS sched:replace:index # 进行中的替换数量
```
```bash
# 查看新节点的探针快照(每次 attempt 生成的 newNode
redis-cli KEYS "probe:<new_node_id>:*"
```
```sql
-- 查看对应 node_events 日志
SELECT created_at, actor, action, detail
FROM node_events
WHERE target = 'node:<old_node_id>'
ORDER BY created_at DESC
LIMIT 20;
```
### 处置步骤
1. **判断失败原因**
-`providerTried` 列出了所有可用 Provider → Provider 全面故障,联系 IaaS 供应商。
- 若探针超时 → 检查新节点 IP 是否立即被封(该 IP 段被 GFW 封锁),换 Provider 或换 IP 段。
- 若 Provider API 报错 → 检查 API 凭证(`ALIYUN_PROBE_ACCESS_KEY_*` 等)是否有效。
2. **手动重入队列**(谨慎操作):
```bash
# 删除失败记录并重新推入队列,让 15E 重试
redis-cli DEL sched:replace:<replacement_uuid>
redis-cli SREM sched:replace:index <replacement_uuid>
# 生成新 UUID,推送新条目
redis-cli LPUSH detect:replace:queue '{"nodeId":"<old_node_id>","replacementUuid":"<new_uuid>"}'
```
3. **临时扩容**:若容量持续不足,通知用户停止新订阅,并从健康 Pool 临时调配权重。
4. **根因修复**:修复 Provider 问题或更换 IP 段后,15E 下次 Tick(30s)自动恢复。
### 升级条件
- 同一 Pool 内 ≥2 个节点同时 `replenish_failed` → 极端容量危机,启动灾备预案。
- Provider 全部不可用超过 1 小时 → 升级到架构层面(新增 Provider)。
---
<a id="watermark-low"></a>
## 3. 水位<70% (watermark_low)
### 含义
某节点池的有效路由权重之和低于满容量的 70%。通常由节点封锁(判封确认后进入 `down`)积累引起,可能影响用户体验(延迟上升、连接失败率增加)。
**触发条件**:池内活跃节点(`up` 状态)权重总和 < 池满容量 × 70%。
### 先查什么
```sql
-- 查看池内各节点状态和权重
SELECT id, status, weight, tier, region, updated_at
FROM nodes
WHERE tier = '<tier>' AND region = '<region>'
ORDER BY status, weight DESC;
```
```bash
# 查看进行中的补充任务
redis-cli SMEMBERS sched:replace:index
for uuid in $(redis-cli SMEMBERS sched:replace:index); do
redis-cli GET sched:replace:$uuid | python3 -m json.tool
done
```
```bash
# 查看灰度坡道(新节点权重爬坡)
redis-cli KEYS "sched:gray:*"
```
### 处置步骤
1. **确认补充进行中**:若有多个 `replenish` 记录在 `probing` / `creating` 阶段,15E 正在恢复,通常等待即可。
2. **若补充全部卡住**:参见 [`replenish_failed`](#replenish-failed)。
3. **加速灰度爬坡**(临时措施):若新节点已在灰度但权重还低,可手动将其权重推高:
```bash
# 通过管理 API 设置节点权重(参见 #8 管理操作)
curl -X POST https://api.internal/admin/nodes/<new_node_id>/weight -d '{"weight":100}'
```
4. **容量预警通知**:若水位持续 <50% 超过 30 分钟,通知运营团队评估影响面。
### 升级条件
- 水位降至 <50% → 紧急:启动备用容量或降级流控。
- 水位持续 <70% 超过 2 小时且无自动恢复 → 升级为容量规划问题。
---
<a id="breaker-tripped"></a>
## 4. 熔断触发 (breaker_tripped)
### 含义
15E 的熔断器(circuit breaker)阻止了新的替换操作。熔断触发表明同一 tier/region 池在短时间内有过多确认封锁,系统认为继续补充可能造成新节点也立即被封(IP 段整体被墙),因此暂停补充以避免浪费资源。此为 Critical 级别告警,需立即人工研判。
熔断后受影响节点的替换记录停留在 `pending` 阶段直到熔断解除。
### 先查什么
```bash
# 查看熔断器计数器(按 tier:region 键)
redis-cli KEYS "breaker:*"
redis-cli GET "breaker:<tier>:<region>:count"
redis-cli TTL "breaker:<tier>:<region>:count"
```
```bash
# 查看待处理的替换任务
redis-cli SMEMBERS sched:replace:index
```
```sql
-- 查看近期封锁事件数量
SELECT DATE_TRUNC('hour', created_at) as hour, COUNT(*) as cnt
FROM node_events
WHERE action = 'transition'
AND detail->>'to' = 'blocked_confirmed'
AND detail->>'tier' = '<tier>'
AND created_at > NOW() - INTERVAL '2 hours'
GROUP BY 1
ORDER BY 1 DESC;
```
### 处置步骤
1. **评估封锁模式**
- 若仅个别节点封锁 → 正常 GFW 例行扫描,等待熔断自动超时(通常 30 min)恢复。
- 若批量封锁(>5 节点/小时)→ IP 段整体被封,需更换 IP 段或切换 Provider。
2. **手动解除熔断**(#8 管理操作):
```bash
# 通过管理 API 清除熔断计数器
curl -X DELETE https://api.internal/admin/breaker/<tier>/<region>
# 或直接在 Redis 删除计数键
redis-cli DEL "breaker:<tier>:<region>:count"
```
3. **IP 段评估**:联系 IaaS 供应商,确认当前 IP 范围是否已进入 GFW 黑名单,必要时申请新 IP 段。
4. **降级保障**:若熔断超过 4 小时,通知运营团队考虑临时迁移到其他 Provider。
### 升级条件
- 多个 region 同时熔断 → 全球性 GFW 扫描事件,启动应急响应。
- 手动解除熔断后立即再次触发 → IP 段问题未解决,升级到基础设施团队。
---
<a id="probe-agent-lost"></a>
## 5. 探针失联 (probe_agent_lost)
### 含义
15F(探针子系统)的第三方拨测 Agent(阿里云云监控)连续 ≥3 次 API 调用失败。这不代表被测节点本身有问题,而是**探测能力本身丧失**:15D 将无法获取国内运营商探测数据,可能导致判封灵敏度下降(漏判)。
**注意**:探针失联不触发节点状态变更,只是减少探测数据的覆盖范围。
### 先查什么
```bash
# 检查阿里云 API 凭证是否有效(在 ec2 上)
curl -s "https://cloudmonitor.cn-hangzhou.aliyuncs.com/" | head -20
# 预期返回 403/401(证明网络可达),而非 connection refused
# 检查 scheduler 进程日志
journalctl -u pangolin-scheduler -n 100 --no-pager | grep "prober_agent"
```
```bash
# 检查阿里云 RAM 子账号配额
# (需在阿里云控制台或通过 aliyun CLI 查询)
```
### 处置步骤
1. **网络连通性**:确认 ec2 可访问 `cloudmonitor.cn-hangzhou.aliyuncs.com`(国内端点需确保没有出口限制)。
2. **API 凭证**:检查 `ALIYUN_PROBE_ACCESS_KEY_ID` / `ALIYUN_PROBE_ACCESS_KEY_SECRET` 环境变量是否正确且未过期。
3. **配额耗尽**:阿里云云拨测按次计费,检查当月用量是否超限。若超限,临时降低探测频率或充值。
4. **服务故障**:访问阿里云状态页确认云监控服务是否有故障。
5. **降级运行**:探针失联期间 15D 仅依赖已有的历史快照(TTL 30 min),封锁判断会有所延迟,可接受短期(<30 min)降级。
### 升级条件
- 探针失联超过 30 分钟 → 15D 的历史快照开始过期,判封能力严重受损,需立即恢复。
- 凭证问题无法快速解决 → 临时切换到自建探针 Agent(参见 probe 包文档)。
---
<a id="heartbeat-missing"></a>
## 6. 心跳缺失>90s (heartbeat_missing)
### 含义
某个**自建(first-party)探针 Agent** 超过 90 秒未向 `/probe/report` 发送任何心跳数据。与探针失联([`probe_agent_lost`](#probe-agent-lost))不同,此告警针对自建 Agent,不是第三方拨测服务。
自建 Agent 心跳缺失意味着来自该 Agent 所在网络位置(特定 ISP/省份)的 L3 数据将中断,影响判封精确度。
### 先查什么
```bash
# 查看对应探针最后一次心跳时间
redis-cli GET "probe:hb:<probe_id>"
# 值为 Unix 时间戳,与当前时间差即为失联时长
redis-cli TTL "probe:hb:<probe_id>"
# 剩余 TTL15min = 900s),若已到期则 key 不存在
```
```bash
# 在对应探针机器上检查 probe agent 进程状态
ssh <probe_host> "systemctl status pangolin-probe-agent"
ssh <probe_host> "journalctl -u pangolin-probe-agent -n 50 --no-pager"
```
```bash
# 检查探针机器与 scheduler 的网络连通性
ssh <probe_host> "curl -v https://<scheduler_host>/probe/report"
```
### 处置步骤
1. **检查 Agent 进程**
- 若进程未运行 → `systemctl restart pangolin-probe-agent`。
- 若进程运行但报错 → 查看日志,常见原因:HMAC 密钥错误、Scheduler 地址配置错误、TLS 证书问题。
2. **网络连通性**:确认探针机器出网正常,且 Scheduler 的 `/probe/report` 端口可达。
3. **HMAC 密钥轮换**:若密钥过期或被更新,更新 Agent 配置文件后重启。
4. **探针机器故障**:若机器故障,从备用位置部署新探针 Agent。
### 升级条件
- 某 ISP / 省份所有探针均失联 → 该区域探测盲区,节点封锁可能被漏判,升级处理。
- 失联超过 2 小时且无法恢复 → 考虑临时增加第三方拨测覆盖(阿里云)弥补缺口。
---
<a id="node-fault"></a>
## 7. 故障态 (fault)
### 含义
15D 检测到某节点**国内与境外探测同时失败**,判定为节点级别的网络故障(不是 GFW 封锁)。故障态节点**不进行状态转换、不触发补充流程**,也不消耗补充配额,仅通知人工研判。
典型场景:节点主机宕机、网卡故障、IDC 网络中断等基础设施问题。
### 先查什么
```bash
# 查看该节点当前探针快照
redis-cli KEYS "probe:<node_id>:*"
# 检查各运营商和境外的探测结果
redis-cli GET "probe:<node_id>:CN::3rd-ChinaTelecom"
redis-cli GET "probe:<node_id>:SG::"
```
```bash
# SSH 登录节点进行基础诊断(若可达)
ssh <node_host> "systemctl status singbox xray"
ssh <node_host> "ss -tlnp | grep -E '443|8080'"
```
```bash
# 从控制平面 ping / traceroute(境外节点)
ping -c 5 <node_ip>
traceroute <node_ip>
```
```sql
-- 查看节点历史状态变化
SELECT created_at, action, detail
FROM node_events
WHERE target = 'node:<node_id>'
ORDER BY created_at DESC
LIMIT 20;
```
### 处置步骤
1. **区分 GFW 封锁与真实故障**:
- 仅国内失败 + 境外正常 → GFW 封锁(此路径不应触发 fault,由 15D Rule 1-3 处理)。
- 国内 + 境外均失败 → 节点级故障(此告警场景)。
2. **IaaS 控制台确认**:登录 Provider 控制台,检查节点(EC2/VPS)运行状态。
3. **若节点可 SSH**:检查服务进程是否崩溃,查看系统日志(`dmesg`, `journalctl`)。
4. **若节点不可 SSH**:通过 Provider 控制台进行 VNC/串口连接或强制重启。
5. **若需下线**
- 手动将节点状态改为 `down`(通过管理 API #8)。
- 手动推入补充队列:
```bash
redis-cli LPUSH detect:replace:queue '{"nodeId":"<node_id>","replacementUuid":"<new_uuid>"}'
```
6. **根因记录**:在节点事件表记录故障原因(通过 WriteAuditLog API)。
### 升级条件
- 同一 IDC / 可用区多节点同时故障 → IDC 事件,联系 IaaS 供应商。
- 故障节点无法通过控制台恢复 → 放弃该节点,补充新节点(手动推入队列)。
- 故障持续 > 1 小时且涉及 >10% 池容量 → 进入水位<70% 处置流程。
---
## 附录:关键 Redis Key 速查
| Key 前缀 | 含义 |
|---|---|
| `probe:<nodeId>:<country>:<region>:<isp>` | 节点探针快照(30min TTL |
| `probe:hb:<probeId>` | 自建探针心跳时间戳(15min TTL) |
| `probe:freq:<nodeId>` | 节点提频标记(进入 suspect 时写入,45min TTL |
| `detect:replace:queue` | 15D → 15E 补充任务队列(LPUSH/RPOP |
| `sched:replace:<uuid>` | 单次替换编排记录(7天审计保留) |
| `sched:replace:index` | 进行中替换任务 UUID 集合 |
| `sched:gray:<nodeId>` | 新节点灰度爬坡记录 |
| `alert:dedup:<type>:<nodeId>` | 告警去重令牌(10min TTL,仅非 Critical 事件) |
| `breaker:<tier>:<region>:count` | 熔断器计数器 |
| `streak:<nodeId>` | 节点连续失败/恢复计数(Redis JSON |