KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

04 · 停机窗口 — keel 龙骨

类型:主题概念 · 应用课对应:06 章 · 升级与回滚:要么升上去,要么原样退回来

类型:主题概念 · 应用课对应:06 章 · 升级与回滚:要么升上去,要么原样退回来

这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。它不讲信号在内核里是怎么传递的。

一、它为什么会出现

worker 手上有一个要跑 20 秒的任务。这时一条停止命令进来了。

两种结局差在一个参数上:

窗口 > 任务耗时:任务跑完 → done
窗口 < 任务耗时:窗口用完 → 进程被杀 → 那一行永远停在 running

第二种结局最难受的地方在于它不报错。客户看到的现象是"有个任务卡住了",而重启 worker 也救不回来——新 worker 只领 pending 的任务。

需求一句话:从"进程随时可以被杀死"到"手上的活不能丢",中间需要一个明确的时长。

二、它是什么

一句话:停机窗口(stop_grace_period)是收到 SIGTERM 之后、被 SIGKILL 之前,留给进程的那段时长。

它不是下面这三样东西:

三、它长什么样

它写在编排文件里,三个服务三个值:

db:      stop_grace_period: 30s
web:     stop_grace_period: 15s
worker:  stop_grace_period: ${BOARD_WORKER_STOP_GRACE:-60s}

worker 那一行是环境变量,因为它必须和现场的任务耗时对齐:

BOARD_JOB_SECONDS=20         # 一个任务要跑多久
BOARD_WORKER_STOP_GRACE=60s  # 停它之前愿意等多久

窗口够用时,worker 的日志是完整的四行:

{"event": "job.started",        "id": 1, "title": "停机窗口实验"}
{"event": "shutdown.requested", "signal": "SIGTERM", "in_flight": 1}
{"event": "job.done",           "id": 1, "seconds": 20.03}
{"event": "worker.stopped",     "in_flight": null}

把窗口改到小于任务耗时,日志就断在第二行:没有 job.done,也没有 worker.stopped。

四、它怎么工作

一次干净停机,四步:

① 送到   停止命令给容器里的 1 号进程发 SIGTERM,窗口开始计时
   ↓
② 接到   进程把信号翻译成"不再领新任务",手上那一个继续跑
   ↓
③ 收尾   手上的活干完、写回结果,然后自己退出
   ↓
④ 兜底   到这一步还没退,窗口用完就强杀 —— 结果只能靠"孤儿回收"事后收拾

第 ④ 步解释了为什么"窗口调对"不是全部:容器被 OOM 杀、机器断电,这些都不看窗口。所以这套系统还有第二道防线——重启时把"多久没心跳"的 running 任务放回 pending。第一道防住常规停机,第二道兜住异常终止。

五、它会带出哪些概念

六、回到应用课

应用课里的那句话 现在你知道它为什么成立
"stop_grace_period 的语义只有一句:SIGTERM 之后等多久才 SIGKILL" 所以它只有一个正确的取值方向:不小于手上那个任务的最长耗时
"设得比任务短:手上的活去哪了?没人知道" 强杀不给进程任何机会,悬空状态只能由"孤儿回收"事后收拾
"它是现场参数,不是制品参数" 任务耗时是现场的,所以这个窗口也必须是现场的——同一个镜像几种角色用几个值
"「窗口对齐」和「孤儿回收」是同一件事的两道防线" 前者管常规停机,后者管异常终止;只看其中一道,就会在另一道上翻车

这一页改变了你的哪个判断:定这个数的时候,你不再问"一般设多少",而是先问**"这个角色手上最长的一个活要跑多久"**——先量活,再定窗口,不是反过来。

→ 这一页的展开在应用课 06 章 · 升级与回滚:要么升上去,要么原样退回来。

自测题

  1. 停机窗口设得比任务耗时短,那行数据会停在什么状态?为什么重启也回收不了它?
  2. 为什么同一个镜像的三个角色要配三个不同的停机窗口?

进入 keel 阅读