Django-应用水平扩展
这篇讲什么
- 单机扛不住的时候,正确的做法不是把一台机器换得更贵,而是让应用变成「可以多开几份」的样子,再用负载均衡把流量分下去。这篇讲三件事:Django 侧要满足什么条件才能水平扩展(无状态),Nginx 的四种分流策略各自会把请求分到哪(实测数字),以及后端挂掉时流量怎么自动绕开(实测)。
- Tengine 是 Nginx 的超集,配置写法通用;它独有的主动健康检查属于外部组件,本机没有安装 Tengine,这部分按官方文档写并标注未验证。
- 实测环境:WSL2 Ubuntu 26.04.1 LTS + Nginx 1.28.3 + Gunicorn 26.2.0(3 个后端实例,每个 2 个 sync worker)+ Django 5.2.19 LTS + MySQL 9.7.4。
- 所有端口都是临时的(8061/8062/8063 后端,8071~8075 负载均衡入口),测完全部关闭。
先搞清「水平扩展」和「垂直扩展」
| 方式 | 做法 | 优点 | 代价 |
|---|---|---|---|
| 垂直扩展 | 给单机加 CPU / 内存 | 不用改代码、不用改架构 | 有上限、停机窗口、单点故障 |
| 水平扩展 | 多开几台一样的应用实例 + 负载均衡 | 近乎线性扩容、天然容错 | 必须无状态,共享资源要外置 |
Django 应用要能水平扩展,前提是实例之间不共享任何本地状态。下面这几项是最常见的隐形状态。
Django 侧的无状态改造清单
| 状态 | 单机默认做法 | 多实例的正确做法 | 实测/依据 |
|---|---|---|---|
| 会话 | db / cached_db 都行 |
数据库或 Redis,别用 signed_cookies 存大对象 |
见《Django-生产环境要注意哪些事项》 |
| 缓存 | LocMemCache(进程内) |
RedisCache / FileBasedCache |
实测 locmem 在 2 个 worker 下同一 URL 出现 2 个不同值 |
| 上传文件 | MEDIA_ROOT 本地目录 |
共享盘(NFS)或对象存储(OSS/S3) | 否则 A 机上传的文件 B 机读不到 |
| 静态资源 | 各自 collectstatic |
同一份共享目录或 CDN | 哈希名一致即可安全共享 |
| 定时任务 | 应用内线程 / crontab | Celery beat 单实例 + worker 多实例 | 见《Django-Celery》 |
| 限流计数 | 进程内存 | Redis 计数器或 Nginx limit_req |
实测见《Django-生产环境中的安全》 |
| 日志 | 本地文件 | 写 stdout 交给 systemd/采集端 | 实测 journald 能直接收到 gunicorn 访问行 |
其中缓存那条最容易翻车,实测很直观:
1 | locmem + 2 个 gunicorn worker:同一个 /cached/ 打 8 次,出现 2 个不同值 |
换成 Redis 之后所有实例共享一份,命中率与内容才稳定。
三、最简单的形态:Nginx 反代一个 Gunicorn
1 | server { |
三个要点:
X-Forwarded-Proto要写$scheme,不要写$http_x_forwarded_proto,后者等于把伪造权交给客户端。- gunicorn 只绑
127.0.0.1,公网入口只有 Nginx。 Host用$http_host(保留端口)或$host,配合 Django 的ALLOWED_HOSTS使用。
实测这一层跑通后的样子(后端 3 worker,/slow/ 固定 0.2s):
1 | 10 个并发请求墙钟 0.86s(3 个 sync worker) |
也就是说单机的并发上限约等于 worker 数,加机器之前先把 worker 数配对。
负载均衡:四种分流策略实测
三个后端实例(8061/8062/8063,每个 2 worker),Nginx 里同时配四组 upstream,各开一个入口端口:
1 | upstream rr { server 127.0.0.1:8061; server 127.0.0.1:8062; server 127.0.0.1:8063; } |
同一客户端 IP 连打 30 次,各实例的 access log 计数:
| 策略 | 配置 | 实测分布(8061 / 8062 / 8063) | 说明 |
|---|---|---|---|
| 轮询(默认) | 无指令 | 10 / 10 / 10 | 严格均匀,不看负载 |
| 加权轮询 | weight=3:1:1 |
18 / 6 / 6 | 新机器给高权重 |
| IP 哈希 | ip_hash |
0 / 0 / 30 | 同一客户端永远同一台 |
| 最少连接 | least_conn |
4 / 4 / 4(12 并发慢请求) | 谁空给谁,长请求更明显 |

4.1 怎么选
- 默认用轮询或
least_conn:应用无状态时最稳,长请求和短请求混在一起时least_conn更公平。 ip_hash只在「实在没法无状态」时当创可贴:它按客户端 IP 分桶,遇到 NAT/公司出口/移动网络会让流量集中到一台,扩容加机器也分不走老用户。- 权重适合机器规格不一致的过渡期,同规格就别写权重,徒增维护成本。
会话保持的两种做法
| 做法 | 位置 | 问题 |
|---|---|---|
ip_hash / cookie sticky |
负载均衡层 | 扩容和故障切换会抖,IP 分布不均 |
| session 存 Redis / 数据库 | 应用层 | 需要一次网络往返,但实例可随意增减 |
推荐后者。实测把 SESSION_ENGINE 指到缓存后端时,Redis 没起来会直接在读写 session 时抛连接错误——这意味着 Redis 变成了核心依赖,必须配高可用(哨兵或集群),不能当成可选组件。
健康检查与自动容错
5.1 Nginx 的被动健康检查(实测)
1 | upstream hc { |
max_fails=2:连续失败 2 次就把这台摘掉。fail_timeout=5s:摘掉 5 秒,之后再试探。down:手工标记不参与分发(维护时用)。
实测把 8061 直接 kill -9,然后连打 10 次:
1 | 连打 10 次(8061 已死、8063 标了 down):200 200 200 200 200 200 200 200 200 200 |
客户端完全没看到失败,因为 Nginx 默认会在失败后重试下一个 upstream(proxy_next_upstream)。真实证据在错误日志里:
1 | connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1 |
等 fail_timeout 过了再打 4 次,仍然全 200(8061 已死透,Nginx 又把它标失败并跳过):
1 | 结果:200 200 200 200 |
全部后端都挂时会怎样
实测把两个 upstream 都杀掉:
1 | 两个 upstream 都停 -> / 502 |
no live upstreams 就是「所有节点都在黑名单里」的信号,排查时先搜这句。
5.3 Tengine 的主动健康检查(官方文档,未在本机验证)
Nginx 开源版只有被动检查;Tengine 提供 ngx_http_upstream_check_module,可以主动探测后端:
1 | upstream django-server { |
本机没装 Tengine,这段配置没有实测过,参数含义以官方文档为准。自建健康检查接口在 Django 侧很简单:
1 | import json |
注意健康检查别写重逻辑(查大表、调外部 API),否则探测本身会把后端拖垮。
完整配置样例
1 | # /etc/nginx/conf.d/django.conf |
几个和 Django 直接相关的点:
keepalive 32要配proxy_http_version 1.1+Connection "",否则复用上不了连接。client_max_body_size默认 1m,上传文件功能会被它在 Nginx 层直接 413,跟 Django 的DATA_UPLOAD_MAX_MEMORY_SIZE/FILE_UPLOAD_MAX_MEMORY_SIZE是两回事。backup节点只在其它都不可用时启用。- 改完配置先
nginx -t再nginx -s reload(实测 reload 不断连接)。
容量估算与压测方法
worker 数怎么定
Gunicorn 官方给的起点是 2 × CPU 核数 + 1,本机 8 核 → 17。但这是 CPU 密集的经验值,实际要按接口形态调:
1 | sync worker:并发上限 = worker 数(实测 1 worker 打 10 个 0.2s 请求 = 2.11s) |
所以选 sync 还是 async,直接决定 worker 数量与数据库连接数预算:
1 | 数据库连接占用 ≈ 实例数 × worker 数 × 每 worker 并发数 |
MySQL 侧要相应看 max_connections(实测本机默认 151)。
一条命令做粗压测
没有 ab/wrk 也能拿到数量级:
1 | $ t0=$(date +%s.%N); for i in $(seq 1 12); do curl -s -o /dev/null http://127.0.0.1:8074/slow/ & done; wait |
实测数据(/slow/ 每次 sleep(0.2)):
1 | 1 个 sync worker 2.11s(串行) |
粗测的意义是看趋势和瓶颈位置,不是拿来做容量承诺;正式压测要用 wrk/locust 并盯着数据库与外部依赖。
Tengine 是什么,要不要换
Tengine 是淘宝发起的 Nginx 分支,兼容 Nginx 配置,在此之上加了:
- 主动健康检查(
upstream_check)、一致性 hash、会话保持模块。 - 动态解析 upstream 域名、异步打印日志与回滚、内存监控。
- 合并多个 CSS/JS 请求(
concat)、去除空白与注释(trim)、按关键字收集运行状态(reqstat)。 - SSL 异步与硬件加速(QAT)、更强的限流模块。
本机没有安装 Tengine,以上特性均未实测,判断依据只有官方文档与配置兼容性。选型建议:
- 你的需求能被 Nginx 开源版覆盖(反代、负载均衡、静态、限流、TLS)就别换,运维资料多。
- 明确需要主动健康检查、
concat/trim、一致性 hash 时再考虑 Tengine,或者用 Nginx Plus / OpenResty 的对应能力。
8.1 源码安装 Tengine(官方文档步骤,未在本机执行)
1 | $ wget https://ftp.pcre.org/pub/pcre/pcre-8.44.tar.gz |
老笔记里那行 cd /data/tengine-2.3.0 与解压出的 tengine-2.3.2 目录名不一致,照抄会 No such file or directory;--with-pcre 要指向 pcre 源码目录而不是安装前缀,这些都以官方文档为准。
上线前的检查清单
- 会话与缓存都不落本地(Redis 或数据库),Redis 自身有高可用。
- 上传文件写共享存储/对象存储,
MEDIA_ROOT不再是本地目录。 collectstatic输出到共享路径,哈希名一致,Nginx 直接alias。- 定时任务只在一个实例上跑(Celery beat 单实例)。
- 限流计数放 Redis 或 Nginx,不放进程内存。
ALLOWED_HOSTS覆盖所有对外域名,CSRF_TRUSTED_ORIGINS带上https://前缀。X-Forwarded-Proto由 Nginx 覆盖写$scheme,后端只监听内网。- 健康检查接口只做
SELECT 1,探测频率与max_fails/fail_timeout匹配。 - 日志写 stdout 交给 journald/采集端,不各写各的本地文件。
- 加机器前先量:worker 数、SQL 条数、慢接口,别用扩容掩盖 N+1。
参考
- Nginx 官方:
http upstream模块:weight、max_fails、fail_timeout、down、backup、keepalive。 - Nginx 官方:
least_conn模块与ip_hash。 - Nginx 官方:
proxy_next_upstream:失败重试的行为边界。 - Tengine 官方文档:主动健康检查、一致性 hash、
concat/trim等扩展模块(本机未安装验证)。 - Django 官方:部署清单:多实例下的 session、缓存、静态资源配置。
- Gunicorn 官方:设计文档:worker 模型与进程管理。