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

15 KiB
Raw Permalink Blame History

Pangolin Scheduler Runbook

适用范围:本文档面向 Pangolin 内部运维人员,描述 scheduler 七种告警事件的排查与处置流程。
保密提示:告警消息含内部节点 ID,不得转发至外部渠道。节点 ID 是不透明内部标识符,不含域名。


目录

  1. 判封确认 (block_confirmed)
  2. 补新连续失败≥3 (replenish_failed)
  3. 水位<70% (watermark_low)
  4. 熔断触发 (breaker_tripped)
  5. 探针失联 (probe_agent_lost)
  6. 心跳缺失>90s (heartbeat_missing)
  7. 故障态 (fault)

1. 判封确认 (block_confirmed)

含义

15D(检测引擎)确认某节点被 GFW 封锁:该节点在 blocked_suspect 状态下连续经历 ≥6 个探测周期(约 30 分钟),国内三大运营商中 ≥2/3 探测失败而境外探测正常,判定为确认封锁(blocked_confirmed)。封锁确认后节点立即转入 down 状态,并进入 15E 补充队列。

正常处置:15E 自动补充新节点,通常无需人工干预。若随后出现 replenish_failed 告警则升级处理。

先查什么

-- 查看 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;
# 查看 15E 补充记录(在 ec2 上)
redis-cli GET sched:replace:<replacement_uuid>
redis-cli SMEMBERS sched:replace:index
# 查看该节点最新探针快照
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
  3. 记录 IP:从节点数据库查取该节点旧 IP,记录到封锁 IP 备案。
  4. 可选 SNI 轮换:如池内同 SNI 多节点均被封,通知基础设施团队轮换 REALITY SNI。

升级条件


2. 补新连续失败≥3 (replenish_failed)

含义

15E(编排引擎)在为某个已封锁节点补充新节点时,连续尝试 3 次(默认 MaxAttempts = 3)均失败(探针验证超时 15 min 或 Provider API 报错),替换记录进入 failed 状态。旧节点仍处于 down 状态,池容量已减少。此为 Critical 级别告警,需立即人工介入。

先查什么

# 读取失败的替换记录
redis-cli GET sched:replace:<replacement_uuid>
# 字段说明:attempts(尝试次数)、providerTried(已试过的 Provider
# 查看目标 Pool 当前容量
redis-cli SMEMBERS sched:replace:index  # 进行中的替换数量
# 查看新节点的探针快照(每次 attempt 生成的 newNode
redis-cli KEYS "probe:<new_node_id>:*"
-- 查看对应 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. 手动重入队列(谨慎操作):

    # 删除失败记录并重新推入队列,让 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)。

3. 水位<70% (watermark_low)

含义

某节点池的有效路由权重之和低于满容量的 70%。通常由节点封锁(判封确认后进入 down)积累引起,可能影响用户体验(延迟上升、连接失败率增加)。

触发条件:池内活跃节点(up 状态)权重总和 < 池满容量 × 70%。

先查什么

-- 查看池内各节点状态和权重
SELECT id, status, weight, tier, region, updated_at
FROM nodes
WHERE tier = '<tier>' AND region = '<region>'
ORDER BY status, weight DESC;
# 查看进行中的补充任务
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
# 查看灰度坡道(新节点权重爬坡)
redis-cli KEYS "sched:gray:*"

处置步骤

  1. 确认补充进行中:若有多个 replenish 记录在 probing / creating 阶段,15E 正在恢复,通常等待即可。
  2. 若补充全部卡住:参见 replenish_failed
  3. 加速灰度爬坡(临时措施):若新节点已在灰度但权重还低,可手动将其权重推高:
    # 通过管理 API 设置节点权重(参见 #8 管理操作)
    curl -X POST https://api.internal/admin/nodes/<new_node_id>/weight -d '{"weight":100}'
    
  4. 容量预警通知:若水位持续 <50% 超过 30 分钟,通知运营团队评估影响面。

升级条件

  • 水位降至 <50% → 紧急:启动备用容量或降级流控。
  • 水位持续 <70% 超过 2 小时且无自动恢复 → 升级为容量规划问题。

4. 熔断触发 (breaker_tripped)

含义

15E 的熔断器(circuit breaker)阻止了新的替换操作。熔断触发表明同一 tier/region 池在短时间内有过多确认封锁,系统认为继续补充可能造成新节点也立即被封(IP 段整体被墙),因此暂停补充以避免浪费资源。此为 Critical 级别告警,需立即人工研判。

熔断后受影响节点的替换记录停留在 pending 阶段直到熔断解除。

先查什么

# 查看熔断器计数器(按 tier:region 键)
redis-cli KEYS "breaker:*"
redis-cli GET "breaker:<tier>:<region>:count"
redis-cli TTL "breaker:<tier>:<region>:count"
# 查看待处理的替换任务
redis-cli SMEMBERS sched:replace:index
-- 查看近期封锁事件数量
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 管理操作):

    # 通过管理 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 段问题未解决,升级到基础设施团队。

5. 探针失联 (probe_agent_lost)

含义

15F(探针子系统)的第三方拨测 Agent(阿里云云监控)连续 ≥3 次 API 调用失败。这不代表被测节点本身有问题,而是探测能力本身丧失:15D 将无法获取国内运营商探测数据,可能导致判封灵敏度下降(漏判)。

注意:探针失联不触发节点状态变更,只是减少探测数据的覆盖范围。

先查什么

# 检查阿里云 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"
# 检查阿里云 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 包文档)。

6. 心跳缺失>90s (heartbeat_missing)

含义

某个自建(first-party)探针 Agent 超过 90 秒未向 /probe/report 发送任何心跳数据。与探针失联(probe_agent_lost)不同,此告警针对自建 Agent,不是第三方拨测服务。

自建 Agent 心跳缺失意味着来自该 Agent 所在网络位置(特定 ISP/省份)的 L3 数据将中断,影响判封精确度。

先查什么

# 查看对应探针最后一次心跳时间
redis-cli GET "probe:hb:<probe_id>"
# 值为 Unix 时间戳,与当前时间差即为失联时长

redis-cli TTL "probe:hb:<probe_id>"
# 剩余 TTL15min = 900s),若已到期则 key 不存在
# 在对应探针机器上检查 probe agent 进程状态
ssh <probe_host> "systemctl status pangolin-probe-agent"
ssh <probe_host> "journalctl -u pangolin-probe-agent -n 50 --no-pager"
# 检查探针机器与 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 小时且无法恢复 → 考虑临时增加第三方拨测覆盖(阿里云)弥补缺口。

7. 故障态 (fault)

含义

15D 检测到某节点国内与境外探测同时失败,判定为节点级别的网络故障(不是 GFW 封锁)。故障态节点不进行状态转换、不触发补充流程,也不消耗补充配额,仅通知人工研判。

典型场景:节点主机宕机、网卡故障、IDC 网络中断等基础设施问题。

先查什么

# 查看该节点当前探针快照
redis-cli KEYS "probe:<node_id>:*"
# 检查各运营商和境外的探测结果
redis-cli GET "probe:<node_id>:CN::3rd-ChinaTelecom"
redis-cli GET "probe:<node_id>:SG::"
# SSH 登录节点进行基础诊断(若可达)
ssh <node_host> "systemctl status singbox xray"
ssh <node_host> "ss -tlnp | grep -E '443|8080'"
# 从控制平面 ping / traceroute(境外节点)
ping -c 5 <node_ip>
traceroute <node_ip>
-- 查看节点历史状态变化
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)。
    • 手动推入补充队列:
      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)