ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Flask生产部署指南:Gunicorn+Supervisor+Nginx+Celery实战

Flask生产部署指南:Gunicorn+Supervisor+Nginx+Celery实战

在开发环境里跑 Flask 项目,python app.py一行命令世界就安静了。但等你真正把项目放到 Linux 服务器上,问题会一个个冒出来:窗口一关服务是不是就跑了?并发一高会不会崩?定时任务谁来触发?进程挂了谁来拉起来?我去年在云服务器上部署一个 Flask 后台项目时,用的就是 Gunicorn + Supervisor + Nginx 这套经典组合,再加一个 Celery 处理定时任务,把一台配置不算好的小机器收拾得服服帖帖。这篇文章就是从零到上线的完整记录,每一段配置背后我会把“为什么这么做”也讲清楚,希望帮你少走几个弯路。

1. 为什么是这套组合:每个组件到底解决什么问题

1.1 Flask 自带服务器,真的只能“开发用”

刚开始学 Flask 的时候,大家应该都敲过flask run或者python app.py。Werkzeug 自带的开发服务器确实方便,改代码还能自动重载,但它有几个非常致命的问题:性能不行,并发高一点请求就直接排队;安全性不行,没有经过严格的生产环境考验;最要命的是它没有进程守护能力,终端窗口一关,整个服务就跟着走了。

所以生产环境里必须引入真正能扛压力的 WSGI 服务器。WSGI 是 Python Web 应用和服务器之间的标准接口,你的 Flask 应用是一个实现了 WSGI 接口的可调用对象,服务器负责接收 HTTP 请求,把它转成 environ 字典,调用你的应用对象,再把返回值转成 HTTP 响应。Gunicorn 就是这样一个成熟稳定的 WSGI 服务器。

1.2 四个组件各司其职

这套组合之所以经典,是因为每个组件都只干自己最擅长的事,不越权,不重复造轮子。我用一个粗糙的比喻来帮你建立直觉:

组件扮演的角色生活化类比
Gunicorn运行 Flask 应用,处理 WSGI 请求后厨里专门炒菜的厨师
Supervisor守护 Gunicorn / Celery 进程,挂了自动拉起来值班经理,盯着人不许撂挑子
Nginx对外部的唯一入口,做反向代理、静态文件服务、负载均衡大堂迎宾,所有客人先见它,它再带你去后厨
Celery处理定时任务和异步任务后厨里设定好时间自动运转的烤箱,到点就干活

Gunicorn 直接对你的 Flask 应用负责,但它自己也可能崩溃,所以要用 Supervisor 在系统层面监督它。Nginx 负责暴露在公网,把请求转给内网的 Gunicorn,同时帮你把静态文件、上传限制、HTTPS 这些事一并办了。Celery 则负责那些“不该让用户等太久”的任务,比如发邮件、生成报表、定时清理临时文件。

1.3 有别的选择吗?对比着看就明白了

这套组合不是唯一解,但对你我这种普通项目来说,是最省心的解。

  • Gunicorn 和 uWSGI:uWSGI 性能更强、功能更全,但配置复杂度也高一个量级,编译安装还容易踩 C 扩展的坑。Gunicorn 是纯 Python 实现,pip 装完就能用,配置逻辑直白,对中小项目绝对够用。
  • Supervisor 和 systemd:现在的 Linux 发行版都自带 systemd,用它也能做进程守护,而且更“原生”。但 systemd 的 unit 文件语法对很多 Python 开发者来说不如 Supervisor 的 INI 配置好懂。另外 Supervisor 可以同时用一行命令方便地查看和管理多个项目进程,我至今还是习惯用它。
  • Celery 和 APScheduler:如果你只是要一个“每天凌晨跑一次”的定时任务,APScheduler 确实更轻。但项目一旦需要异步任务、分布式、任务重试、结果存储,Celery 的生态优势就出来了。既然标题都写了 Celery,我们就不回头了。

2. 动手前的项目结构与环境准备

2.1 一个可部署的 Flask 项目长什么样

部署不只是写配置,项目结构先要清爽。我自己习惯的组织方式大概是这样的(以演示项目为例,项目名叫flask-demo):

/home/deploy/flask-demo/ ├── app/ │ ├── __init__.py # 应用工厂,创建 Flask app │ ├── views/ │ │ └── main.py # 业务路由 │ └── tasks/ │ └── periodic.py # Celery 定时任务 ├── config.py # 配置文件 ├── requirements.txt # 依赖清单 ├── wsgi.py # 入口:from app import create_app ├── gunicorn.conf.py # Gunicorn 配置 ├── run_celery.sh # 可选,封装 Celery 启动命令 └── static/ # 静态资源

注意wsgi.py必须是“模块级”的,Gunicorn 要能通过from wsgi import application导入应用对象。我最开始把创建 app 的代码塞在app/__init__.py里,Gunicorn 参数传成app:app也行,但一旦要用应用工厂,就必须有一个独立的 wsgi 入口,否则每次都会踩“模块无法导入”的坑。

2.2 虚拟环境:部署的第一道安全网

千万不要把依赖装到系统 Python 里,必须为项目建一个独立虚拟环境。我用的是venv:

cd /home/deploy/flask-demo python3 -m venv venv source venv/bin/activate pip install -r requirements.txt

requirements.txt里至少要有这些核心依赖:

Flask==3.0.3 gunicorn==22.0.0 celery==5.4.0 redis==5.0.4 supervisor==4.2.5

Supervisor 其实全局装更好,因为它是系统级工具,和项目虚拟环境无关。但如果你图省事,也可以装在项目虚拟环境里,后面启动时用绝对路径指向它就行。我一般直接pip install supervisor在系统里,这样supervisord命令在任何路径下都能用。

2.3 准备一个能跑的 Flask 示例

下面是最小可运行版本。wsgi.py内容:

# wsgi.py from app import create_app application = create_app() if __name__ == "__main__": application.run()

app/__init__.py里写一个简单的应用工厂:

# app/__init__.py from flask import Flask def create_app(): app = Flask(__name__) app.config.from_pyfile("../config.py") @app.route("/") def index(): return "Hello, Flask!" return app

config.py里留一个 Redis 配置的位置,后面 Celery 要用:

# config.py import os SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret") CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://127.0.0.1:6379/0") CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://127.0.0.1:6379/1")

到这里手动跑python wsgi.py能通,部署的第一步就准备好了。

3. Gunicorn:WSGI 服务器的选型与配置细节

3.1 为什么不用 uWSGI 或单纯靠 Nginx

有人可能会问,Nginx 本身能不能直接跑 Python?不行,Nginx 对 WSGI 协议支持有限,通常只做反向代理和静态资源。真正跟你 Flask 代码打交道的,必须是一个 WSGI 服务器。Gunicorn 的核心能力就是启动多个 worker 进程,每个 worker 独立处理请求,它们共享同一个监听端口,由操作系统做负载分发。

uWSGI 我在几个项目里也用过,性能非常猛,但它配置项多到让人头大,而且经常要编译安装依赖一堆 C 语言库。对于并发量在几百到几千这个量级的 Flask 项目,Gunicorn 的收益够高,心智负担低很多。

3.2 先跑通命令行,再谈配置

先手动验证 Gunicorn 能不能把项目跑起来:

cd /home/deploy/flask-demo source venv/bin/activate gunicorn --workers 4 --bind 127.0.0.1:8000 wsgi:application

这里wsgi:application的意思就是“从 wsgi.py 模块里导入名为 application 的对象”。如果你启动时报ModuleNotFoundError,十有八九是当前路径不对或虚拟环境没激活。Gunicorn 自己没有“自动定位项目目录”能力,必须用cd先切到项目根目录,或者用后面的 Supervisor 配置里的directory把工作目录固定住。

跑通之后浏览器访问http://服务器IP:8000,能看到文字说明已经成功。注意这个 bind 地址我用的是127.0.0.1,不是0.0.0.0,因为我的架构里 Nginx 和 Gunicorn 在同一台机器上,Nginx 通过内网访问它,没必要把端口直接暴露到公网。如果 Nginx 在另一台机器,bind 地址就要改成内网 IP。

3.3 worker 类型:sync、gthread 还是 gevent

worker 类型是 Gunicorn 最重要的一个选择。默认是 sync——一个 worker 同时只能处理一个请求,如果代码里有 IO 等待(比如查数据库、调第三方 API、读 redis),其他请求就得排队。这对大多数 API 服务来说并不是最优解。

我一般按这个原则选:

  • 项目以 IO 密集型为主,比如大量查库、调接口,用gevent或gthread最有优势;
  • 项目是简单计算任务,或者就是返回几个页面,sync 也行;
  • 用了 gevent 一定要确保代码库里没有会阻塞事件循环的同步库,否则反而更慢。

gevent需要额外安装:

pip install gevent

然后在 Gunicorn 配置里指定:

worker_class = "gevent" worker_connections = 1000

worker_connections表示每个 worker 能同时处理的连接数。我的经验值是 1000,如果服务器内存不大,调成 500 更稳妥。

3.4 把 Gunicorn 配置写进文件

命令行参数虽然方便,但你不希望每次重启都敲一长串参数。我习惯写一个gunicorn.conf.py:

# gunicorn.conf.py import multiprocessing bind = "127.0.0.1:8000" # 官方的通用公式:CPU 核数 × 2 + 1 workers = multiprocessing.cpu_count() * 2 + 1 worker_class = "gevent" worker_connections = 1000 timeout = 30 graceful_timeout = 30 keepalive = 5 # 防止单个 worker 内存无限涨 max_requests = 1000 max_requests_jitter = 100 daemon = False

几个参数说明一下:

  • workers:不是越多越好,每个 worker 都是一个独立进程,都会占内存。机器是 2 核的话,2*2+1=5个 worker 已经很多了,内存小的机器建议固定写 2 或 3。
  • timeout:如果某个接口跑太久没返回,Gunicorn 会把它杀掉。注意这不适用于 gevent 的长时间挂起连接,但能兜底一些死锁请求。
  • max_requests和max_requests_jitter:worker 处理到 1000 个请求左右就会自行重启,有效缓解 Python 进程内存缓慢增长的问题。jitter 是随机抖动,避免所有 worker 同一时刻一起重启造成服务抖动。

4. Supervisor:让 Gunicorn 和 Celery 变成“杀不死”的后台服务

4.1 先想清楚:到底让 Supervisor 管哪些进程

部署系统里,Supervisor 要管的进程至少有三个:Gunicorn 的 worker(通常 master 进程会派生多个 worker)、Celery 的worker服务、Celery 的beat调度器。注意 Celery 的 worker 和 beat 是两个不同进程,很多人只启动了 worker,忘了 beat,导致定时任务压根不触发,这是最经典的失误。

我再强调一次:celery worker负责执行任务,celery beat负责按计划发送任务指令。生产环境里两者需要同时运行。

4.2 安装和基础配置

先安装 Supervisor(我用的是 Debian/Ubuntu 系命令):

sudo apt update sudo apt install -y supervisor

安装完后配置文件在/etc/supervisor/,其中/etc/supervisor/conf.d/是放自定义进程配置的地方。主配置文件/etc/supervisor/supervisord.conf最后一行通常会有:

[include] files = /etc/supervisor/conf.d/*.conf

没有的话请手动补上。

然后在/etc/supervisor/conf.d/下新建项目配置文件,我命名为flask-demo.conf:

[program:flask-demo] command=/home/deploy/flask-demo/venv/bin/gunicorn -c /home/deploy/flask-demo/gunicorn.conf.py wsgi:application directory=/home/deploy/flask-demo user=deploy autostart=true autorestart=true startsecs=5 stderr_logfile=/var/log/supervisor/flask-demo.err.log stdout_logfile=/var/log/supervisor/flask-demo.out.log loglevel=info

字段含义:

  • command:必填,必须用绝对路径,最好直接指向虚拟环境里的 gunicorn 可执行文件,不要图省事只写gunicorn。
  • directory:工作目录,Gunicorn 会在该目录下启动,避免找不到wsgi.py。
  • user:以哪个系统用户运行。建议用一个普通账号,比如deploy,别用 root,否则项目文件权限一旦混乱,后面静态文件、日志都会出问题。
  • autostart:Supervisor 启动时就拉起这个进程。
  • autorestart:进程意外挂了自动重启。
  • startsecs:进程启动后要稳定存活多少秒才算“启动成功”,否则会判定启动失败并触发重启。

Celery 的配置也写在同一个文件里,再添加两个 program 段:

[program:flask-demo-celery] command=/home/deploy/flask-demo/venv/bin/celery -A app.tasks.periodic worker --loglevel=info directory=/home/deploy/flask-demo user=deploy autostart=true autorestart=true startsecs=10 stderr_logfile=/var/log/supervisor/flask-demo-celery.err.log stdout_logfile=/var/log/supervisor/flask-demo-celery.out.log [program:flask-demo-beat] command=/home/deploy/flask-demo/venv/bin/celery -A app.tasks.periodic beat --loglevel=info directory=/home/deploy/flask-demo user=deploy autostart=true autorestart=true startsecs=10 stderr_logfile=/var/log/supervisor/flask-demo-beat.err.log stdout_logfile=/var/log/supervisor/flask-demo-beat.out.log

注意-A参数指向的是承载 Celery 实例的模块,我们后面会写这个模块,这里先留个标记。

4.3 用 supervisorctl 管理进程

配置写好之后执行:

sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl status

reread让 Supervisord 重新扫描配置文件目录,update会把新增的 program 启动起来。如果没有报错,status 输出里应该能看到三行 RUNNING。日常操作命令:

sudo supervisorctl restart flask-demo sudo supervisorctl restart flask-demo-celery sudo supervisorctl restart flask-demo-beat sudo supervisorctl tail -f flask-demo

tail -f可以直接看进程日志,排查问题比journalctl直观太多。这是我一直喜欢 Supervisor 的原因之一。

4.4 设置日志切割,防止磁盘被撑爆

默认日志会一直无限写下去,时间长了磁盘肯定受不了。Supervisor 主配置里有一段:

[supervisord] logfile=/var/log/supervisor/supervisord.log logfile_maxbytes=50MB logfile_backups=10

每个 program 的日志也建议配一下:

stdout_logfile_maxbytes=50MB stdout_logfile_backups=10 stderr_logfile_maxbytes=50MB stderr_logfile_backups=10

加了这个之后,单个日志文件超过 50MB 会自动轮转,保留最近 10 份。我因为忘了配这个吃过一次亏:一个 Celery worker 日志半年写了 30GB,直接塞满磁盘导致服务全部停摆。

5. Nginx:反向代理与静态资源分流

5.1 为什么前面还要再架一层 Nginx

Gunicorn 已经能提供服务了,Supervisor 已经能保活了,为什么还要 Nginx?因为 Gunicorn 处理 HTTP 请求没问题,但让它处理静态文件、大文件上传、HTTPS 证书这些“边缘事务”效率并不高。Nginx 是 C 语言写的,对静态文件、连接管理、并发连接的处理能力都远胜 Python 层。

另外,Nginx 作为唯一暴露在公网的入口,可以把这个端口后面到底是哪台机器、哪个端口都藏起来。以后哪怕 Gunicorn 从 8000 端口换到 9000,外部客户端也不需要感知。

5.2 写一份能用的 server 与 location 配置

在/etc/nginx/sites-available/下新建一个文件,比如flask-demo:

server { listen 80; server_name 你的域名或IP; # 请求体大小限制,默认是 1MB,上传文件需要调大 client_max_body_size 20m; location / { proxy_pass http://127.0.0.1:8000; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /static/ { alias /home/deploy/flask-demo/static/; expires 7d; } }

几个配置点要注意:

  • proxy_pass必须和 Gunicorn 的 bind 地址一致,这里都是127.0.0.1:8000。
  • proxy_set_header Host $host一定要有,否则 Flask 里request.host得到的会是127.0.0.1:8000,你做域名相关逻辑时会踩坑。
  • X-Forwarded-Proto也重要,如果你的 Flask 应用用request.is_secure判断是不是 HTTPS 请求,缺了这个头就会永远得到 False。
  • /static/用alias而不是root,区别在于alias会把 URL 前缀/static/去掉再映射到目录,root则会拼接路径。

创建完软链接到sites-enabled:

sudo ln -s /etc/nginx/sites-available/flask-demo /etc/nginx/sites-enabled/flask-demo

然后测试配置并重载:

sudo nginx -t sudo systemctl reload nginx

nginx -t是必做的操作,语法错了它会直接告诉你哪一行有问题,避免了改坏配置导致整个站点挂掉的惨剧。我身边有人改完配置没做语法检查就直接 reload,结果 Nginx 起不来,全部站点瞬间 502,这种事故一次就够记一辈子。

5.3 静态文件的分流策略

为什么要让 Nginx 直接服务静态文件?因为 Flask 处理静态文件时,每次请求都要经过 Python 层,涉及路由解析、上下文创建、响应封装,开销不小。Nginx 直接读文件发回去,速度能快一个量级。

如果项目里用到了 Flask 的蓝图和url_for('static', filename='...'),配置location /static/时注意静态文件实际路径别写错。可以用nginx -T查看最终生效的配置来核对。

6. Celery 定时任务:和 Flask 同住一个屋檐下的注意事项

6.1 核心问题:Celery 任务拿不到 Flask 应用上下文

Celery 的任务函数通常定义在项目模块里,但 Celery 进程和 Flask 应用进程是相互独立的——Celery worker 启动时不会运行你的 Flask 应用,所以任务函数里直接from flask import current_app大概率得到的是RuntimeError: Working outside of application context。

这是很多新手第一次部署 Celery 时必经的拦路虎。解决方案也不是很复杂,就是把 Celery 实例的创建方式改造成和 Flask 应用上下文绑定。

6.2 延迟初始化方案

我采用一个make_celery函数来解决:

# app/tasks/__init__.py from celery import Celery from flask import Flask def make_celery(app: Flask) -> Celery: celery = Celery( app.import_name, broker=app.config.get("CELERY_BROKER_URL", "redis://127.0.0.1:6379/0"), backend=app.config.get("CELERY_RESULT_BACKEND", "redis://127.0.0.1:6379/1"), ) celery.conf.update( timezone="Asia/Shanghai", enable_utc=True, task_serializer="json", result_serializer="json", accept_content=["json"], ) class ContextTask(celery.Task): def __call__(self, *args, **kwargs): with app.app_context(): return self.run(*args, **kwargs) celery.Task = ContextTask return celery

关键是celery.Task = ContextTask:每个任务在执行时都会自动包一层app.app_context(),这样任务函数内部就能使用current_app、db这些 Flask 上下文相关的对象了。

在应用工厂里创建全局的celery实例:

# app/__init__.py from flask import Flask from app.tasks import make_celery celery = None def create_app(): app = Flask(__name__) app.config.from_pyfile("../config.py") global celery celery = make_celery(app) return app

这里还涉及一个重要问题:如果整个项目有多个 Flask 进程,每个进程都创建自己的 Celery 实例是没问题的,因为 broker 和 backend 都在 Redis,多个进程共享同一个队列,互不冲突。

6.3 定时任务的几个典型配置

Celery 的定时任务由beat进程负责调度,配置文件里设置beat_schedule。我现在经常挂在任务模块里的配置长这样:

# app/tasks/periodic.py from celery.schedules import crontab from app import celery @celery.task def clean_expired_file(): # 实际业务逻辑:清理过期临时文件 return "cleaned" @celery.on_after_configure.connect def setup_periodic_tasks(sender, **kwargs): sender.add_periodic_task( crontab(hour=2, minute=0), clean_expired_file.s(), name="clean-expired-file-daily", )

crontab(hour=2, minute=0)表示每天凌晨 2 点执行。Celery 的 crontab 支持day_of_month、month_of_year等字段,基本能覆盖你见过的所有 cron 表达式。如果只是要“每 10 分钟跑一次”,就写成:

sender.add_periodic_task( 600.0, clean_expired_file.s(), name="clean-expired-file-10min", )

注意任务函数里一旦涉及 Django orm、Flask-SQLAlchemy 这类需要应用上下文的东西,就必须走我们刚才的ContextTask,否则会直接报错。

6.4 定时任务部署后最容易犯的错

  • 忘记启动 beat:只启动了celery worker,定时任务永远不触发。Supervisor 的配置里必须单独管理 beat 进程。
  • EUR 时区和本地时区搞混:如果enable_utc=True但timezone没设置,定时任务会按 UTC 时间执行,凌晨 2 点实际上是北京时间上午 10 点。
  • 任务里传 ORM 对象:Celery 序列化的是任务参数,如果你把一个 SQLAlchemy 模型实例传进去,反序列化时会因为数据库 session 已经关闭而报各种诡异错误。正确做法是传对象 ID,在任务内部重新查询。
  • 任务日志不落盘:Celery 默认日志输出到 stdout,如果 Supervisor 没重定向,日志会丢失。在 command 里加--logfile=/var/log/supervisor/celery.log会更稳。

7. 部署完成后的验证流程与几个真实的坑

7.1 上线前的九步验证法

配置全部写完后,别急着说大功告成。我每次部署新环境都会走一遍这个验证清单:

# 1. 检查 Supervisor 是否正常管理三个进程 sudo supervisorctl status # 2. 看日志有没有错误 sudo supervisorctl tail -f flask-demo sudo supervisorctl tail -f flask-demo-celery sudo supervisorctl tail -f flask-demo-beat # 3. 直接用 curl 打 Gunicorn 端口 curl -I http://127.0.0.1:8000/ # 4. 再通过 Nginx 入口打一遍 curl -I http://127.0.0.1/ # 5. 检查 Nginx 错误日志 sudo tail -f /var/log/nginx/error.log # 6. 检查 Celery 任务有没有注册成功 celery -A app.tasks.periodic inspect registered # 7. 手动触发一次任务,看 worker 日志 celery -A app.tasks.periodic call app.tasks.periodic.clean_expired_file --args='[]' # 8. 等一会儿看 beat 日志,确认定时任务真的触发了 sudo tail -f /var/log/supervisor/flask-demo-beat.out.log # 9. 用外部机器访问公网 IP,确认 Nginx 入口对外开放正常

第 7 步手动触发任务是我最推荐的调试手段。很多定时任务问题你以为“写对了”,其实任务函数里有一行语法错误或者导入错误,手动触发能第一时间暴露出来,比干等 beat 到点再报错高效太多。

7.2 权限问题:deploy 用户和 Nginx 用户的恩怨

部署完最常见的表象是:页面访问 502、静态文件 403。绝大多数时候是权限问题。Gunicorn 进程是deploy用户跑的,静态文件目录如果权限是root读写,Nginx worker 读不了,就会 403;如果 Gunicorn 的工作目录里的临时文件、日志路径没有写权限,进程直接起不来。

我的习惯是:

sudo chown -R deploy:deploy /home/deploy/flask-demo sudo chmod -R 755 /home/deploy/flask-demo

如果用了 Nginx 直接服务静态文件,还要保证deploy用户至少对静态文件目录有读权限,Nginx worker 用户也能沿路径访问到。遇到 403 用namei -om /home/deploy/flask-demo/static/xxx.png查看路径上每一级目录的权限,是排查权限问题最快的命令。

7.3 内存管理与 max_requests 的真实收益

我的项目启动时每个 Gunicorn worker 占用内存大概 150MB,5 个 worker 就是 750MB,加上 Celery worker、beat、Redis、Nginx,2G RAM 的小机器已经开始紧张。时间一长,Python 进程内部会有碎内存堆积,典型表现是内存占用只涨不降。

max_requests的意义就在这里——让每个 worker 处理固定数量的请求后自行退出,由 Gunicorn 重新拉起一个全新的 worker。这相当于给进程装了个自动重启机制,比任何内存监控脚本都简单有效。把max_requests绑上去之后,我就再没做过定时重启 Gunicorn 的操作。

7.4 时区与日志:两个看似不起眼却总出事的点

时区问题在 Celery 定时任务里特别隐蔽。你写crontab(hour=2),自以为自己每天凌晨 2 点跑,结果全公司都在上午 10 点看到定时任务日志才出现。检查方法很简单,在 Celery worker 日志里看任务触发时打出的时间戳,以及celery.timezone的实际值。

日志方面,我强烈建议把 Supervisor 和 Celery 的日志级别都调到 INFO,这样能保留完整的任务执行轨迹。如果你的任务执行失败,日志里会有Task xuixxx raised的 stacktrace,这是排查问题最关键的线索。我见过有人连 loglevel 都省了,任务挂了只能一脸懵。

部署这套组合,我第一次走完全程用了一天半,踩了差不多七八个坑。后来再部署类似的 Flask 项目,半个小时就能把环境拉起。这套组合最舒服的地方在于:每个部分都“信得过”,Gunicorn 负责跑,Supervisor 负责管,Nginx 负责扛流量入口,Celery 负责定时干活,结构清晰,也方便以后横向扩展。最后再分享一个小习惯:每次改完任何配置,都先在测试环境完整走一遍验证流程,别直接上生产操作。服务器部署这件事,稳定永远比炫技重要。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进