1. 为什么需要Gunicorn启停脚本?
在Python Web开发中,Gunicorn作为WSGI HTTP服务器几乎是生产环境的标准选择。但每次手动输入完整的启动命令既繁琐又容易出错,特别是在需要频繁重启的开发阶段。一个典型的Gunicorn启动命令可能长这样:
gunicorn -w 4 -k gevent -b 0.0.0.0:8000 --access-logfile - --error-logfile - --pid /tmp/gunicorn.pid myapp:app想象一下每天要输入这种命令十几次的场景。更糟的是,当需要停止服务时,你还得先找到PID文件再kill进程。这就是为什么我们需要一个智能化的启停脚本——它不仅能简化操作,还能避免人为失误导致的服务异常。
2. 基础版启停脚本实现
2.1 脚本框架设计
我们先从最基础的版本开始,创建一个名为gunicorn_manager.sh的文件:
#!/bin/bash APP_NAME="myapp" APP_MODULE="${APP_NAME}:app" PID_FILE="/tmp/gunicorn.pid" LOG_FILE="/var/log/gunicorn.log" GUNICORN_CMD="gunicorn -w 4 -k gevent -b 0.0.0.0:8000 --access-logfile ${LOG_FILE} --error-logfile ${LOG_FILE} --pid ${PID_FILE} ${APP_MODULE}" case "$1" in start) echo "Starting Gunicorn..." $GUNICORN_CMD ;; stop) echo "Stopping Gunicorn..." kill -TERM `cat ${PID_FILE}` ;; *) echo "Usage: $0 {start|stop}" exit 1 esac这个脚本已经实现了基本功能:
- 通过
start参数启动服务 - 通过
stop参数停止服务 - 将日志统一输出到指定文件
- 使用PID文件管理进程
2.2 使用方式与权限设置
给脚本添加执行权限:
chmod +x gunicorn_manager.sh启动服务:
./gunicorn_manager.sh start停止服务:
./gunicorn_manager.sh stop注意:首次运行前请确保已创建日志文件目录并设置适当权限:
sudo mkdir -p /var/log/ sudo touch /var/log/gunicorn.log sudo chown `whoami` /var/log/gunicorn.log
3. 增强版脚本功能实现
基础版虽然能用,但缺乏健壮性。我们来逐步添加生产环境需要的功能。
3.1 状态检查与进程守护
添加status和restart命令,并实现进程存在性检查:
#!/bin/bash # ...(保留之前的变量定义) is_running() { [ -f "$PID_FILE" ] && kill -0 `cat "$PID_FILE"` >/dev/null 2>&1 } case "$1" in start) if is_running; then echo "Gunicorn is already running" exit 1 fi echo "Starting Gunicorn..." $GUNICORN_CMD ;; stop) if ! is_running; then echo "Gunicorn is not running" exit 1 fi echo "Stopping Gunicorn..." kill -TERM `cat ${PID_FILE}` ;; restart) $0 stop sleep 2 $0 start ;; status) if is_running; then echo "Gunicorn is running (PID: `cat ${PID_FILE}`)" else echo "Gunicorn is not running" fi ;; *) echo "Usage: $0 {start|stop|restart|status}" exit 1 esac新增功能:
status命令检查服务状态restart命令实现重启- 防止重复启动或停止不存在的服务
- 使用
kill -0检查进程是否存在
3.2 日志轮转与后台运行
生产环境还需要以下改进:
#!/bin/bash # ...(保留之前的定义) # 新增日志轮转配置 LOG_ROTATE_SIZE="10M" # 日志文件达到10MB时轮转 LOG_BACKUP_COUNT=5 # 保留5个历史日志文件 rotate_logs() { if [ -f "$LOG_FILE" ] && [ `stat -c %s "$LOG_FILE"` -gt $(( ${LOG_ROTATE_SIZE%M} * 1024 * 1024 )) ]; then for i in $(seq $((LOG_BACKUP_COUNT-1)) -1 1); do [ -f "${LOG_FILE}.$i" ] && mv "${LOG_FILE}.$i" "${LOG_FILE}.$((i+1))" done mv "$LOG_FILE" "${LOG_FILE}.1" touch "$LOG_FILE" fi } case "$1" in start) if is_running; then echo "Gunicorn is already running" exit 1 fi rotate_logs echo "Starting Gunicorn..." nohup $GUNICORN_CMD > /dev/null 2>&1 & echo "Gunicorn started (PID: $!)" ;; # ...(其他命令保持不变) esac关键改进:
- 使用
nohup实现真正的后台运行 - 自动日志轮转防止日志文件过大
- 启动后显示进程PID
4. 生产环境最佳实践
4.1 多环境配置支持
实际项目中,我们通常需要区分开发、测试和生产环境。改进脚本支持环境变量:
#!/bin/bash # 默认环境配置 ENV=${ENV:-"production"} CONFIG_DIR="/etc/gunicorn" CONFIG_FILE="${CONFIG_DIR}/${ENV}.conf" # 加载环境特定配置 if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE" else echo "Warning: Config file not found for environment $ENV, using defaults" fi # 默认值 APP_NAME=${APP_NAME:-"myapp"} WORKERS=${WORKERS:-4} BIND_ADDR=${BIND_ADDR:-"0.0.0.0:8000"} # ...其他变量同理 GUNICORN_CMD="gunicorn -w ${WORKERS} -k ${WORKER_CLASS} -b ${BIND_ADDR} --access-logfile ${ACCESS_LOG} --error-logfile ${ERROR_LOG} --pid ${PID_FILE} ${APP_MODULE}"然后创建不同环境的配置文件,如/etc/gunicorn/development.conf:
# 开发环境配置 WORKERS=2 ACCESS_LOG="/var/log/gunicorn_dev.access.log" ERROR_LOG="/var/log/gunicorn_dev.error.log"使用方式:
ENV=development ./gunicorn_manager.sh start4.2 系统服务集成
为了让脚本能在系统启动时自动运行,我们可以创建systemd服务文件/etc/systemd/system/gunicorn.service:
[Unit] Description=Gunicorn application server After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/path/to/your/app Environment="ENV=production" ExecStart=/path/to/gunicorn_manager.sh start ExecStop=/path/to/gunicorn_manager.sh stop ExecReload=/path/to/gunicorn_manager.sh restart Restart=on-failure [Install] WantedBy=multi-user.target然后启用服务:
sudo systemctl daemon-reload sudo systemctl enable gunicorn sudo systemctl start gunicorn5. 高级技巧与故障排查
5.1 优雅停机与超时控制
默认的kill -TERM可能无法在所有情况下正常关闭工作进程。改进停止逻辑:
stop() { if ! is_running; then echo "Gunicorn is not running" return 1 fi echo "Stopping Gunicorn gracefully..." kill -TERM `cat ${PID_FILE}` # 等待最多30秒 TIMEOUT=30 while is_running && [ $TIMEOUT -gt 0 ]; do sleep 1 TIMEOUT=$((TIMEOUT-1)) done if is_running; then echo "Force stopping Gunicorn..." kill -9 `cat ${PID_FILE}` rm -f "$PID_FILE" else echo "Gunicorn stopped gracefully" fi }5.2 常见问题排查
问题1:PID文件存在但进程已死解决方案:在检查进程状态时同时验证PID文件内容
is_running() { if [ ! -f "$PID_FILE" ]; then return 1 fi local pid=$(cat "$PID_FILE") if ! kill -0 $pid >/dev/null 2>&1; then # 清理无效PID文件 rm -f "$PID_FILE" return 1 fi return 0 }问题2:端口已被占用解决方案:启动前检查端口
port_in_use() { local port=$(echo $BIND_ADDR | awk -F: '{print $2}') netstat -tuln | grep -q ":$port " } case "$1" in start) if port_in_use; then echo "Error: Port ${BIND_ADDR##*:} is already in use" exit 1 fi # ...其余启动逻辑 ;; esac问题3:依赖缺失解决方案:启动前检查Python环境和依赖
check_dependencies() { if ! command -v gunicorn >/dev/null 2>&1; then echo "Error: gunicorn is not installed" exit 1 fi if ! python -c "import ${APP_NAME}" >/dev/null 2>&1; then echo "Error: Python application ${APP_NAME} cannot be imported" exit 1 fi } case "$1" in start|restart) check_dependencies # ...其余逻辑 ;; esac6. 完整脚本示例
结合所有改进点,最终的脚本可能如下:
#!/bin/bash # 配置部分 ENV=${ENV:-"production"} CONFIG_DIR="/etc/gunicorn" CONFIG_FILE="${CONFIG_DIR}/${ENV}.conf" # 加载环境配置 [ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE" # 默认值 APP_NAME=${APP_NAME:-"myapp"} APP_MODULE=${APP_MODULE:-"${APP_NAME}:app"} WORKERS=${WORKERS:-4} WORKER_CLASS=${WORKER_CLASS:-"gevent"} BIND_ADDR=${BIND_ADDR:-"0.0.0.0:8000"} PID_FILE=${PID_FILE:-"/tmp/gunicorn.pid"} ACCESS_LOG=${ACCESS_LOG:-"/var/log/gunicorn.access.log"} ERROR_LOG=${ERROR_LOG:-"/var/log/gunicorn.error.log"} LOG_ROTATE_SIZE=${LOG_ROTATE_SIZE:-"10M"} LOG_BACKUP_COUNT=${LOG_BACKUP_COUNT:-5} GUNICORN_CMD="gunicorn -w ${WORKERS} -k ${WORKER_CLASS} -b ${BIND_ADDR} --access-logfile ${ACCESS_LOG} --error-logfile ${ERROR_LOG} --pid ${PID_FILE} ${APP_MODULE}" # 函数定义 is_running() { [ -f "$PID_FILE" ] && kill -0 `cat "$PID_FILE"` >/dev/null 2>&1 } port_in_use() { local port=$(echo $BIND_ADDR | awk -F: '{print $2}') netstat -tuln | grep -q ":$port " } check_dependencies() { if ! command -v gunicorn >/dev/null 2>&1; then echo "Error: gunicorn is not installed" exit 1 fi if ! python -c "import ${APP_NAME}" >/dev/null 2>&1; then echo "Error: Python application ${APP_NAME} cannot be imported" exit 1 fi } rotate_logs() { for logfile in "$ACCESS_LOG" "$ERROR_LOG"; do if [ -f "$logfile" ] && [ `stat -c %s "$logfile"` -gt $(( ${LOG_ROTATE_SIZE%M} * 1024 * 1024 )) ]; then for i in $(seq $((LOG_BACKUP_COUNT-1)) -1 1); do [ -f "${logfile}.$i" ] && mv "${logfile}.$i" "${logfile}.$((i+1))" done mv "$logfile" "${logfile}.1" touch "$logfile" fi done } stop() { if ! is_running; then echo "Gunicorn is not running" return 1 fi echo "Stopping Gunicorn gracefully..." kill -TERM `cat ${PID_FILE}` TIMEOUT=30 while is_running && [ $TIMEOUT -gt 0 ]; do sleep 1 TIMEOUT=$((TIMEOUT-1)) done if is_running; then echo "Force stopping Gunicorn..." kill -9 `cat ${PID_FILE}` rm -f "$PID_FILE" else echo "Gunicorn stopped gracefully" fi } # 主逻辑 case "$1" in start) if is_running; then echo "Gunicorn is already running (PID: `cat ${PID_FILE}`)" exit 1 fi if port_in_use; then echo "Error: Port ${BIND_ADDR##*:} is already in use" exit 1 fi check_dependencies rotate_logs echo "Starting Gunicorn for ${APP_NAME} in ${ENV} environment..." nohup $GUNICORN_CMD > /dev/null 2>&1 & echo "Gunicorn started (PID: $!)" ;; stop) stop ;; restart) $0 stop sleep 2 $0 start ;; status) if is_running; then echo "Gunicorn is running (PID: `cat ${PID_FILE}`)" else echo "Gunicorn is not running" fi ;; *) echo "Usage: $0 {start|stop|restart|status}" echo "Environment variables:" echo " ENV Runtime environment (default: production)" echo " APP_NAME Python application name (default: myapp)" echo " BIND_ADDR Binding address (default: 0.0.0.0:8000)" exit 1 esac exit 0这个脚本已经具备了生产环境所需的大部分功能:
- 多环境支持
- 依赖检查
- 端口冲突检测
- 日志轮转
- 优雅停机
- 完善的错误处理
- 详细的用法说明
7. 实际部署建议
7.1 目录结构规范
建议采用以下目录结构管理相关文件:
/opt/myapp/ ├── app/ # 应用代码 ├── logs/ # 日志目录 │ ├── gunicorn.access.log │ └── gunicorn.error.log ├── scripts/ # 管理脚本 │ └── gunicorn_manager.sh └── config/ # 配置文件 ├── development.conf └── production.conf7.2 权限管理
确保正确的文件权限:
sudo chown -R www-data:www-data /opt/myapp/logs sudo chmod -R 755 /opt/myapp/scripts7.3 监控与告警
集成到现有监控系统:
# 简单的监控检查脚本 #!/bin/bash if ! /opt/myapp/scripts/gunicorn_manager.sh status | grep -q "running"; then echo "Gunicorn is down!" | mail -s "Gunicorn Alert" admin@example.com /opt/myapp/scripts/gunicorn_manager.sh restart fi添加到cron每5分钟运行一次:
*/5 * * * * /opt/myapp/scripts/monitor_gunicorn.sh8. 性能调优技巧
8.1 Worker数量计算
经验公式:
# 建议worker数量 = (2 * CPU核心数) + 1 CPU_CORES=$(nproc) RECOMMENDED_WORKERS=$((2 * CPU_CORES + 1))8.2 Worker类选择
根据应用特点选择:
- 同步Worker:适合CPU密集型
- gevent:适合IO密集型
- tornado:需要WebSocket支持时
8.3 其他重要参数
# 在GUNICORN_CMD中添加这些参数 --max-requests 1000 # 每个worker处理1000请求后重启 --max-requests-jitter 100 # 添加随机性防止所有worker同时重启 --timeout 30 # 请求超时时间(秒) --graceful-timeout 30 # 优雅停机超时9. 安全加固措施
9.1 限制绑定IP
生产环境不要绑定到0.0.0.0:
BIND_ADDR="127.0.0.1:8000" # 只允许本地访问9.2 使用Unix Socket
更高性能和安全:
SOCKET_FILE="/tmp/gunicorn.sock" GUNICORN_CMD="gunicorn -w ${WORKERS} -k ${WORKER_CLASS} --bind unix:${SOCKET_FILE} ..." # 记得设置socket权限 chmod 660 "$SOCKET_FILE" chown www-data:www-data "$SOCKET_FILE"9.3 日志敏感信息过滤
在应用代码中添加过滤器,防止记录敏感信息如密码、token等。
10. 扩展思考
10.1 多应用管理
如果需要管理多个Gunicorn应用,可以扩展脚本:
#!/bin/bash APPS=("app1" "app2" "app3") BASE_DIR="/opt" manage_app() { local app=$1 local action=$2 ( cd "${BASE_DIR}/${app}" ENV=production ./scripts/gunicorn_manager.sh "$action" ) } case "$1" in start-all) for app in "${APPS[@]}"; do manage_app "$app" start done ;; # 其他命令类似 esac10.2 与Supervisor集成
替代systemd的方案:
[program:myapp] command=/opt/myapp/scripts/gunicorn_manager.sh run directory=/opt/myapp user=www-data autostart=true autorestart=true stderr_logfile=/var/log/myapp.err.log stdout_logfile=/var/log/myapp.out.log10.3 容器化部署
对于Docker环境,调整脚本:
#!/bin/bash # 在容器中直接运行,不使用nohup exec gunicorn -w ${WORKERS} -k ${WORKER_CLASS} -b ${BIND_ADDR} --access-logfile - --error-logfile - ${APP_MODULE}然后在Dockerfile中:
COPY gunicorn_manager.sh /app/ CMD ["/app/gunicorn_manager.sh", "start"]