0%

为什么 Nginx 比直接跑 Gunicorn 快,而且快这么多

手头一个 Flask 应用,Gunicorn 跑在 8081 端口,用户直接访问 http://ip:8081/。页面加载很慢,有时候好几秒才出来。同一台服务器上另一个静态站点(Next.js 生成,Nginx 托管)响应就很快。

查了一圈:服务器负载正常、数据库正常、代码没性能瓶颈。最后发现问题出在访问路径上——用户走的 8081 端口 根本没经过 Nginx,所有请求直捅 Gunicorn。

把 8081 端口从 Gunicorn 手里拿过来交给 Nginx 监听,Gunicorn 改到内网 8083,问题解决了。首页加载从超时降到了 70 毫秒,CSS 文件从 232KB 变成 31KB(gzip)。

一张图说清楚架构变化

1
2
3
4
5
6
7
8
改前:
用户 → http://ip:8081 → Gunicorn(Python 进程处理一切:HTML + CSS + JS + 数据库)

改后:
用户 → http://ip:8081 → Nginx(C 语言事件驱动)
├── 静态文件 → Nginx 直出(零拷贝,0.3ms)
├── gzip 压缩 → 232KB → 31KB
└── 动态请求 → 转发到 Gunicorn:8083(只干活不跑腿)

为什么直连 Gunicorn 会慢

Gunicorn 是一个 WSGI 服务器。它的职责是跑 Python 代码,不是做静态文件分发或连接管理。但当用户直接访问 8081 端口时,Gunicorn 被迫干了三件它不擅长的事。

1. 静态文件要走一遍 Python 解释器

浏览器请求 bootstrap.min.css(232KB)。直连 Gunicorn 时:

1
2
Nginx 处理:sendfile() 系统调用,零拷贝,直接从磁盘到网卡。
Gunicorn 处理:Flask 路由匹配 → open() → read() → 拼 HTTP 头 → write() → Python 解释器每行代码都要跑

实测数据:

资源 大小 Nginx 响应 Gunicorn 响应
bootstrap.min.css 232KB 0.27ms 几十毫秒起(排队更久)
bootstrap-icons.min.css 82KB 0.3ms 同上
bootstrap.bundle.min.js 80KB 0.3ms 同上

Gunicorn 的 worker 是 sync 模式,一个 woker 一次只能处理一个请求。如果有人在传整改报告、有人在查数据库,CSS 文件也得等。

2. 没有 gzip,传输量大了 5-7 倍

232KB 的 CSS,gzip 后只有 31KB。

Nginx 做 gzip:zlib(C 语言)处理,几毫秒,不阻塞其他请求。
Gunicorn 做 gzip:可以配,但每个请求都要 Python 跑一遍压缩,CPU 先上去,worker 更不够用。

开了 gzip 之后:

资源 原文大小 gzip 后 节省
bootstrap.min.css 232KB 31KB 86%
bootstrap-icons.min.css 82KB 13KB 84%
页面 HTML ~35KB ~8KB 77%

对于移动端或网络状况差的用户,这几百 KB 的差距就是”等 5 秒”和”瞬间打开”的区别。

3. Sync worker 模式的排队问题

Gunicorn 配置了 6 个 sync worker。每个时间片只能处理一个请求。

当页面加载时,浏览器会同时发出多个请求(HTML + CSS + JS + 轮询接口)。如果 6 个 worker 都被占着,第 7 个请求就开始排队。

从 nginx 的访问日志里看到了很多 499 状态码——客户端超时断开连接时 nginx 记录的标志。在我测试 https://six-mah.yaoyoupei.com/inspection/ 时,这个 499 出现了好几次:curl 等了 10 秒没响应,自己关了。

改了什么

第一步:Gunicorn 改到内网端口

/data/projects/2025form/gunicorn.conf.py

1
2
3
# 改前:bind = "0.0.0.0:8081"
# 改后:只监听 127.0.0.1,不暴露公网
bind = "127.0.0.1:8083"

第二步:Nginx 接管 8081 端口

six-mah.conf 里新增一个 server block,监听 8081:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 8081;
server_name _;

gzip on;
gzip_comp_level 5;
gzip_types text/css application/javascript application/json;

# 静态文件 Nginx 直出
location /static {
alias /data/projects/2025form/static;
expires 7d;
}

# 动态请求转给 Gunicorn
location / {
proxy_pass http://127.0.0.1:8083;
}
}

就这些。改完重启:

1
2
sudo systemctl restart 2025form.service
sudo systemctl reload nginx

改造后的数据对比

测试项 直连 Gunicorn(改前) Nginx → Gunicorn(改后)
首页 / 加载 超时(>15s) 70ms
CSS 文件(gzip 后) 232KB 原文 31KB(gzip)
静态文件响应 几十毫秒,还排队 0.3ms,不排队
首页完整渲染 用户体感 3-5 秒 < 1 秒

什么时候用这种架构

Flask/Django 这类 Python Web 应用上线时,前面加一层 Nginx 不是可选项,是必选项。

不是因为 Nginx 有什么黑科技,原因很简单:

  • Nginx 干它擅长的活:静态文件、gzip、SSL、连接管理
  • Gunicorn 干它擅长的活:跑 Python 代码、查数据库、渲染动态页面

C 语言干 C 语言的事,Python 干 Python 的事。各司其职。

如果你的应用出现了:

  • 页面加载慢但服务器负载不高
  • 静态文件请求拖慢动态页面响应
  • 客户端出现大量超时断开

先看看你的用户是不是直接连到了 Python 进程上。如果是,把这个活交给 Nginx。