云沙盒空闲暂停的判定与保活方案。适用于 instabox 这类"一段时间不操作就把整台 VM 冻结"的沙盒平台。
实测结果:连续 45 小时零冻结(uptime 2 天,SUSPENDED_GAP 0 条)。
| 做法 | 结果 |
|---|---|
| 沙盒内制造 CPU 负载 | 无效,照样被冻结 |
| 沙盒内定时网络请求 | 无效 |
| 从外部访问沙盒的端口映射地址 | 有效 |
判定发生在宿主机层面,沙盒内部做什么它都不看。要续命只能从外面碰它。
这不是实现细节问题,是层级问题。三条证据可以定性:
沙盒内每分钟执行一次的心跳脚本从未缺席,但日志出现了断档:
04:17:44 → 04:18:49 → 04:19:54 → 04:20:59
SUSPENDED_GAP: 941s ← 被冻结 15 分 41 秒
04:36:40 → 04:37:42 → 04:38:46
断档前后都是准点条目,说明心跳进程没死,是整台机器被冻住了。
沙盒内的 guest agent 每 5 分钟向宿主机上报一次(op=reclaim),在同一时间窗口也一起断档。连平台自己的组件都停了,说明冻结在 VM 层,不在应用层。
04:36:34 op=sync_clock ← 重新校时
04:36:34 op=configure ← 重新下发配置
04:36:35 op=configure_ipv6
只有 VM 挂起后恢复才会走这套流程。
三条同时成立,就可以确定:继续在沙盒内部研究"制造什么活动"是浪费时间。
对 guest agent 做 strings,能看到这些符号:
internal/guestagent/activity.go
guestagent.(*Agent).activity
guestagent.activityTimeout
guestagent.ActivityProbes
guestagent.RunProbe / probeSignal / probeTimeout
probe output has no "busy"
systemctl is-active %s
看起来像是在检测 CPU busy、服务状态,很容易据此得出"造 CPU 负载就能保活"的结论。
但这是错的。 如果 CPU busy 真能续命,证据一里的 941 秒冻结就不可能发生。这些探针只是使用统计,不参与暂停决策。
教训:看到二进制里有 activity 相关符号,不等于找到了控制开关。必须用行为验证,不能靠符号推断。
平台通常把沙盒端口映射成 <端口>-<机器ID>.<通配域>。注意裸域往往不解析,只有子域解析到统一网关:
# 机器 ID(沙盒内执行)
cat /etc/instabox/identity | sed 's/^m_//'
# 输出形如 abc123def456ghi789jkl(25 位左右的小写字母数字)
# 验证网关
curl -sS -m 10 https://3000-<MACHINE_ID>.instabox.tech返回 Nothing is listening on this port in the sandbox 说明网关可达、沙盒该端口空着。
这条 URL 是不需要登录态的外部探活入口,比控制台 API 容易自动化得多。
curl -fsSL https://raw.githubusercontent.com/poisonhs/instabox-keepalive/main/scripts/sandbox-side.sh | sh -s -- 3000脚本会装一个 systemd 服务,用 python3 在指定端口常驻监听。
验证:
systemctl is-active ka-http
ss -tlnp | grep ':3000'必须是另一台机器。沙盒自己访问自己不算外部流量,等于没做。
curl -fsSL https://raw.githubusercontent.com/poisonhs/instabox-keepalive/main/scripts/outside-side.sh | sh -s -- 3000-<MACHINE_ID>.instabox.tech脚本装一个 5 分钟一次的 systemd timer,跑完自动判读首次结果。
curl -fsSL https://raw.githubusercontent.com/poisonhs/instabox-keepalive/main/scripts/hold-keepalive.py \
-o /usr/local/bin/hold-keepalive.py
chmod +x /usr/local/bin/hold-keepalive.py
# 改文件里的 HOST 变量后再装 systemd 单元(见文件末尾注释)与定时敲门的区别:定时任务每次新建连接、请求完就断;长连接一次 TLS 握手后复用同一 TCP,每 60 秒打一次请求,网关看到的是持续会话。
同时挂两条时无法区分哪条在起作用,验收只按整体判。
不要凭"现在能通"下结论。 单次 200 什么都不能证明,必须等过了平台的空闲窗口再看:
| 指标 | 合格线 |
|---|---|
uptime |
连续不少于 2 天 |
心跳日志 SUSPENDED_GAP |
0 条 |
敲门日志 502 |
0 条 |
三条同时成立才算保活成立。
| 码 | 含义 | 动作 |
|---|---|---|
200 |
链路通,沙盒活着 | 正常 |
502 |
沙盒内该端口没服务 | 先查沙盒侧进程 |
429 |
请求太密,网关限流 | 间隔拉到 60 秒以上 |
000 |
DNS / 网络 / 超时 | 查域名拼写与 DNS |
429 和 000 都不代表沙盒挂了,只有 502 指向沙盒侧。 报告问题时这点要说清楚,否则容易把限流当故障。
| 项 | 值 | 原因 |
|---|---|---|
| 定时敲门间隔 | 5 分钟 | 远低于平台空闲阈值 |
| 长连接复用节奏 | 60 秒 | 15 秒一次会吃 429 |
| 监听端口 | 任意空闲端口 | 3000 只是习惯 |
| 误区 | 事实 |
|---|---|
看到二进制里有 activity 符号,就以为找到了保活开关 |
符号只说明有这功能,不说明它决定暂停。必须用行为验证 |
用 timeout 3 yes || timeout 3 yes 做兜底 |
timeout 超时退出码是 124(非零),会触发兜底分支,跑两遍 |
| 15 秒敲一次更保险 | 会吃网关 429。60 秒是安全下限 |
看到 429 以为沙盒挂了 |
只有 502 指向沙盒侧,429 是本地节奏太快 |
| 心跳脚本里加日志轮转 | tail > tmp && mv 在 oneshot 里会偶发失败,直接 >> 追加即可 |
部署后连续运行 45.6 小时:
| 指标 | 数值 |
|---|---|
| 连续运行 | 45.6 小时 |
| 系统 uptime | 2 天 24 分 |
SUSPENDED_GAP |
0 次 |
| 敲门成功 | 511 / 521 |
| 502 出现次数 | 0 |
对照组:只跑内部心跳时,停止操作 941 秒即被冻结。
Q: 为什么不直接在沙盒里跑个 while 循环?
A: 因为冻结发生在宿主机,沙盒内部的进程连同 CPU 一起被挂起。你的循环在冻结期间根本不执行。
Q: 用沙盒自己 curl 自己的端口算吗?
A: 不算。流量没有离开机器,网关看不到。
Q: 定时任务必须放公网 VPS 吗?
A: 不必须,只要是一台常开且能访问互联网的机器即可。家里的树莓派、路由器 cron、甚至另一台云主机都行。关键是不能是沙盒自己。
Q: 平台更新后突然失效了?
A: 先看 502 是否出现。如果沙盒侧服务还在但外部访问不通,可能是网关或映射规则变了,重新确认端口映射地址。
Q: 会不会被平台判定为滥用?
A: 保持 5 分钟一次的频率、走正常 HTTPS 请求,就是普通访问。不要压到秒级,那会触发 429 甚至被当成攻击。
本项目仅用于自己拥有或经授权管理的沙盒实例,目的是避免因平台策略导致正在运行的任务中断。请遵守所用平台的服务条款。
MIT