You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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项目中,同样出现该异常。

原因分析

  1. Docker信号转发机制问题:未使用-it参数时,Docker不会将终端SIGINT信号转发给容器内的PID 1进程。bash作为PID 1时,默认不会将信号转发给子进程,即使使用exec替换进程,无终端环境下信号传递仍可能异常。
  2. Gunicorn与Uvicorn Worker的信号兼容性:Uvicorn Worker对SIGINT的处理依赖终端环境支持,非交互式终端模式下,Worker无法正确接收并处理信号,导致Gunicorn超时后发送SIGKILL强制终止。
  3. 手动信号转发的局限性:第三种方法仅转发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 17:55:04