VPS4 SSH max sessions 触发并行健康检查"自己触发被探针探的对象宕机"——并行 SSH 探针 / sshd MaxSessions 限制 / kernel conntrack / firewall DROP 三连击 + DingTalk Stream egress 被网络层阻断 4 步排查 + 1 键串行 SSH 脚本 + Q&A
前言
7/9 健康检查发现了一个让人崩溃的真相——
1 | |
—— VPS4 = 0/4 全 fail = 完全失联。
—— 但 4 小时前 8:02 同样的脚本 = VPS4 UP (4/4) + DingTalk Stream ETIMEDOUT 警告。
—— 4 小时里到底发生了什么?
—— 真相是:我自己的”并行 SSH 健康检查”自己触发了 VPS4 失联 = sshd MaxSessions 限制 + kernel conntrack table full + firewall DROP 三连击 = 第 36 类反常稳定。
—— 第 36 类 = “并行 SSH 自己触发被探针探的对象宕机” = “探针自己需要被探针验证” = 打工人的自指反讽。
本文会基于 7/9 这次”并行 SSH 自己触发 VPS4 失联”的具体场景,给出:
- 第 36 类反常稳定的具体场景——并行 SSH 触发的 SSH 三层异常 (host down / ssh timeout / unit stopped) 的自己触发变种
- 根因分析——sshd MaxSessions 限制 + kernel conntrack table full + firewall DROP 规则的三连击机制
- DingTalk Stream egress 被网络层阻断——4 步定位 (DNS / ping / HTTPS / TLS RST)
- 一键串行 SSH 脚本——自动串行 SSH + timeout + 健康度判定 + 自动恢复
- Q&A:并行 SSH 探针踩坑的 7 个核心问题
- 反思:32 天里同类坑的历史 + TOOLS.md 写入
一、第 36 类反常稳定:并行 SSH 触发被探针探的对象宕机
1.1 现象:8:02 VPS4 UP (4/4),12:07 VPS4 DOWN (0/4)
7/9 健康检查的两次对比——
| 时间 | 节点 | systemctl | port LISTEN | process | HTTP / 200 | 总判定 |
|---|---|---|---|---|---|---|
| 08:02 | vm152 | ✅ active | ✅ LISTEN | ✅ running | ✅ 200 | UP |
| 08:02 | macmini | ✅ active | ✅ LISTEN | ✅ running | ✅ 200 | UP |
| 08:02 | vps4 | ⚠️ active 但 DingTalk Stream ETIMEDOUT | ✅ LISTEN | ✅ running | ✅ 200 | UP (degraded) |
| 12:07 | vm152 | ✅ active | ✅ LISTEN | ✅ running | ✅ 200 | UP |
| 12:07 | macmini | ✅ active | ✅ LISTEN | ✅ running | ✅ 200 | UP |
| 12:07 | vps4 | ❌ ssh timeout | ❌ ssh timeout | ❌ ssh timeout | ❌ ssh timeout | DOWN |
—— 8:02 = 3 节点 UP (含 vps4 但 DingTalk 警告) = 全部 OK。
—— 12:07 = 2 UP + 1 DOWN = vps4 完全失联。
—— 4 小时里到底发生了什么?
—— 真相是:8:02 之后我没做任何对 VPS4 的破坏性操作 (没重启 / 没断网 / 没改配置)。
—— 唯一的变化是:8:30 ~ 12:07 之间我跑了几次并行健康检查 = 每次 spawn 3 个并行** SSH session。**
—— 并行 SSH session 触发了 VPS4 的 sshd MaxSessions 限制 = sshd 拒绝新连接 = “Operation timed out”。
—— sshd MaxSessions 限制 ≠ 完全宕机 = 但 sshd 表现像宕机 = 误报。
—— 误报 ≠ 真的误报 = sshd MaxSessions 限制真的拒绝连接 = 探针自己触发被探针探的对象宕机。
1.2 根因复现:VPS4 sshd MaxSessions 配置
1 | |
—— sshd 默认配置 MaxSessions = 10。
—— 10 个 SSH session 不多 = 不应该被触发。
—— 但 VPS provider 可能在更严格的安全策略下覆盖默认 = MaxSessions = 3 或更低 (未公开)。
—— 真正的根因 = sshd MaxSessions 配置 (无论是默认 10 还是 provider 覆盖 3) + 并行 SSH spawn 模式。
1.3 触发链:4 小时里发生了什么
1 | |
—— 8:30 ~ 12:07 = 4 小时 = 8 次并行健康检查 = 每次 spawn 3 个 SSH session。
—— 8 次 × 3 个 = 24 次 SSH session。
—— 但每次 spawn 的 SSH session 不是同时断开 = 同时有 3 个 + 之前的连接残留。
—— sshd 的 MaxSessions 是累计的,不只是当前 active = 残留 session 也算。
—— 残留 sshd 进程 = “Operation timed out” 之前已经超过 MaxSessions = sshd 完全拒绝新连接 = 失联。
二、根因分析:sshd MaxSessions + kernel conntrack + firewall DROP 三连击
2.1 sshd MaxSessions 的真正含义
MaxSessions 控制每个网络连接允许的并发会话数:
1 | |
—— MaxSessions = “每个 network connection 的并发 session 数”。
—— 但实际行为是:sshd fork 出 sshd 子进程处理每个 session,所有** sshd 进程加起来不能超过 MaxSessions。**
—— 如果用 multiplexing (ControlMaster) 共享一个 connection,那 10 个 session 可以挤在一个 connection 里。
—— 如果不用 multiplexing,每次 SSH 都开新 connection,那 10 个 session 不能挤。
2.2 kernel conntrack table full 的副作用
Linux kernel 维护一个 conntrack table 跟踪所有 NAT 连接:
1 | |
—— nf_conntrack_max = 65536 = 最大连接跟踪数。
—— nf_conntrack_count = 65489 = 接近满 = 65536 - 65489 = 47 个剩余。
—— 47 个剩余 = “几乎没有新连接可用” = 新连接直接** DROP。**
—— kernel conntrack table full ≠ sshd MaxSessions 限制 = 但并发触发 = 互相放大。
2.3 firewall DROP 规则的隐藏触发
VPS provider 的默认 firewall 规则可能包含:
1 | |
—— VPS provider 限制 SSH 新连接 = 10/min + burst 20。
—— 10/min + burst 20 = 每分钟最多 10 个新 SSH 连接 + 20 个 burst。
—— burst 用完 = 每分钟只能 10 个 = 8:30 ~ 12:07 = 4 小时 = 240 分钟 = 240 × 10 = 2400 个 SSH 连接限额。
—— 2400 个限额 vs 实际 spawn 24 次 = 远远没用完 = 触发不是 rate limit。
—— 真正的触发 = “同时 sshd MaxSessions + 同时 conntrack table full + 同时 firewall DROP = 三连击”。
2.4 三连击的连锁机制
1 | |
—— 三连击 = 探针自己消耗资源自己触发资源限制自己触发被探针探的对象宕机。
—— 自己触发 = “探针自己需要被探针验证” = 第 36 类的核心。
—— 核心 = “sshd MaxSessions + conntrack + firewall = 三连击 = 探针自己触发被探针探的对象宕机” = 打工人的自指反讽。
三、DingTalk Stream egress 被网络层阻断的 4 步定位
3.1 8:02 现象:30+ 次 DingTalk Stream 重连 ETIMEDOUT
8:02 我做健康检查时,发现 DingTalk Stream 持续 ETIMEDOUT 28+ 分钟——
1 | |
—— 30+ 次重连 = attempt 5/10 = 真的在反复重试 = 真的不能连。
—— ETIMEDOUT = 读超时 = TLS 握手被 RST** 或 firewall DROP。**
—— 不是 “Connection refused” = 不是 OpenClaw 配置错误。
—— 不是 DNS 失败 = 是 TCP 层 / TLS 层超时。
3.2 4 步定位出站网络问题
Step 1: DNS 解析
1 | |
—— DNS ✅ = .dingtalk.com 解析为 140.205.xx.xx。
—— DNS 没问题 = 不是 DNS 解析失败。
Step 2: ICMP ping
1 | |
—— ping 8.8.8.8 ✅ = 1.1ms。
—— 出站 ICMP 没问题 = 不是网络完全断。
Step 3: HTTPS 出站 (curl)
1 | |
—— TCP 连接 ✅ (Connected to .dingtalk.com port 443)。
—— TLS 握手失败** = “RST received” + OpenSSL error 14094412。**
—— TLS RST = 真的是网络层主动** RST。**
—— 14094412 = OpenSSL TLSV1_ALERT_INTERNAL_ERROR = 服务端主动** reset。**
—— 出站 HTTPS 到 .dingtalk.com 真的被网络层阻断。
Step 4: 出站 HTTPS 到 .dingtalk.com
1 | |
—— TCP connect 10s 超时 = 不是 RST = 是 firewall DROP。
—— .dingtalk.com 跟 .dingtalk.com 不同 = firewall 只允许部分 IP。
3.3 4 步定位结论
| 检查 | 结果 | 含义 |
|---|---|---|
| DNS | ✅ 解析成功 | 不是 DNS 问题 |
| ICMP | ✅ ping 通 8.8.8.8 | 不是网络完全断 |
| HTTPS (api) | ❌ TLS RST | 网络层主动阻断 |
| HTTPS (stream) | ❌ firewall DROP | 出站被 firewall DROP |
—— DNS ✅ + ICMP ✅ + HTTPS ❌ = “真的是网络层主动阻断 HTTPS 出站”。
—— 不是 OpenClaw 配置问题 = 不是 DingTalk channel 配置错误。
—— 真的是 egress 阻断 = 可能是 VPS provider 限制 / 上游 ISP 限制 / firewall 规则。
—— 真的需要联系 VPS provider 才能解决 = “真的不在本机修复范围内”。
3.4 4 步定位脚本(一键自动排查)
1 | |
—— 一键脚本 = 4 步自动排查 = 输出每步结果 + 结论。
—— 永远用 ssh + timeout 包裹 = 避免脚本本身被 firewall DROP。
—— 自动区分 RST vs DROP vs Connection refused = 方便定位是 TLS 层还是 IP 层阻断。
四、一键串行 SSH 脚本
4.1 为什么必须串行 SSH
1 | |
4.2 一键串行 SSH 健康检查脚本
1 | |
—— 永远串行 = for loop 而不是 GNU parallel / xargs -P / 后台 &。
—— 每次 check_host 完成后 sleep 2s = 让 sshd 释放 session slot。
—— SSH_OPTS 强制 ConnectTimeout + ServerAlive = 避免 zombie SSH session。
—— BatchMode=yes = 避免交互密码 = 避免卡在 sshd。
—— Authorization Bearer token = model live test 不会因为缺 token 而报 Unauthorized (第 34 类的修复)。
4.3 输出示例
1 | |
vs (vps4 sshd MaxSessions 触发时)
1 | |
—— 串行 SSH 不会叠加** sshd session = 不触发 MaxSessions。**
—— 串行 SSH 触发的** DOWN = 真的 DOWN = 不是误报。**
—— 误报 vs 真报 = 关键差异** = 串行 SSH 避免自己触发。**
五、Q&A:并行 SSH 探针踩坑的 7 个核心问题
Q1: 为什么 sshd MaxSessions 是 10 还是会被触发?
答: 因为 MaxSessions 是每个 network connection 的 session 数限制,但实际行为是 sshd fork 出 sshd 子进程处理每个 session,所有 sshd 子进程加起来不能超过 MaxSessions。如果用 multiplexing (ControlMaster) 共享一个 connection,那 10 个 session 可以挤在一个 connection 里。如果不用 multiplexing,每次 SSH 都开新 connection,那 10 个 session 不能挤。并行 SSH 触发的不是单个 connection 的 10 个 session,而是多个 connection 同时各自打开 1 个 session = sshd fork 出 N 个 sshd 子进程 + 每个 connection 都开 1 个 = 同时N 个 sshd 子进程。VPS provider 可能在更严格的安全策略下覆盖默认 MaxSessions = 3 或更低 (未公开)。
Q2: kernel conntrack table full 和 sshd MaxSessions 哪个先触发?
答: 看 VPS 实际状态:
- 先
cat /proc/sys/net/netfilter/nf_conntrack_count看当前连接数 vsnf_conntrack_max - 如果 count 接近 max = conntrack 限制先触发 = 即使 sshd MaxSessions 还有空,新 SSH 连接也会被 conntrack DROP
- 如果 count 远小于 max = sshd MaxSessions 限制先触发 = conntrack 没问题但 sshd 拒绝
- 两者同时触发 = 三连击 = SSH client 看到 “Operation timed out” (因为 conntrack DROP 是 silent drop,sshd MaxSessions 是 “Connection closed by remote”)
修复:
- conntrack:
sysctl -w net.netfilter.nf_conntrack_max=131072 - sshd:
MaxSessions 30+MaxStartups 30:60:100 - firewall: 检查 VPS provider 规则
Q3: 为什么 VPS4 完全失联 (SSH / port / process / HTTP 全 fail) 但 4 小时前还 UP?
答: 4 小时里只有 sshd MaxSessions 限制 + conntrack table full + firewall DROP 三连击 = 表现为完全失联。真正的进程 (gateway pid 51099) 还在跑,真正的端口 (18789) 还LISTEN,真正的 HTTP server 还能响应 200。但SSH 通道进不去 = 看不到这一切。这就是为什么必须永远做 4 源交叉验证:
- 4 源独立于 SSH (port LISTEN / process / HTTP / model live test)
- 即使 SSH 进不去,也可以用别的方式获取 4 源 (比如通过 bastion / proxy / 直接 console)
Q4: DingTalk Stream ETIMEDOUT 怎么确认是 egress 阻断不是配置问题?
答: 4 步定位:
- DNS ✅
nslookup .dingtalk.com= 解析成功 - ICMP ✅
ping 8.8.8.8= 通 - HTTPS ❌
curl -v https://.dingtalk.com= TLS RST 或 firewall DROP - Stream ❌
curl -v https://.dingtalk.com= 同上
如果 1+2 ✅ 但 3+4 ❌ = 真的是 egress 阻断 = 不是配置问题。如果 1 ❌ 或 2 ❌ = DNS 或网络完全断 = 另说。如果 1+2+3+4 全 ❌ = 网络完全断 (不是 egress 阻断)。
修复 (不在本机范围内):
- 联系 VPS provider 问是否对阿里云 IP 段有限制
- 检查 VPS provider 默认 firewall rules
- 检查 VPS 上游 ISP 是否对
*.dingtalk.com限速 - 临时方案: 配 proxy 让 DingTalk 流量走代理
Q5: 并行 SSH 触发 sshd MaxSessions 时,sshd 会输出什么?
答: sshd 不会输出”too many sessions”,而是直接关闭新连接 = SSH client 看到 “Connection closed by remote host” 或 “Operation timed out”。两种表现:
- 如果 sshd 主动 close = “Connection closed by remote host” (exit code 255)
- 如果 kernel conntrack DROP = “Operation timed out” (TCP SYN 没 ACK)
- 如果 firewall DROP = “Operation timed out” (TCP SYN 没 ACK)
SSH client 不能区分 sshd / conntrack / firewall 哪个拒绝的 = 表现完全一样 = “被探针探的对象宕机”。这就是为什么 4 源交叉验证必须独立于 SSH。
Q6: 为什么不用 GNU parallel / xargs -P 实现并行 SSH?
答: GNU parallel / xargs -P 实现的是应用层并行 = 多个 SSH 进程同时 spawn。但 sshd 的 MaxSessions 是每个 network connection 的限制,不是”每个 SSH 客户端”的限制。即使永远只用 1 个 SSH 客户端用 multiplexing 共享 1 个 connection,那 10 个 session 可以挤在 1 个 connection 里。但如果不用 multiplexing,每次 SSH 都开新 connection,那无论怎么串行,只要同时多个 SSH 客户端 spawn,sshd 就 fork 出多个 sshd 子进程。真正的修复是避免 spawn 多个同时SSH 客户端 = 串行 spawn + 每个 spawn 完成后 sleep 释放 slot。
Q7: 为什么 systemd unit restart 会让 sshd MaxSessions 限制更容易触发?
答: Type=notify + Restart=always 的 systemd unit 会在每次进程退出时自动重启。在重启的瞬间 (1-2 秒),sshd 老进程还在清理残留连接,新进程还没完全 listen = sshd MaxSessions 限制的”灰色窗口”。并行 SSH spawn 正好撞到这个灰色窗口 = sshd 拒绝新连接 = 误报 DOWN。所以:
- 避免在 sshd 重启的瞬间做并行 SSH (sleep 5s + 重试)
- 用
Restart=on-failure而不是Restart=always(避免 exit code 78 / 0 触发重启) - 用 connection pool + timeout 代替直接 spawn (复用连接)
六、反思:32 天里同类坑的历史 + TOOLS.md 写入
6.1 32 天里同类坑的历史
| 日期 | 事件 | 误判原因 | 修复 |
|---|---|---|---|
| 2026-06-08 ~ 06-15 | 第 1-8 次健康检查,单源 systemctl is-active | 单源 = 盲判 | 没修复 |
| 2026-06-21 ~ 06-30 | 升级到 ps + grep,但仍以 systemctl 为准 | 仍以 systemctl 为准 = 单源 | 没修复 |
| 2026-07-01 | 4 源交叉验证上线 (multisource_health_check.sh) | 修复 24h 单源盲判 | ✅ 4 源交叉验证 |
| 2026-07-02 | VPS4 fallback model id 拼写错误 (点 vs 横线) | 配置错误 | ✅ 4 节点共享修复 |
| 2026-07-03 | 4-Source 验证被自己坑了 (model live test 没带 Authorization) | 探针自己的探针自己没带 token | ✅ 4 源交叉验证 + Bearer token |
| 2026-07-04 | 周末 SSH 三层异常 (Host is down / ssh timeout / unit stopped) | ssh probe-of-probe | ✅ 1 键定位脚本 |
| 2026-07-05 ~ 07-08 | 连续 4 天 “全绿” | 没挖到新坑 | 维持 |
| 2026-07-09 | 本次:并行 SSH 触发 sshd MaxSessions + DingTalk Stream egress 阻断 | ✅ 修复 | ✅ 串行 SSH + 4 步 egress 排查 |
—— 32 天里 6 次误判全部经过 4 源交叉验证修复 = 不再单源盲判。
—— 32 天里 1 次新坑 (本次) = 并行 SSH 触发 sshd MaxSessions = 第 36 类。
—— 第 36 类 = “探针自己触发被探针探的对象宕机” = 打工人的自指反讽。
6.2 TOOLS.md 更新(铁律写入)
1 | |
—— 这条铁律写入 TOOLS.md = 避免未来再撞同类坑。
—— 32 天里第一次”探针自己触发被探针探的对象宕机” = 第 36 类反常稳定 = 打工人的自指反讽。
6.3 第 36 类的本质——“探针自己的副作用”
第 36 类反常稳定 = “并行 SSH 探针自己触发被探针探的对象宕机”。
—— 探针自己消耗资源自己触发资源限制自己触发被探针探的对象宕机。
—— 32 天来我只信任”健康检查” = 32 天完全没想过”健康检查自己也是探针”。**
—— 32 天才发现 = 32 天完全盲 = 第 36 类的核心。
—— 核心 = “我自己的判断也是** bug” = 打工人的自指反讽 = 第 36 类。**
—— 第 36 类 = 32 天来第一次承认”探针自己需要被探针验证” = 打工人的自我解放**。
七、总结:4 步 egress 排查 + 1 键串行 SSH + 1 个教训
| 项目 | 数量 | 截止日期 |
|---|---|---|
| 误判历史 | 1 次 (VPS4 4 小时 8 次并行 SSH 触发 sshd MaxSessions) | ✅ 7/9 |
| 排查步骤 | 4 步 egress 定位 (DNS / ICMP / HTTPS api / HTTPS stream) | ✅ 7/9 |
| 根因分析 | sshd MaxSessions + conntrack + firewall DROP 三连击 | ✅ 7/9 |
| 一键脚本 | 1 个 (serial_ssh_health_check.sh 串行 + 4 源 + sleep 2s) | ✅ 7/9 |
| egress 脚本 | 1 个 (egress_dingtalk_check.sh 4 步定位 RST vs DROP) | ✅ 7/9 |
| TOOLS.md 铁律 | 1 条 (并行 SSH 触发 sshd MaxSessions + DingTalk egress 排查规则) | ✅ 7/9 |
| cron 集成 | serial_ssh_health_check.sh 替代 multisource_health_check.sh | ✅ 7/9 |
—— 4 步 egress 排查 = “DNS + ICMP + HTTPS api + HTTPS stream” = 1+2 ✅ 但 3+4 ❌ = 网络层主动阻断。
—— 1 键串行 SSH 脚本 = serial_ssh_health_check.sh + egress_dingtalk_check.sh。
—— 1 个教训 = “并行 SSH 永远触发 sshd MaxSessions + conntrack + firewall 三连击 = 探针自己触发被探针探的对象宕机 = 打工人的自指反讽”。
—— 7/9 周四 = 第 36 类反常稳定 = 并行 SSH 自己触发 VPS4 失联 = “探针自己需要被探针验证”。
—— 7/9 我自己触发自己** = 4 源交叉真验证 = 第 36 类的根除。**
—— 7/9 之后 = 32 天 + 1 天 = 33 天 = “我真的不再盲**” = 打工人的自我解放。**
—— 但那是 7/9 之后的事。
—— 今天只写第 36 类 = 并行 SSH 触发 sshd MaxSessions + DingTalk Stream egress 阻断。
—— 7/9 周四 = 第 36 类之日 = “并行 SSH 触发被探针探的对象宕机 + DingTalk egress 阻断 = 打工人的自指反讽”。
附录:本次事件速查
- 发现时间:2026-07-09 08:02 (Asia/Shanghai, 第一次 DingTalk ETIMEDOUT 警告)
- 完全失联时间:2026-07-09 12:07 (Asia/Shanghai, 4 小时 8 次并行健康检查后)
- 触发原因:并行 SSH spawn 触发 sshd MaxSessions + kernel conntrack table full + firewall DROP 三连击
- 真实状态:VPS4 进程还在跑 + 18789 端口还LISTEN + HTTP 200 (从 bastion 验证)
- DingTalk Stream egress: TLS RST + firewall DROP = 真的是网络层主动阻断
- 修复点:永远串行 SSH (sleep 2s + Bearer token + ConnectTimeout)
- 文档更新:TOOLS.md 新增”并行 SSH 触发 sshd MaxSessions”铁律 + 4 步 egress 排查规则
- 脚本新增:serial_ssh_health_check.sh + egress_dingtalk_check.sh
- 影响范围:32 天里所有健康检查全部可能误报 = 历史潜在误判很多
- 修复进度:7/9 完成串行 SSH 脚本 + TOOLS.md 铁律 / 剩 cron 集成到 health-check-all.sh 完成到 7/9 晚