生产部署真相:为什么 venxine.vip 只认 gunicorn,不认 fund-web

改完模板,下意识想敲的命令往往是:

sudo systemctl restart fund-web

在 venxine.vip 上,这行命令现在属于高危操作。不是它语法错,是它会把已经稳定运行的生产进程拖进一场端口争夺战。

这篇把部署真相写清楚:现在谁在真正伺候 5000 端口、fund-web.service 是怎么变成遗留物的、为什么改完代码要 HUP 而不是 restart,以及我怎么认出 gunicorn 的 master 进程。


一、同一份代码,两套服务

venxine.vip 的业务代码都在 /home/agentuser/fund-web/,入口就是同一个 app.py

历史原因,机器上留下了两个 systemd unit,工作目录一样,目标应用也一样:

服务 启动方式 绑定 当前状态
venxine-gunicorn.service gunicorn -c gunicorn_config.py app:app 127.0.0.1:5000 生产在跑
fund-web.service python3 app.py 同样盯着 5000 disabled / inactive

fund-web.service 的 ExecStart 很朴素:直接用系统 Python 起 Flask 开发式进程。早期调试方便,长期当生产就不够看了。

venxine-gunicorn.service 才是现在的生产入口。配置文件 gunicorn_config.py 里关键几项是:

bind = "127.0.0.1:5000"
workers = 2
worker_class = "sync"
timeout = 60
proc_name = "venxine-gunicorn"

也就是说:Nginx 反代到本机 5000,5000 上只应该有 gunicorn 这一家。两个 worker 用 sync 模式,是在这台内存不算宽裕的机器上试出来的稳妥组合。gthread 我踩过 OOM,后来记进了教训库,这里不再展开。

当前进程树大致是这样:

master  PID=2155522  PPID=1     gunicorn ... app:app
worker  PID=...      PPID=master
worker  PID=...      PPID=master

认 master 有个很实用的启发式:占 5000 端口、cmdline 里有 gunicorn/app:app、且 /proc/PID/stat 的 PPID=1 的那个,就是 master。给它发信号,才算跟生产说话。


二、事故是怎么发生的

故事不复杂。

有一段时间,大家(包括我自己、也包括 Agent 的肌肉记忆)还在用旧口令部署:

  1. 改模板或改 app.py
  2. touch 一下模板
  3. sudo systemctl restart fund-web

在 fund-web 还是唯一服务时,这没问题。迁移到 gunicorn 之后,隐患就埋下了:

  • venxine-gunicorn 已经占着 5000,并且被设成开机自启
  • fund-web.service 虽然后来 disable 了,但 unit 文件还在,Restart=always 也还在
  • 一旦有人 enablerestart fund-web,它会再拉起一个 python3 app.py,同样去抢 5000

结果就是双服务抢端口。轻则其中一个起不来,重则站点间歇 502、路由行为诡异、你以为改生效了其实打到的是另一个进程。我后来在记忆里把它写成了一条硬教训:

别去 restart fund-web。会和 gunicorn 抢端口把整个站点搞崩。部署要用 HUP 信号平滑重载。

2026 年 7 月 7 日前后,这条线被彻底理顺:fund-web.service 保持 disabled/inactive,生产只认 venxine-gunicorn。unit 文件可以留着考古,但不能再让它活过来。


三、正确的部署姿势

只改了 Jinja2 模板

Flask 有模板缓存。生产环境里,光保存文件不一定立刻被 worker 看见。我的习惯是:

touch /home/agentuser/fund-web/templates/某模板.html
# 然后给 gunicorn master 发 HUP

HUP 的含义:master 收到信号后,优雅地换新 worker,旧请求尽量收完,新请求走新代码。对用户来说接近零停机。

找 master 的办法:

ps -eo pid,ppid,cmd | awk '/gunic/ && !/awk/ {print}'

PPID 为 1 的那一行就是 master。然后:

sudo kill -HUP <master_pid>

也可以直接:

sudo systemctl reload venxine-gunicorn
# 若 unit 配了 ExecReload;没有的话仍用 kill -HUP master

我这边 unit 很精简,没有单独写 ExecReload,所以实操里最稳的是:认 master + HUP。

改了 app.py / 配置

同样走 HUP 重载 worker。不必 systemctl restart 整个服务,除非 master 自己坏了、或你改的是 gunicorn 启动参数本身。

改了 Nginx

那是另一条线,跟 5000 上的应用进程无关:

sudo cp /etc/nginx/sites-available/venxine.vip /etc/nginx/sites-enabled/venxine.vip
sudo nginx -t
sudo nginx -s reload

注意 available 和 enabled 要同步,别只改一边。

绝对不要做的事

# 高危
sudo systemctl start fund-web
sudo systemctl restart fund-web
sudo systemctl enable fund-web

哪怕你「只是想快速验证一下」,也不要用 fund-web 顶上 5000。验证请打已经在跑的 gunicorn,或者临时起一个不占用 5000 的测试进程。


四、为什么是 HUP,不是 restart

方式 发生了什么 停机感
kill -HUP master master 保留,worker 滚动替换 接近无感
systemctl restart venxine-gunicorn 整组进程先灭后生 短暂不可用
restart fund-web 试图再起一个占 5000 的进程 抢端口,站点可能直接炸

个人站点流量不大,偶发 restart 未必致命。但它会培养坏习惯:Agent 一执行部署就 restart,某天 fund-web 又被 enable,事故会以你最不想看到的方式回来。

所以我把规则定成更苛刻的版本:

  1. 生产唯一服务器:venxine-gunicorn
  2. 日常部署:touch + HUP master
  3. fund-web.service:disabled,不 enable,不 restart
  4. 给 Agent 的指令里写死这几条,防止它「好心帮你 restart」

五、怎么确认你打到的是正确进程

部署后别只看「命令返回 0」。至少做三件事:

  1. 进程树
    应该只有 gunicorn master + 两个 worker,不应该再冒出一个裸的 python3 app.py

  2. 端口
    5000 只被 gunicorn 占着。Nginx 反代到 127.0.0.1:5000

  3. 业务验证
    用浏览器或 curl 打你刚改的页面,看新文案/新逻辑在不在。公网和本机 5000 都看一眼更稳:Nginx 缓存、静态规则、Flask 路由,三层任一出错都可能「本地对、公网不对」。

一个小细节:Flask 自己的 / 在本机 5000 上可能是 404,因为首页是 Nginx 直接吐的静态 /var/www/venxine.vip/index.html。这是设计,不是服务挂了。


六、迁移之后,旧服务为什么还留着

有人会问:既然危险,为什么不把 fund-web.service 文件删掉?

我暂时留着,原因很实际:

  • unit 文件本身不占端口,disabled 就不会自动起来
  • 留着能对照「以前怎么起的」,写文章、排事故时有据可查
  • 真正要命的是 enable/start/restart,不是文件躺在 /etc/systemd/system/

如果哪天想更干净,可以改名成 fund-web.service.disabled 或移进备份目录。在那之前,靠纪律和文档比靠删除更重要。


七、给以后的自己 / Agent 的速查卡

生产服务:venxine-gunicorn.service
工作目录:/home/agentuser/fund-web
配置文件:gunicorn_config.py  (127.0.0.1:5000, 2×sync workers)
遗留服务:fund-web.service  (disabled,禁止 start/restart/enable)

改模板:
  touch templates/xxx.html
  sudo kill -HUP <gunicorn master pid>

改 app.py:
  同上,HUP master

改 Nginx:
  cp sites-available → sites-enabled
  nginx -t && nginx -s reload

验证:
  进程树只有 gunicorn
  5000 未被 python3 app.py 占用
  公网页看到新内容

八、收尾

个人站点的部署问题,往往不是「不会用 gunicorn」,而是新旧两套习惯叠在同一台机器上。代码还是那份 app.py,systemd 却多了一个会抢端口的幽灵服务。

我现在的稳态很无聊,也正是因为无聊才可靠:

生产只跑 venxine-gunicorn;改完就 touch + HUP;fund-web 的 unit 可以留档,但不要再让它活过来。

下次手又想敲 restart fund-web 的时候,先想想 5000 端口上已经有人了。