5d4b484646
新增 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>
403 lines
15 KiB
Markdown
403 lines
15 KiB
Markdown
# 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>"
|
||
# 剩余 TTL(15min = 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) |
|