Docker环境下如何让Gunicorn将SIGINT信号转发给Uvicorn?
问题描述
在WSL2的Docker容器中通过CMD启动bash脚本时,SIGINT信号(Ctrl+C)处理异常。脚本内容如下:
#!/usr/bin/env bash python manage.py init_db exec gunicorn foobar.asgi:application \ --worker-class uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8000 \ --graceful-timeout 5 \ --log-level debug \ -w 4
按下Ctrl+C后,Gunicorn会在5秒后强制终止Uvicorn Worker,输出如下错误:
^C[2024-10-29 21:15:35 +0000] [1] [INFO] Handling signal: int [2024-10-29 21:15:40 +0000] [1] [ERROR] Worker (pid:8) was sent SIGKILL! Perhaps out of memory? [2024-10-29 21:15:40 +0000] [1] [ERROR] Worker (pid:9) was sent SIGKILL! Perhaps out of memory? [2024-10-29 21:15:40 +0000] [1] [ERROR] Worker (pid:10) was sent SIGKILL! Perhaps out of memory? [2024-10-29 21:15:40 +0000] [1] [ERROR] Worker (pid:7) was sent SIGKILL! Perhaps out of memory?
已尝试三种临时方法:
- 进入容器内部启动脚本:Uvicorn Worker能按时退出,但Gunicorn仍输出SIGINT错误日志
^C[2024-10-29 21:21:56 +0000] [1] [INFO] Handling signal: int ... worker shutdown cleanup output omitted [2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:15) was sent SIGINT! [2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:14) was sent SIGINT! [2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:13) was sent SIGINT! [2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:10) was sent SIGINT! [2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:11) was sent SIGINT! - 在
docker run命令中添加-it参数:表现与上述方法一致docker run -it -p 8000:8000 -v $(pwd):/app foobar:latest - 替换
exec为手动信号转发:按下Ctrl+C仅输出Gunicorn处理信号的日志,但Worker未执行关闭逻辑
输出:#!/usr/bin/env bash python manage.py init_db # Forward SIGINT signal trap 'kill -INT $PID' INT # Start Gunicorn gunicorn foobar.asgi:application \ --worker-class uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8000 \ --graceful-timeout 5 \ --log-level debug \ -w 4 & \ PID=$! # Wait for Gunicorn process wait $PID^C[2024-10-29 21:26:37 +0000] [7] [INFO] Handling signal: int
即使直接在Dockerfile中运行Gunicorn,也会出现相同异常:
CMD ["gunicorn", "foobar.asgi:application", \ "--worker-class", "uvicorn.workers.UvicornWorker", \ "--bind", "0.0.0.0:8000", \ "--graceful-timeout", "5", \ "--log-level", "debug", \ "-w", "4"]
完整Dockerfile如下:
# >>> Build stage <<< FROM python:3.11-slim AS build WORKDIR /app # Install C toolchain and C build-time dependencies. RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install --no-install-recommends --assume-yes \ # gcc, make, etc. build-essential \ # psycopg2 client libs and header files for building psycopg2 libpq-dev && \ rm -rf /var/lib/apt/lists/* # Create virtual environment and add it to PATH. RUN python -m venv /venv ENV PATH="/venv/bin:$PATH" # Copy requirements and install. COPY requirements.txt . RUN pip install --upgrade pip && \ pip install --no-cache-dir --no-warn-script-location -r requirements.txt # >>> Run stage <<< FROM python:3.11-slim WORKDIR /app # Install C runtime dependencies. RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install --no-install-recommends --assume-yes \ # psycopg2 runtime lib libpq5 && \ rm -rf /var/lib/apt/lists/* # Create and switch to appuser. RUN groupadd -r appuser && \ useradd --no-log-init -r -g appuser appuser && \ chown -R appuser:appuser /app USER appuser # Copy virtual environment from the build stage and add it to PATH. COPY --chown=appuser:appuser --from=build /venv /venv ENV PATH=/venv/bin:$PATH # Copy the rest of the application code. COPY --chown=appuser:appuser . . # Set Python environment variables. # Prevent Python from writing .pyc files ENV PYTHONDONTWRITEBYTECODE=1 # Ensure output is sent to stdout/stderr immediately ENV PYTHONUNBUFFERED=1 # Start server. CMD ["/app/deployment/start-django"]
补充:在仅依赖django、gunicorn、uvicorn[standard]的全新Django项目中,同样出现该异常。
原因分析
- Docker信号转发机制问题:未使用
-it参数时,Docker不会将终端SIGINT信号转发给容器内的PID 1进程。bash作为PID 1时,默认不会将信号转发给子进程,即使使用exec替换进程,无终端环境下信号传递仍可能异常。 - Gunicorn与Uvicorn Worker的信号兼容性:Uvicorn Worker对SIGINT的处理依赖终端环境支持,非交互式终端模式下,Worker无法正确接收并处理信号,导致Gunicorn超时后发送SIGKILL强制终止。
- 手动信号转发的局限性:第三种方法仅转发SIGINT给Gunicorn主进程,但后台运行的Gunicorn在非终端环境下,信号链传递不完整,Worker无法触发关闭逻辑。
解决方案
方法1:交互式终端模式(开发环境适用)
始终用-it参数启动容器,确保信号正确转发:
docker run -it -p 8000:8000 foobar:latest
此方法能让Worker正常退出,仅保留Gunicorn的SIGINT错误日志(属于日志逻辑问题,不影响关闭流程)。
方法2:使用tini作为PID 1进程(生产环境推荐)
tini是容器专用初始化进程,能正确转发信号给子进程,解决PID 1的信号处理缺陷。修改Dockerfile:
# >>> Run stage <<< FROM python:3.11-slim WORKDIR /app # Install tini and runtime dependencies RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install --no-install-recommends --assume-yes \ libpq5 \ tini && \ rm -rf /var/lib/apt/lists/* # ... 其余原有内容不变 ... # Set tini as entrypoint ENTRYPOINT ["/usr/bin/tini", "--"] # 保持原CMD不变 CMD ["/app/deployment/start-django"]
tini作为PID 1会将SIGINT正确转发给bash脚本,再传递给Gunicorn和Worker,确保所有进程正常关闭。
方法3:优化bash脚本信号处理
修改启动脚本,确保信号传递给所有子进程,避免后台运行导致的信号丢失:
#!/usr/bin/env bash python manage.py init_db # 转发所有终止信号给Gunicorn进程 trap 'kill -TERM $PID; wait $PID' INT TERM QUIT gunicorn foobar.asgi:application \ --worker-class uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8000 \ --graceful-timeout 5 \ --log-level debug \ -w 4 & PID=$! # 等待Gunicorn进程退出,确保脚本不提前结束 wait $PID
结合tini使用此脚本,可最大化保证信号传递完整性。
方法4:直接使用Uvicorn替代Gunicorn(可选)
若不需要Gunicorn的多进程管理特性,直接用Uvicorn启动服务,信号处理更简洁:
#!/usr/bin/env bash python manage.py init_db exec uvicorn foobar.asgi:application \ --host 0.0.0.0 \ --port 8000 \ --workers 4 \ --log-level debug
Uvicorn对信号处理的兼容性更好,在容器环境中能更稳定响应SIGINT。
内容的提问来源于stack exchange,提问作者Robert Kajic
相关产品推荐
相关产品推荐

