执行摘要
- 一句话:修复 HTTP 服务器启动与信号处理器注册的竞态问题
- 推荐动作:该 PR 值得精读,因为它展示了异步启动中信号处理器注册的时序问题,以及如何通过轮询等待和任务状态检查来避免竞态。重点关注
while 循环的设计与启动失败时的异常传播,这是同类问题中的典型解法。对于维护者,建议后续补充针对启动失败场景的测试,并关注 njhill 提到的潜在 follow-up 改进。
功能与动机
PR body 中说明:server.serve 会先调用 capture_signals,它会覆盖 SIGINT 和 SIGTERM 处理器,然后再执行 startup 并启动服务器。如果 app 的信号处理器先注册,就会被 capture_signals 覆盖,导致服务器先被关闭,app 的优雅关闭流程无法按预期工作。这是一个真实的竞态条件,影响服务优雅关闭的可靠性。
实现拆解
实现拆解如下:
-
修改文件与位置:变更集中在 vllm/entrypoints/launcher.py 的 serve_http 函数。在创建 server_task(调用 server.serve())之后、注册 app 的 SIGINT/SIGTERM 处理器之前,插入一段等待逻辑。
-
等待 UVicorn 启动完成:新增 while 循环,条件为 not server.started and not server_task.done(),每 0.1 秒 await asyncio.sleep(0.1) 检查一次。server.started 在 uvicorn 的 startup() 完成后置位,此时 capture_signals() 必然已执行过,因此 app 的信号处理器不会再被覆盖。
-
处理启动失败场景:循环体退出后,再检查 if server_task.done(),若 uvicorn 启动失败(如端口绑定错误导致 SystemExit),则 await server_task 会重新抛出异常,使 serve_http 及时报错而不是无限挂起。
-
后续逻辑:等待结束(或异常传播)后才创建 shutdown_event、定义 signal_handler 并调用 loop.add_signal_handler 注册 app 的信号处理器,从而保证时序正确。
该改动不涉及测试、配置或部署配套变更,仅调整了启动阶段的控制流。
关键文件:
vllm/entrypoints/launcher.py(模块 启动器;类别 source;类型 core-logic;符号 serve_http, signal_handler): 唯一的变更文件,位于 HTTP 服务启动入口,修复了信号处理器注册的竞态,并处理启动失败场景。
关键符号:serve_http
关键源码片段
vllm/entrypoints/launcher.py
唯一的变更文件,位于 HTTP 服务启动入口,修复了信号处理器注册的竞态,并处理启动失败场景。
# 创建 uvicorn server task 后,需要等待 server 真正启动完成再注册 app 自己的信号处理器。
# 原因 : uvicorn.Server.serve() 内部先调用 capture_signals() 安装它自己的 SIGINT/SIGTERM 处理器,
# 再执行 startup() 启动服务。若 app 的信号处理器先注册,就会被 uvicorn 覆盖,
# 导致优雅关闭逻辑失效。server.started 在 startup() 完成后才置位,因此等待它即可保证顺序。
watchdog_task = loop.create_task(watchdog_loop(server, app.state.engine_client))
server_task = loop.create_task(server.serve(sockets=[sock] if sock else None))
# 等待 server 启动完成,或 server task 提前结束(如端口绑定失败抛异常)。
logger.info("API server: waiting for HTTP server to start")
while not server.started and not server_task.done():
await asyncio.sleep(0.1)
if server_task.done():
# 若启动失败,这里会重新抛出异常,避免 serve_http 无限挂起。
await server_task
logger.info("API server: HTTP server started")
# 此时 uvicorn 已安装自己的信号处理器,注册 app 的处理器不会被覆盖。
shutdown_event = asyncio.Event()
def signal_handler() -> None:
if shutdown_event.is_set():
return
logger.info_once("[shutdown] API server: shutdown triggered")
shutdown_event.set()
loop.add_signal_handler(signal.SIGINT, signal_handler)
loop.add_signal_handler(signal.SIGTERM, signal_handler)
评论区精华
核心讨论来自 AI 辅助审查机器人 inferact-vllm-bot(代表 maintainer simon-mo 审核):
风险与影响
关联脉络
参与讨论