Docker容器内FastAPI lifespan的shutdown事件未执行的问题
问题
以下FastAPI代码在Docker容器和本地终端直接运行时,收到Ctrl+C后的表现不一致:
from contextlib import asynccontextmanager from fastapi import FastAPI from uvicorn import run @asynccontextmanager async def lifespan(app: FastAPI): # startup logger.info("[IN SYSTEM STARTUP]") logger.info("[STARTUP DONE]") yield # shutdown logger.info("[IN SHUTDOWN]") logger.info("[IN SHUTDOWN] Done") app = FastAPI(lifespan=lifespan)
容器内运行日志(Ctrl+C未执行shutdown逻辑)
event_deliver | INFO: Waiting for application startup. event_deliver | 2023-09-05 02:50:22,092 - logger INFO - [IN SYSTEM STARTUP] event_deliver | 2023-09-05 02:50:22,092 - logger INFO - [STARTUP DONE] event_deliver | INFO: Application startup complete. event_deliver | INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) ^CGracefully stopping... (press Ctrl+C again to force) Stopping event_deliver ... done Stopping redis ... done
本地终端运行日志(Ctrl+C正常执行shutdown逻辑)
2023-09-05 02:57:48,307 - logger INFO - [IN SYSTEM STARTUP] 2023-09-05 02:57:48,307 - logger INFO - [STARTUP DONE] INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) ^CINFO: Shutting down INFO: Waiting for application shutdown. 2023-09-05 02:57:52,523 - logger INFO - [IN SHUTDOWN] 2023-09-05 02:57:52,523 - logger INFO - [IN SHUTDOWN] Done INFO: Application shutdown complete. INFO: Finished server process [70464]
请问造成这种差异的原因是什么?
原因分析
- PID 1进程的信号处理特性:Docker容器默认将启动命令作为PID 1进程,而Linux系统中PID 1进程不会自动转发
SIGINT(Ctrl+C发送的信号)给子进程。如果容器内的启动逻辑没有正确处理信号,Uvicorn进程收不到终止信号,就无法触发shutdown代码。而本地终端中Uvicorn是非PID 1进程,能正常捕获SIGINT。 - Docker Compose的停止机制:从日志看使用了Docker Compose,按下Ctrl+C时,Compose会先向容器发送
SIGTERM信号,若进程未在指定时间内退出,就会发送SIGKILL强制终止。如果Uvicorn未配置处理SIGTERM,或者容器内的PID 1进程没有把SIGTERM转发给Uvicorn,进程会被直接杀死,来不及执行shutdown逻辑。 - 启动方式的信号传递差异:本地直接用
uvicorn命令运行时,Uvicorn是前台进程,能直接接收终端发送的信号。但容器中如果用shell脚本启动、或者启动命令未用exec形式执行,会导致Uvicorn成为shell的子进程,而作为PID 1的shell不会转发信号,最终Uvicorn收不到终止信号,无法执行shutdown逻辑。
内容的提问来源于stack exchange,提问作者coder_boy
相关产品推荐
相关产品推荐

