- 定义 ping target 配置:host、count、packetSize - 定义 ping expect 断言:alive、maxPacketLoss、maxAvgLatencyMs、maxMaxLatencyMs - 设计跨平台 ping 输出解析器(Linux/macOS/Windows 含多语言支持) - 双重超时保障:ping 命令自身超时 + AbortSignal 兜底 - 扩展 checker-runner-abstraction spec 支持 ping checker 子进程控制 - 更新 probe-config spec 支持 ping type 配置
191 lines
8.6 KiB
Markdown
191 lines
8.6 KiB
Markdown
## Context
|
||
|
||
项目当前有 HTTP、CMD、DB、TCP 四种 checker,均遵循 `CheckerDefinition<TResolved>` 接口规范。TCP checker 是最近实现的网络层 checker,其模式(Bun.spawn / 原生 socket + AbortSignal + 断言链)是 ICMP checker 的直接参考。
|
||
|
||
ICMP Ping 是最基础的网络探测手段,但 Node.js/Bun 生态中没有合适的纯 JS ICMP 实现(均依赖 `raw-socket` 原生 addon,Bun N-API 兼容性不确定且需要 root 权限)。现有的命令封装库(`ping`、`pingman`)虽然提供了跨平台解析,但它们封装了 `child_process.spawn` 且不暴露子进程引用,无法配合我们的 `ctx.signal` 超时控制机制。
|
||
|
||
经过调研和讨论,确定自行实现:用 `Bun.spawn` 调用系统 `ping` 命令 + 自行编写跨平台输出解析器。
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- 实现 `type: ping` checker,支持主机存活检测、延迟监控、丢包率检查
|
||
- 跨平台支持 Linux、macOS、Windows(含中文 Windows)
|
||
- 完全复用现有 checker 架构(registry、schema、expect、failure)
|
||
- 零外部依赖
|
||
|
||
**Non-Goals:**
|
||
- 不实现 traceroute 功能
|
||
- 不实现 IPv6 专项支持(系统 ping 命令会自动处理 IPv6 地址)
|
||
- 不实现原始 ICMP socket(权限要求过高)
|
||
- 不提供 per-packet 逐包结果(只提供 summary 统计)
|
||
|
||
## Decisions
|
||
|
||
### Decision 1: 调用系统 `ping` 命令而非原始 ICMP socket
|
||
|
||
**选择**: 通过 `Bun.spawn` 调用系统 `ping` 可执行文件
|
||
|
||
**替代方案**:
|
||
- 原始 ICMP socket(`raw-socket` addon):需要 root/CAP_NET_RAW 权限,Bun N-API 兼容性不确定
|
||
- 三方库 `pingman`/`ping`:封装了 spawn 但不暴露子进程引用,无法配合 AbortSignal 超时控制
|
||
|
||
**理由**: 系统 `ping` 命令无需特殊权限(大多数系统),`Bun.spawn` 给我们完全的子进程生命周期控制,与现有 cmd checker 模式一致。
|
||
|
||
### Decision 2: 自行实现跨平台解析器
|
||
|
||
**选择**: 在 `parse.ts` 中实现 `parsePingOutput(stdout, platform)` 函数,用正则匹配 summary 统计行
|
||
|
||
**替代方案**:
|
||
- 引入 `pingman` 作为依赖使用其 parser:模块不是公开 API,deep import 不稳定
|
||
- 引入 `ping`(danielzzz)的 parser:同上,且返回值类型全是 string
|
||
|
||
**理由**: 我们只需要 summary 行的统计数据(transmitted/received/loss/min/avg/max),不需要逐包 body 解析。三套正则约 40-50 行代码,完全可控且零依赖。
|
||
|
||
### Decision 3: 跨平台命令构建策略
|
||
|
||
```
|
||
平台判断: process.platform (win32 vs 其他)
|
||
|
||
Linux:
|
||
ping -c <count> -s <packetSize> -W <timeoutSec> <host>
|
||
|
||
macOS:
|
||
ping -c <count> -s <packetSize> -W <timeoutMs> <host>
|
||
|
||
Windows:
|
||
ping -n <count> -l <packetSize> -w <timeoutMs> <host>
|
||
```
|
||
|
||
**超时参数传递策略:双重保障**
|
||
|
||
传递平台对应的超时参数(`-W`/`-w`),同时保留外层 `ctx.signal` + `proc.kill()` 作为兜底:
|
||
- **ping 命令自身超时**:确保每个 ICMP 包在指定时间内超时返回,避免 ping 进程因网络异常而无限等待
|
||
- **外层 AbortSignal**:作为最终兜底,防止 ping 命令因任何原因卡死不退出
|
||
|
||
各平台超时参数单位差异:
|
||
- Linux `-W`:单位为**秒**(整数),需将 timeoutMs 转换为秒(向上取整)
|
||
- macOS `-W`:单位为**毫秒**(整数)
|
||
- Windows `-w`:单位为**毫秒**(整数)
|
||
|
||
超时值计算:使用外层 `timeoutMs` 作为 ping 命令的超时参数值。这样 ping 命令自身会在 timeout 内完成,外层 signal 作为额外保障。
|
||
|
||
### Decision 4: Windows 多语言输出解析策略
|
||
|
||
Windows `ping` 输出语言跟随系统 locale(中文系统输出中文、英文系统输出英文、日文系统输出日文等)。
|
||
|
||
**选择**: 基于数字模式和行结构匹配,不依赖关键词
|
||
|
||
具体策略:
|
||
- **丢包行**: 匹配 `(\d+).*?(\d+).*?(\d+).*?(\d+(?:\.\d+)?%)` 模式——提取"已发送"、"已接收"、"丢失"和百分比数字,不依赖中英文关键词
|
||
- **延迟行**: 匹配 `(\d+)ms.*?(\d+)ms.*?(\d+)ms` 模式——提取 min/max/avg 三个数字(Windows 输出顺序固定为 Minimum/Maximum/Average)
|
||
|
||
**替代方案**: 枚举所有语言的关键词——维护成本高,且无法覆盖所有 locale
|
||
|
||
### Decision 5: 解析结果数据结构
|
||
|
||
```typescript
|
||
interface PingStats {
|
||
alive: boolean;
|
||
transmitted: number;
|
||
received: number;
|
||
packetLoss: number; // 0-100
|
||
minLatencyMs: number | null;
|
||
avgLatencyMs: number | null;
|
||
maxLatencyMs: number | null;
|
||
}
|
||
```
|
||
|
||
`latencyMs` 字段为 `null` 表示主机不可达时无延迟数据。
|
||
|
||
### Decision 6: 断言执行顺序(短路)
|
||
|
||
```
|
||
alive → packetLoss → avgLatency → maxLatency → duration
|
||
```
|
||
|
||
理由:
|
||
1. `alive` 是最基础的判断,不可达时后续断言无意义
|
||
2. `packetLoss` 比延迟更严重(丢包意味着部分请求完全失败)
|
||
3. `avgLatency` 和 `maxLatency` 是延迟质量指标
|
||
4. `duration` 是整体执行时间兜底
|
||
|
||
### Decision 7: ping 命令不存在时的错误处理
|
||
|
||
当系统未安装 `ping` 命令时(常见于精简容器镜像如 Alpine),`Bun.spawn` 会抛出 ENOENT 错误。
|
||
|
||
处理方式:在 spawn 阶段 try/catch,返回结构化错误:
|
||
```typescript
|
||
failure: errorFailure("ping", "spawn", `ping 命令不可用: ${error.message}`)
|
||
statusDetail: "ping command not found"
|
||
```
|
||
|
||
文档中注明系统依赖:容器环境需确保 `ping` 命令可用(如 Alpine 需安装 `iputils-ping`)。
|
||
|
||
### Decision 8: configKey 和 type 命名
|
||
|
||
**选择**: `type: "ping"`, `configKey: "ping"`
|
||
|
||
**替代方案**: `type: "icmp"` — 但 `ping` 更贴近用户认知,且配置中 `ping.host` 比 `icmp.host` 更直观。
|
||
|
||
### Decision 9: 超时控制与子进程生命周期
|
||
|
||
与 cmd checker 相同的模式:
|
||
```typescript
|
||
ctx.signal.addEventListener("abort", () => {
|
||
try { proc.kill(); } catch { /* best-effort */ }
|
||
}, { once: true });
|
||
```
|
||
|
||
当 signal abort 时 kill 子进程,然后在结果中记录超时错误。这需要修改 `checker-runner-abstraction` spec 中"仅 cmd checker 可在 signal abort 时 proc.kill()"的约束。
|
||
|
||
### Decision 10: Linux/macOS 解析正则
|
||
|
||
```
|
||
丢包统计行:
|
||
Linux: "3 packets transmitted, 3 received, 0% packet loss, time 2003ms"
|
||
macOS: "3 packets transmitted, 3 packets received, 0.0% packet loss"
|
||
正则: /(\d+)\s+packets?\s+transmitted.*?(\d+)\s+(?:packets?\s+)?received.*?(\d+(?:\.\d+)?)%\s+packet\s+loss/
|
||
|
||
延迟统计行:
|
||
Linux: "rtt min/avg/max/mdev = 1.234/2.345/3.456/0.567 ms"
|
||
macOS: "round-trip min/avg/max/stddev = 1.234/2.345/3.456/0.567 ms"
|
||
正则: /(?:rtt|round-trip).*?=\s*([\d.]+)\/([\d.]+)\/([\d.]+)/
|
||
```
|
||
|
||
### Decision 11: Windows 解析正则
|
||
|
||
```
|
||
丢包统计行(数字模式,语言无关):
|
||
英文: "Packets: Sent = 3, Received = 3, Lost = 0 (0% loss)"
|
||
中文: "数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失)"
|
||
正则: /=\s*(\d+).*?=\s*(\d+).*?=\s*(\d+).*?(\d+(?:\.\d+)?)%/
|
||
|
||
延迟统计行(数字模式,语言无关):
|
||
英文: "Minimum = 1ms, Maximum = 3ms, Average = 2ms"
|
||
中文: "最短 = 1ms,最长 = 3ms,平均 = 2ms"
|
||
正则: /=\s*(\d+)ms.*?=\s*(\d+)ms.*?=\s*(\d+)ms/
|
||
```
|
||
|
||
Windows 延迟顺序固定为 min/max/avg(注意与 Linux/macOS 的 min/avg/max 不同)。
|
||
|
||
## Risks / Trade-offs
|
||
|
||
### [Risk] 系统未安装 ping 命令 → 清晰的错误提示 + 文档说明
|
||
容器环境(Alpine、scratch)可能不包含 ping。通过 spawn 阶段 catch ENOENT 给出明确提示,并在 README 中注明依赖。
|
||
|
||
### [Risk] 未知 locale 的 Windows 输出无法解析 → 降级为 alive=false + 原始输出
|
||
如果正则无法匹配任何统计行,将 alive 判定为 `received > 0`(通过检查 exit code:Windows ping 在全部丢包时 exit code 为 1),延迟字段为 null。statusDetail 展示原始输出前 80 字符供用户排查。
|
||
|
||
### [Risk] ping 命令被防火墙/网络策略阻断 → 用户可预期的行为
|
||
ICMP 在某些网络环境中被阻断。这不是 checker 的 bug,而是网络配置问题。checker 会正确报告 `alive=false` 和 100% packet loss。
|
||
|
||
### [Trade-off] 双重超时保障
|
||
传递 `-W`/`-w` 超时参数给 ping 命令,同时保留外层 AbortSignal + proc.kill() 兜底。
|
||
优势:ping 命令自身会在超时后正常退出,不依赖外部 kill;即使 ping 命令因异常卡死,外层 signal 仍能强制终止。
|
||
劣势:需要处理三平台超时参数的单位差异(Linux 秒 vs macOS/Windows 毫秒),增加少量命令构建复杂度。
|
||
|
||
### [Trade-off] 不引入三方库
|
||
优势:零依赖、完全可控、与项目规范一致。
|
||
劣势:需要自行维护跨平台解析正则。但 ping 输出格式极其稳定(几十年未变),维护成本极低。
|