1509 字
8 分钟
同步等长命令,等的是连接不是结果

命令跑多久,那条传回结果的连接就要活多久。连接一断,结果只在断掉那一刻的内存里,没有第二个地方存过。我的判断是:这条连接不该承载结果。让结果先在沙箱侧落下来,调用方自己来取,连接断没断就不重要了。

同步等待与提交-轮询两条路径上「结果存在哪里」的差别

72 次里 17 次,命令其实早就跑完了#

实验集群上跑 64 并发、72 条长命令,17 条(23.6%)已经结束却没有返回。进程早就退出,调用方那边就是不返回,一直挂到自己超时。

沙箱里的 execd 要等命令结束才发回结果,结果经引擎、沙箱服务沿一条连接传回,期间每 3 秒一次心跳。这条连接中途断掉,结束通知就没有第二条路回来。

断开的原因到今天也没查明。中间任何一跳静默丢弃连接——负载均衡的空闲回收、代理的连接回收——都长这个样子。

修复前,沙箱服务这边还把后果放大了一层:读这条连接时把读超时显式关掉了(read=None)。通知一丢,服务自己先无限等,调用方跟着挂死。

真正卡死的是 exec 不幂等。响应丢了,调用方查不到这条命令现在怎么样,也不敢重发——重发可能让它执行两遍。剩下的只有等。

加大超时在这里不解决问题。超时管的是等多久放弃,不管结果去了哪。

提交拿编号,剩下的事靠查#

改法是把结果从连接里搬出去,沙箱服务加三个端点:

端点语义
POST /exec-jobs后台提交,立即返回 command_id;同一个幂等键不重复执行
GET /exec-jobs/{id}查状态,按游标拿增量输出
DELETE /exec-jobs/{id}请求取消,不等终止完成就返回,终态经 GET 确认

execd 本来就有后台提交、状态查询、游标日志、中断这几个原语,沙箱服务这边是三个薄封装,引擎那一层不用动。

幂等键在执行之前就写进表里,占位的是一个 Future。后到的同键请求等在这个 Future 上,拿到同一个 command_id,命令只跑一次。响应丢失后 SDK 自动重试,正好是「首次提交还在途中又来一次」这种形态。

提交这一步不跟着请求走:shield 包住,再丢进独立 task。客户端中途断开,已经发出去的提交不会被掐掉,结果留在表里等重试来取。

有一种失败说不清命令到底跑没跑:execd 收下了命令,但 id 没返回。这种占位留在表里,同键重提一律拿到同一个 502,不会再执行一次。只有确认没执行的失败才移除占位,放行真正的重试。

超时由沙箱侧强制执行,提交时一并下发给 execd。客户端断网、进程被杀,命令照样到点被终止,不留下孤儿进程。没有这条,「调用方不等了」就等于「没人管了」。

判定 timed_out 要用的原始超时值由调用方查询时带回来(SDK 自动带),服务端不存。沙箱服务重启、多开副本,这个判定都不会错。

游标式读输出,坑都在收尾那一段#

终态不等于输出读完。 命令已经 exit,最后一块日志可能还没拉到。SDK 拿到终态之后还要短轮询收尾,直到一轮「拉取成功且没有新输出」才算完。少这一步就是静默截断:不报错,输出短一截。

「拉取失败」和「没有更多输出」在协议上长得一样,都是空。GET 用一个显式的 logs_ok=false 把两者分开。日志拉取失败不连累状态查询——状态是轮询判停用的,输出下一轮再补。

游标丢失有两种,处理方向相反。 从头请求却返回了无游标,按全量快照整体替换,否则每轮都往后追加同一批内容,复杂度退化成 O(N²)。带游标请求丢了游标,丢弃本块、下一轮重取,不会重复也不会少。

404 只认结构化状态码。 不去匹配错误文案里的子串:瞬态错误的文案里恰好带 not found,会被当成永久 404,SDK 就把一个还活着的任务放弃了。

代价和还没解决的#

轮询比守着一条连接费请求。间隔做成自适应,2 秒涨到 15 秒,压的就是 64 并发以上的请求量;按每个沙箱最多 3 秒 1 次算,500 个沙箱一起轮询,落到引擎上约 170 次/秒。

幂等表是进程内内存,只在单副本下成立。这个服务的生产部署从 2 副本降到 1,一部分原因就是它——两个副本下,同键重试落到另一个副本会把命令再执行一遍。挪到外部存储才能恢复多副本。

同步 /exec 为什么不返回、连接断在哪一跳,前面说了,还是没查出来。这个改法只是绕开它。

执行结果也没有持久记录。execd 重启后,调用方即使还拿着 command_id,拿到的也只是 JOB_NOT_FOUND。这个报错只能说明当前这个 execd 不认识这个编号,不能说明命令没执行过。进程还在不在,要看是 execd 单进程重启、整个容器重启,还是沙箱已经被删掉。

短命令仍旧走同步 /exec,单次往返开销最低。分界线定在一分钟:预计一分钟内完成用 exec,更长一律走 job。同步上限 7200 秒,job 上限 14400 秒——后台执行不占服务到沙箱的那条 HTTP 连接,所以能放更长。