Flask 多 worker 部署时为什么不能依赖全局变量保存状态?
简化版
Flask 生产部署常用 Gunicorn/uWSGI 多进程 worker。每个 worker 有自己的内存空间,全局变量不会在 worker 之间共享;请求打到不同 worker 时会看到不同状态。因此不能用全局变量保存用户会话、计数器、任务状态等业务数据,应使用数据库、Redis 或外部存储。
详细版
开发环境单进程里,全局变量看起来能保存状态:
counter = 0
@app.get("/count")
def count():
global counter
counter += 1
return {"counter": counter}
生产如果有 4 个 worker,每个 worker 都有自己的 counter。用户连续请求可能分别命中不同 worker,看到 1、1、2、1 这类结果。面试重点是:全局变量只在当前进程内有效,不是跨请求可靠状态存储。
完整版教学
一、为什么开发环境容易误导人
开发服务器常是单进程单实例,全局变量看起来“能用”。但生产为了利用多核和提高稳定性,通常会启动多个 worker 进程。
Gunicorn master
worker 1: counter=10
worker 2: counter=3
worker 3: counter=8
这些 worker 之间内存不共享。一个请求改了 worker 1 的全局变量,worker 2 完全不知道。
二、进程内存隔离是什么
操作系统进程有独立地址空间。Python 全局变量存在进程内存里,不会自动同步到其他进程。多线程共享内存,多进程不共享内存。
线程: 同进程,共享全局变量
进程: 独立内存,全局变量各一份
Flask 多 worker 本质上是多进程模型,所以全局状态各自一份。
三、负载均衡会放大问题
Nginx、Gunicorn master 或平台负载均衡会把请求分配给不同 worker。用户不一定总打到同一个进程。即使有粘性会话,也不应该依赖内存状态,因为 worker 重启就丢。
请求1 -> worker A -> cart=[book]
请求2 -> worker B -> cart=[]
这就是为什么购物车、登录态、任务进度不能放普通全局变量。
四、哪些状态可以放全局变量
只读配置、加载后不变的模型、线程安全的客户端对象可以谨慎放全局,作为进程内缓存。但业务可变状态不应放全局。
| 状态 | 是否适合全局 |
|---|---|
| 常量配置 | 可以 |
| 数据库连接池对象 | 视扩展管理方式 |
| 用户 session | 不适合 |
| 订单状态 | 不适合 |
| 本地只读模型 | 可以但注意内存 |
全局变量可以做进程内只读缓存,不能做跨 worker 事实来源。
五、正确存储位置怎么选
用户会话可用 Flask session、Redis session 或数据库;计数器用 Redis atomic incr 或数据库;任务状态用 Celery backend、Redis、数据库;共享缓存用 Redis/Memcached。
跨请求状态 -> Redis/DB
跨 worker 状态 -> Redis/DB
跨机器状态 -> Redis/DB/对象存储
如果状态丢了会影响业务,就不要只放进程内存。
六、线程安全也要考虑
即使单 worker 多线程,全局可变变量也会有竞态条件。counter += 1 不是业务层面的安全并发更新。多线程要锁,多进程要外部原子存储。
线程安全问题: 同一进程内并发修改
多进程一致性: 不同进程状态不同步
这两个问题经常同时存在。
七、部署配置怎么影响结论
Gunicorn 的 workers、threads、preload_app 都会影响全局对象的行为。preload_app 会在 master 进程预加载应用,再 fork worker;这能节省部分内存,但 fork 后可变状态仍然是各 worker 自己维护。
preload -> fork -> copy-on-write
修改全局变量 -> 当前 worker 私有副本
所以不要因为 preload 看起来“先加载一次”,就误以为全局变量跨 worker 共享。
八、面试回答怎么落到方案
可以把答案分成三层:进程内只读对象可以全局复用;可丢失缓存可以全局但要能容忍不一致;业务真实状态必须进 Redis、数据库、队列或对象存储。
只读配置: global OK
临时缓存: global maybe
业务状态: external storage
这样回答能体现你不是简单否定全局变量,而是在按状态性质分类。
九、常见误区与追问
- 误区:Flask 全局变量所有请求共享,所以能保存状态。 只在当前进程内共享,多 worker 不一致。
- 误区:开发环境能用,生产也能用。 部署模型不同,生产多进程会暴露问题。
- 误区:加锁就能解决多 worker 状态。 进程间内存不共享,线程锁只管当前进程。
- 追问:用户登录态放哪里? Cookie session、Redis session 或数据库。
- 追问:计数器怎么做? Redis INCR 或数据库原子更新。
- 追问:本地缓存能不能用? 可以做非关键进程内缓存,要能容忍不一致和失效。
- 追问:worker 重启会怎样? 进程内全局状态全部丢失。
十、加强记忆
Flask 全局状态记成“每个 worker 一份”。开发单进程能跑,不代表生产多进程正确;跨请求、跨 worker、跨机器的状态要放 Redis、数据库或外部存储。全局变量只适合只读配置或可丢失缓存,不能当业务事实来源。