05-缓存、静态资源与性能优化

1. 阶段定位

第五阶段是 Nginx 从“安全可靠入口层”进入“高性能资源治理层”的关键阶段。前四个阶段已经解决了部署、配置体系、请求匹配、反向代理、负载均衡、HTTPS、安全治理和入口访问控制。本阶段重点解决下面这些问题:

1
2
3
4
5
6
7
8
9
10
11
一个请求进入 Nginx 后:
-> 如果请求的是静态资源,如何尽快返回
-> 如果资源可以缓存,缓存应该放在浏览器、Nginx 还是上游系统
-> 浏览器缓存、协商缓存、代理缓存分别解决什么问题
-> gzip、Brotli 是否应该开启,开启后 CPU 和带宽如何权衡
-> sendfile、tcp_nopush、tcp_nodelay、open_file_cache 如何影响性能
-> proxy_cache 如何设计 key、TTL、绕过规则、刷新规则和清理策略
-> 动态接口、登录态接口、个性化数据能不能缓存
-> 大文件、视频、下载、图片、前端构建产物如何治理
-> worker、连接数、文件描述符、端口、内核参数如何配合调优
-> 如何通过压测和指标判断 Nginx 性能瓶颈

Nginx 的性能优化不是把所有参数调大,也不是盲目开启缓存。真正重要的是先理解资源类型、请求链路和瓶颈位置,再选择合适的优化手段。

完成本阶段后,应该具备下面的能力:

  • 能设计静态资源目录结构、缓存头、过期时间、版本化文件和回滚策略。
  • 能解释浏览器强缓存、协商缓存、代理缓存、CDN 缓存和 Nginx 本地缓存的区别。
  • 能正确配置 expiresCache-ControlETagLast-Modified 等缓存相关响应头。
  • 能配置 gzip 压缩,并理解压缩等级、压缩类型、CPU 消耗和传输收益。
  • 能理解 Brotli 的适用场景,以及开源 Nginx 默认不内置 Brotli 的事实。
  • 能配置 sendfiletcp_nopushtcp_nodelayaiodirectio 等静态文件发送相关参数。
  • 能使用 open_file_cache 减少频繁文件元数据查询带来的系统调用开销。
  • 能设计和落地 proxy_cache,包括缓存区、缓存 key、TTL、状态码策略、绕过规则、锁、防穿透和清理。
  • 能识别哪些内容不能缓存,避免缓存用户隐私数据、权限数据、个性化响应和错误响应。
  • 能使用 wrkabheycurl、日志字段和系统指标评估优化效果。
  • 能从 CPU、内存、磁盘 IO、网络带宽、文件描述符、连接状态和 upstream 能力判断瓶颈。

2. 学习边界

2.1 本阶段重点

本阶段重点关注 Nginx 性能优化和缓存治理,包括:

  • 静态资源服务优化。
  • 浏览器缓存与 HTTP 缓存语义。
  • expiresCache-Control
  • ETagLast-Modified
  • 前端构建产物缓存策略。
  • 图片、CSS、JS、字体、下载文件缓存策略。
  • gzip 压缩配置与验证。
  • Brotli 基础认知与部署边界。
  • sendfiletcp_nopushtcp_nodelay
  • aiodirectio 与大文件下载。
  • open_file_cache 文件缓存。
  • proxy_cache_pathproxy_cacheproxy_cache_key
  • 缓存 TTL、状态码缓存、缓存锁、缓存绕过、缓存刷新。
  • X-Cache-Status 缓存命中观测。
  • 防止缓存穿透、缓存击穿和缓存雪崩。
  • Nginx worker、连接数和文件描述符调优。
  • Linux 内核参数与网络队列基础调优。
  • 压测方法、指标解释和性能优化闭环。

2.2 本阶段暂不深入

下面内容与性能有关,但不是本阶段重点:

  • CDN 厂商平台的完整边缘缓存体系。
  • OpenResty Lua 动态缓存和本地字典缓存。
  • Redis、Memcached 等应用层缓存设计。
  • Kubernetes Ingress Controller 性能注解体系。
  • HTTP/3、QUIC 的深入机制。
  • Nginx 源码中的 sendfile、event loop 和 filter chain 实现细节。
  • 大规模 WAF、Bot 管理、边缘计算和全局流量调度。
  • 自研缓存淘汰算法或代理缓存模块开发。

本阶段目标不是把 Nginx 变成完整 CDN,而是掌握生产中最常见、最实用的静态资源优化、代理缓存和系统性能调优能力。

3. 性能优化的基本原则

3.1 不要先调参数,要先定位瓶颈

很多人学习 Nginx 性能优化时,会先搜索一份“高并发配置模板”,然后直接复制下面这些配置:

1
2
3
4
5
worker_processes auto;
worker_connections 65535;
sendfile on;
gzip on;
keepalive_timeout 65;

这些配置本身并不一定错,但如果不了解瓶颈位置,盲目使用可能没有收益,甚至产生副作用。

常见瓶颈来源包括:

瓶颈位置 表现 常见原因
CPU top 显示 Nginx worker CPU 很高 gzip 等压缩开销、TLS 开销、日志过多、正则匹配复杂
内存 系统内存持续上涨或发生 OOM 缓冲区过大、连接过多、缓存区规划不合理
磁盘 IO 静态文件或缓存读取慢 磁盘性能差、缓存目录随机 IO 多、日志同步写入压力大
网络带宽 Nginx CPU 不高但吞吐上不去 出口带宽打满、网卡队列瓶颈、包量过大
文件描述符 出现 too many open files worker_rlimit_nofile、系统 ulimit 太低
连接队列 大量连接超时或拒绝 somaxconn、backlog、worker 连接数不足
上游服务 Nginx 等待 upstream 时间长 后端慢、连接池不足、数据库慢、线程池耗尽
客户端 日志中 $request_time 高但 upstream 不高 客户端网络慢、下载慢、移动网络不稳定

性能优化要先回答:

1
2
3
4
5
6
7
8
当前慢在哪里?
-> 是 Nginx 自身慢
-> 是磁盘慢
-> 是网络慢
-> 是后端慢
-> 是客户端慢
-> 是缓存未命中
-> 是压缩或 TLS 消耗太高

3.2 性能优化闭环

推荐使用下面的闭环做优化:

1
2
3
4
5
6
7
8
明确目标
-> 建立基线
-> 单项调整
-> 压测验证
-> 观察指标
-> 对比结果
-> 保留有效调整
-> 回滚无效调整

不要一次改十几个参数。否则性能变好或变差时,很难知道真正原因。

3.3 常见优化目标

不同场景下,优化目标不同:

场景 优化目标 重点手段
静态站点 降低延迟、提高吞吐、减少带宽 sendfile、缓存头、gzip、Brotli、文件版本化
图片站点 提高命中率、降低源站压力 长缓存、CDN、文件名版本化、合理压缩
API 网关 降低后端压力、控制突发流量 proxy_cache、限流、连接复用、超时治理
下载站点 稳定大文件传输 sendfileaiodirectio、限速、Range 支持
高并发入口 增加连接承载能力 worker、连接数、文件描述符、内核队列
低带宽环境 减少传输体积 gzip、Brotli、图片压缩、缓存
后端慢接口 减少重复请求 短 TTL 代理缓存、缓存锁、降级策略

4. 静态资源服务基础

Nginx 天然适合服务静态资源。静态资源通常包括:HTML 页面、CSS 文件、JavaScript 文件、图片文件、字体文件、视频文件、下载文件、前端构建产物、文档、压缩包和安装包。

最基础的静态资源配置如下:

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name static.example.com;

root /data/www/static;

location / {
try_files $uri $uri/ =404;
}
}

假设请求 GET http://static.example.com/assets/app.js,实际查找路径是 /data/www/static/assets/app.js

4.1 root 与 alias 复习

root 是把请求 URI 追加到指定目录后面:

1
2
3
location /images/ {
root /data/www;
}

请求 /images/a.png 对应文件路径 /data/www/images/a.png

alias 是把 location 前缀替换成指定目录:

1
2
3
location /images/ {
alias /data/pictures/;
}

请求 /images/a.png 对应文件路径 /data/pictures/a.png

生产中要特别注意:alias 目录通常要以 / 结尾,location 前缀也建议以 / 结尾,避免路径拼接异常。

4.2 静态资源目录建议

推荐将不同类型资源拆分目录:

1
2
3
4
5
6
7
8
9
10
/data/www/site/
├── index.html
├── assets/
│ ├── css/
│ ├── js/
│ ├── img/
│ └── fonts/
├── upload/
├── download/
└── error_pages/

不同目录使用不同缓存策略:

目录 资源特点 建议缓存策略
/assets/ 构建产物,文件名带 hash 长缓存,一年或更久
/img/ 图片资源,较少变化 较长缓存,按版本或路径更新
/fonts/ 字体文件,较少变化 长缓存,注意跨域
/upload/ 用户上传内容,可能变化 按业务决定,谨慎长缓存
/download/ 大文件下载 支持 Range,可限速
/index.html 前端入口 HTML 不建议长缓存,通常短缓存或不缓存
/api/ 动态接口 默认不按静态资源缓存

4.3 前端单页应用配置

前端 SPA 常见配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
listen 80;
server_name app.example.com;

root /data/www/app;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}

location /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}

location = /index.html {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}

这里的关键点是:

  • /assets/ 中的文件名通常带 hash,例如 app.8f3a2c.js,可以长缓存。
  • index.html 是入口文件,经常引用新的资源文件,不应该长缓存。
  • try_files $uri $uri/ /index.html 用于支持前端路由刷新。
  • API 请求不要误落入 /index.html,一般应单独配置 /api/ 代理。

4.4 错误示例:把所有内容都长缓存

错误配置:

1
2
3
4
5
location / {
expires 1y;
add_header Cache-Control "public";
try_files $uri $uri/ /index.html;
}

问题:

  • index.html 被长缓存后,前端发布新版本,用户仍可能加载旧入口文件。
  • 如果 HTML 引用的新旧资源不一致,可能出现白屏、接口异常或静态文件 404。
  • 动态页面或用户相关页面被缓存,可能产生严重安全问题。

正确思路:入口 HTML 短缓存或不缓存,带 hash 的静态资源长缓存,动态接口默认不缓存,用户相关内容谨慎缓存。

5. HTTP 缓存基础

HTTP 缓存主要分为三类:

1
2
3
4
5
6
7
8
9
10
11
强缓存
-> 浏览器直接使用本地缓存,不向服务端发请求

协商缓存
-> 浏览器向服务端验证资源是否变化
-> 未变化返回 304
-> 已变化返回 200 + 新内容

代理缓存
-> 缓存在代理层,例如 Nginx、CDN、网关
-> 多个用户共享缓存结果

5.1 强缓存

强缓存主要由下面响应头控制:

1
2
Cache-Control: max-age=31536000
Expires: Sat, 11 Jul 2027 00:00:00 GMT

现代浏览器优先使用 Cache-ControlExpires 是 HTTP/1.0 时代的字段,仍常用于兼容。

Nginx 配置示例:

1
2
3
4
location /assets/ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}

如果同时使用 expiresadd_header Cache-Control ...,需要避免多个 Cache-Control 头语义冲突。生产中建议统一规划,明确每类资源的缓存策略。

5.2 Cache-Control 常用指令

指令 含义 常见场景
public 可以被浏览器和共享缓存保存 静态资源
private 只能被用户浏览器缓存,不应被共享缓存保存 用户个人页面
max-age=秒 浏览器缓存有效期 静态资源、短缓存接口
s-maxage=秒 共享缓存有效期,优先于 max-age CDN、代理缓存
no-cache 可以存储,但每次使用前必须验证 HTML、频繁变化资源
no-store 不允许存储 登录、支付、敏感数据
must-revalidate 过期后必须重新验证 严格一致性资源
immutable 有效期内资源不会变化 带 hash 的构建产物

常见策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 带 hash 的静态资源
location /assets/ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}

# HTML 入口文件
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}

# 用户敏感页面
location /account/ {
add_header Cache-Control "private, no-store" always;
}

5.3 协商缓存

协商缓存主要依赖 Last-ModifiedIf-Modified-SinceETagIf-None-Match

流程:

1
2
3
4
5
6
7
8
第一次请求:
服务端返回 200 + 内容 + Last-Modified/ETag

第二次请求:
浏览器携带 If-Modified-Since/If-None-Match
服务端判断资源是否变化
-> 未变化:返回 304,不返回响应体
-> 已变化:返回 200 + 新响应体

Nginx 默认会对静态文件返回 Last-Modified,也通常会启用 ETag。可以通过下面指令控制:

1
2
etag on;
if_modified_since exact;

不建议随意关闭 ETag。如果有多台静态资源服务器,且文件元数据不一致,可能出现 ETag 不一致问题,此时可以考虑关闭 ETag,然后依赖文件名 hash 和 Cache-Control 做版本治理。

5.4 304 不是错误

访问日志中如果看到大量 304,一般不是错误。它表示客户端缓存可用,服务端无需返回完整响应体。

状态码 含义 是否异常
200 返回完整资源 正常
304 资源未变化,使用本地缓存 正常
404 文件不存在 需要检查路径或发布结果
403 权限不足或目录禁止访问 需要检查权限和配置
416 Range 请求范围不合法 大文件下载场景需要关注

6. 静态资源缓存策略设计

6.1 推荐策略总表

资源类型 是否适合长缓存 推荐策略
HTML 入口文件 no-cache 或短缓存
带 hash 的 JS/CSS public, max-age=31536000, immutable
不带 hash 的 JS/CSS 谨慎 短缓存或改造成带版本号
图片资源 通常适合 长缓存,依赖 URL 版本变化
字体资源 适合 长缓存,注意 CORS
用户头像 视业务而定 URL 带版本号可长缓存,否则短缓存
下载文件 适合 长缓存,支持 Range
API 响应 默认不适合 根据接口语义单独设计
登录态页面 不适合 private, no-store
支付、订单、账户 不适合 no-store

6.2 文件名 hash 是长缓存前提

长缓存最可靠的方式是“内容变,文件名变”。例如:

1
2
3
4
app.js                 不适合长缓存
app.20260711.js 可以缓存,但版本粒度粗
app.8f3a2c91.js 更适合长缓存
style.a81c0f09.css 更适合长缓存

推荐发布流程:

1
2
3
4
5
6
7
构建生成带 hash 文件
-> 上传静态资源到服务器
-> 确认新资源文件存在
-> 更新 index.html 引用
-> reload 或无感发布
-> 保留旧 hash 文件一段时间
-> 定期清理历史资源

不要发布新版本时立即删除旧资源。因为用户浏览器可能还缓存着旧的 index.html,它仍会引用旧 hash 文件。如果旧文件被删除,就会出现静态资源 404。

6.3 SPA 发布缓存策略

推荐配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
server {
listen 80;
server_name app.example.com;

root /data/www/app;
index index.html;

location /api/ {
proxy_pass http://backend_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}

location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|webp|svg|ico|woff2?)$ {
try_files $uri =404;
expires 30d;
add_header Cache-Control "public, max-age=2592000" always;
}

location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}

location / {
try_files $uri $uri/ /index.html;
}
}

注意顺序:/api/ 要优先独立代理,不要落到前端路由;/assets/ 带 hash 的资源可以长缓存;index.html 不要长缓存;前端路由由最后的 location / 兜底。

6.4 字体跨域缓存

如果字体资源被多个域名引用,可能需要 CORS:

1
2
3
4
5
location ~* \.(?:woff|woff2|ttf|otf)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Access-Control-Allow-Origin "*" always;
}

不要对所有资源无脑加 Access-Control-Allow-Origin "*"。字体、公开图片等静态资源可以这样做,但带用户身份或敏感数据的接口不能这样处理。

7. gzip 压缩

7.1 gzip 解决什么问题

gzip 用 CPU 换带宽。它适合压缩文本类资源,例如 HTML、CSS、JavaScript、JSON、XML、SVG 等。它不适合重复压缩已经压缩过的资源,例如 jpg、png、webp、gif、mp4、zip、gz、br 等。

压缩收益取决于资源类型:

类型 gzip 收益 说明
HTML 文本重复多
CSS 文本规则多
JS 文本重复多
JSON 字段名重复多
SVG XML 文本
JPEG/PNG/WebP 已经是压缩格式
ZIP/GZ/BR 几乎无收益 已压缩
视频 几乎无收益 已压缩

7.2 gzip 基础配置

推荐放在 http 上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
http {
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_disable "msie6";

gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
application/atom+xml
image/svg+xml;
}

参数说明:

指令 说明 建议
gzip on 开启 gzip 常用
gzip_comp_level 压缩等级,1-9 生产常用 4-6
gzip_min_length 小于该大小不压缩 1KB 或 2KB 起
gzip_vary 返回 Vary: Accept-Encoding 建议开启
gzip_proxied 对代理请求启用压缩 视场景配置
gzip_types 指定压缩 MIME 类型 只压缩文本类
gzip_disable 禁用老旧客户端 一般保留

7.3 gzip 验证

使用 curl 验证:

1
curl -I -H 'Accept-Encoding: gzip' http://static.example.com/assets/app.js

重点看:

1
2
Content-Encoding: gzip
Vary: Accept-Encoding

也可以下载对比大小:

1
2
3
curl -H 'Accept-Encoding: gzip' -o /tmp/app.js.gz http://static.example.com/assets/app.js
curl -H 'Accept-Encoding:' -o /tmp/app.js http://static.example.com/assets/app.js
ls -lh /tmp/app.js /tmp/app.js.gz

7.4 gzip 常见问题

开启 gzip 后没生效,优先检查客户端是否发送 Accept-Encoding: gzip、响应 Content-Type 是否在 gzip_types 中、文件大小是否小于 gzip_min_length、是否已经被上游压缩、是否命中了不压缩的 location、是否为图片或视频等不适合 gzip 的类型。

压缩等级不是越高越好。gzip_comp_level 9 会显著增加 CPU 消耗,但比 5 或 6 多出来的压缩收益通常有限。CPU 充足且带宽昂贵时可以提高到 6;高并发且 CPU 紧张时使用 4 或 5;极端高吞吐静态站应考虑预压缩或 CDN。

8. Brotli 压缩

8.1 Brotli 是什么

Brotli 是一种压缩算法,通常对文本资源的压缩率优于 gzip,尤其适合 HTML、CSS、JavaScript、JSON、SVG 等资源。浏览器通过 Accept-Encoding: br, gzip, deflate 声明支持,服务端通过 Content-Encoding: br 返回压缩结果。

8.2 Nginx 与 Brotli 的关系

开源 Nginx 官方默认模块中不包含 Brotli。要使用 Brotli,常见方式是:

  • 使用带 Brotli 模块的发行版。
  • 自行编译第三方 ngx_brotli 模块。
  • 在 CDN 层开启 Brotli。
  • 对静态资源做预压缩,配合支持模块返回 .br 文件。

如果没有明确的编译和维护能力,不建议在生产核心入口上随意引入第三方模块。很多场景下,在 CDN 层开启 Brotli 更简单、更稳定。

8.3 Brotli 配置示例

如果已经安装 Brotli 模块,常见配置类似:

1
2
3
4
5
6
7
8
9
10
11
12
brotli on;
brotli_comp_level 5;
brotli_min_length 1024;
brotli_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
image/svg+xml;

验证:

1
curl -I -H 'Accept-Encoding: br' http://static.example.com/assets/app.js

重点看 Content-Encoding: br

8.4 gzip 与 Brotli 如何选择

场景 建议
普通站点 gzip 足够
前端静态资源较多 gzip 必开,可考虑 CDN Brotli
带宽成本很高 Brotli 有价值
CPU 已经紧张 谨慎动态 Brotli
没有模块维护能力 不建议自行编译第三方模块
已使用 CDN 优先在 CDN 层开启 Brotli

9. sendfile 与文件发送优化

9.1 sendfile 的作用

传统文件发送大致流程:

1
2
3
4
5
磁盘文件
-> 内核缓冲区
-> 用户态 Nginx 缓冲区
-> 内核 socket 缓冲区
-> 网卡发送

开启 sendfile 后,可以减少用户态和内核态之间的数据拷贝:

1
2
3
4
磁盘文件
-> 内核缓冲区
-> socket 缓冲区
-> 网卡发送

配置:

1
2
3
http {
sendfile on;
}

对于静态文件服务,通常建议开启。它尤其适合 CSS、JS、图片、下载文件等直接从磁盘读取并返回给客户端的场景。

9.2 tcp_nopush

tcp_nopush 通常与 sendfile 一起使用,让 Nginx 尽量把响应头和文件内容组织成更合适的数据包发送。

1
2
3
4
http {
sendfile on;
tcp_nopush on;
}

适合场景:

  • 静态文件服务。
  • 大响应体返回。
  • 下载文件。
  • 希望减少网络包数量的场景。

9.3 tcp_nodelay

tcp_nodelay 用于禁用 Nagle 算法,减少小包延迟,常用于 keepalive 连接:

1
2
3
http {
tcp_nodelay on;
}

生产中常见组合:

1
2
3
sendfile on;
tcp_nopush on;
tcp_nodelay on;

不要把这三个参数理解成互相替代。它们作用在不同阶段和不同发送行为上:sendfile 关注文件发送路径,tcp_nopush 关注尽量合并包,tcp_nodelay 关注降低小响应延迟。

9.4 大文件下载配置

大文件下载示例:

1
2
3
4
5
6
7
8
9
10
11
location /download/ {
root /data/files;

sendfile on;
tcp_nopush on;

limit_rate_after 10m;
limit_rate 2m;

add_header Accept-Ranges bytes;
}

说明:

  • Accept-Ranges bytes 表示支持断点续传。
  • limit_rate_after 10m 表示前 10MB 不限速。
  • limit_rate 2m 表示之后限制每个连接约 2MB/s。
  • 限速可以避免少量大下载连接长期占满出口带宽。

10. aio、directio 与大文件场景

10.1 aio 的定位

aio 用于异步文件 IO,在部分大文件或特殊文件系统场景中有价值。但对于多数普通静态资源站点,开启 sendfile 已经足够。

Linux 下常见配置:

1
2
3
4
5
6
7
8
location /video/ {
root /data/media;

sendfile on;
aio threads;
directio 8m;
output_buffers 1 512k;
}

这类配置要结合 Nginx 编译参数、内核版本、文件系统和实际压测结果决定,不建议照抄。

10.2 directio 的作用

directio 可以让大文件读取绕过系统页缓存,避免大文件下载把页缓存冲掉,影响小文件访问。

示例:

1
directio 8m;

含义:大于等于 8MB 的文件使用 directio。

适合:

  • 视频文件。
  • 大安装包。
  • 大压缩包。
  • 大下载站点。

不适合:

  • 小文件很多的静态站点。
  • 前端 CSS/JS/图片资源。
  • 没有压测验证的普通业务入口。

11. open_file_cache 文件缓存

11.1 open_file_cache 解决什么问题

Nginx 服务静态文件时,需要频繁检查文件是否存在、文件元数据、权限等信息。这会产生系统调用,例如 stat()open()

open_file_cache 可以缓存:

  • 打开的文件描述符。
  • 文件大小和修改时间。
  • 目录是否存在。
  • 文件查找错误,例如 404。

这可以减少高频访问静态资源时的系统调用开销。

11.2 配置示例

1
2
3
4
5
6
http {
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 120s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
}

参数说明:

指令 说明
open_file_cache max=10000 inactive=60s 最多缓存 10000 个条目,60 秒未访问则移除
open_file_cache_valid 120s 每 120 秒检查一次缓存条目有效性
open_file_cache_min_uses 2 inactive 时间内访问至少 2 次才缓存
open_file_cache_errors on 缓存文件不存在、权限错误等结果

11.3 注意事项

open_file_cache_errors on 会缓存 404 等错误结果。如果刚发布了新文件,但之前请求过并缓存了不存在状态,短时间内可能仍返回 404,直到缓存有效性检查更新。

如果发布频繁,配置不要过于激进:

1
2
3
4
open_file_cache max=5000 inactive=30s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors off;

12. 代理缓存 proxy_cache

12.1 proxy_cache 是什么

proxy_cache 是 Nginx 作为反向代理时,把上游响应缓存到本地磁盘。后续相同请求可以直接由 Nginx 返回,减少 upstream 压力。

请求链路:

1
2
3
4
5
第一次请求:
client -> Nginx -> upstream -> Nginx 写缓存 -> client

后续命中:
client -> Nginx -> Nginx 读缓存 -> client

适合缓存:公开页面、新闻文章页、商品详情页的非登录态版本、配置类接口、热点榜单、查询成本高但短时间可接受旧数据的接口、图片缩略图处理结果。

不适合缓存:登录后的用户首页、购物车、订单、支付、用户资料、权限相关接口、带 Cookie 的个性化响应、返回私密数据的接口。

12.2 proxy_cache_path

代理缓存首先要定义缓存区:

1
2
3
4
5
6
7
8
http {
proxy_cache_path /data/nginx/cache/api
levels=1:2
keys_zone=api_cache:100m
max_size=10g
inactive=60m
use_temp_path=off;
}

参数说明:

参数 含义
/data/nginx/cache/api 缓存文件存储目录
levels=1:2 缓存文件目录层级,避免单目录文件过多
keys_zone=api_cache:100m 共享内存区名称和大小,用于存储缓存 key 元数据
max_size=10g 缓存目录最大体积
inactive=60m 60 分钟未被访问的缓存可被清理
use_temp_path=off 临时文件直接放缓存目录,减少跨文件系统 rename 成本

缓存目录要确保 Nginx worker 用户有读写权限:

1
2
mkdir -p /data/nginx/cache/api
chown -R nginx:nginx /data/nginx/cache

12.3 基础 proxy_cache 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
upstream backend_api {
server 127.0.0.1:8080;
keepalive 32;
}

server {
listen 80;
server_name api.example.com;

location /public/ {
proxy_pass http://backend_api;

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_cache api_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_valid 500 502 503 504 0s;

add_header X-Cache-Status $upstream_cache_status always;
}
}

$upstream_cache_status 常见值:

含义
MISS 未命中缓存,请求了上游
HIT 命中缓存,直接返回
EXPIRED 缓存过期,请求上游刷新
STALE 使用了过期缓存
UPDATING 正在更新缓存,其他请求使用旧缓存
BYPASS 绕过缓存
REVALIDATED 重新验证后继续使用缓存

12.4 缓存 key 设计

缓存 key 决定“什么样的请求算同一个缓存”。常见 key:

1
proxy_cache_key "$scheme$request_method$host$request_uri";

包括协议、请求方法、Host、路径和查询参数。如果接口响应与语言有关,需要加入语言:

1
proxy_cache_key "$scheme$request_method$host$request_uri$http_accept_language";

错误示例:

1
proxy_cache_key "$uri";

问题:忽略查询参数,/search?q=a/search?q=b 可能命中同一份缓存;忽略 Host,不同域名可能串缓存;忽略协议和方法,缓存边界不清晰。

12.5 缓存方法

默认只缓存 GETHEAD。通常不要缓存 POST

如果确实要缓存某些 POST 查询接口,必须非常谨慎:

1
2
proxy_cache_methods GET HEAD POST;
proxy_cache_key "$scheme$request_method$host$request_uri$request_body";

但这会带来风险:请求体过大导致 key 设计困难,请求体包含敏感数据,请求体顺序不同但语义相同,Nginx 变量读取请求体也存在额外条件。生产中更推荐把可缓存查询设计成 GET 接口,并把参数体现在 URL 中。

12.6 缓存 TTL 设计

不同接口 TTL 应根据业务接受的数据新鲜度决定:

内容 建议 TTL
新闻详情页 1-10 分钟
商品详情基础信息 10 秒-5 分钟
商品价格库存 谨慎,通常短 TTL 或不缓存
首页推荐 10 秒-1 分钟
热点榜单 10 秒-5 分钟
配置字典 5-60 分钟
公开图片处理结果 数小时到数天
404 结果 10 秒-1 分钟
5xx 错误 通常不缓存

示例:

1
2
3
4
5
6
7
location /news/ {
proxy_cache api_cache;
proxy_cache_valid 200 5m;
proxy_cache_valid 301 302 10m;
proxy_cache_valid 404 30s;
proxy_cache_valid 500 502 503 504 0s;
}

12.7 绕过缓存

常见绕过条件:请求带登录 Cookie、请求带 Authorization、请求参数包含刷新标记、后台预览请求、非 GET/HEAD 请求。

可以使用 map 定义变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
http {
map $http_authorization $skip_cache_by_auth {
default 1;
"" 0;
}

map $http_cookie $skip_cache_by_cookie {
default 0;
~*"session|token|jwt|auth" 1;
}

map $arg_nocache $skip_cache_by_arg {
default 0;
1 1;
true 1;
}

map "$skip_cache_by_auth$skip_cache_by_cookie$skip_cache_by_arg" $skip_cache {
default 0;
~1 1;
}
}

location 中使用:

1
2
3
4
5
6
7
location /public/ {
proxy_cache api_cache;
proxy_cache_bypass $skip_cache;
proxy_no_cache $skip_cache;

add_header X-Cache-Status $upstream_cache_status always;
}

区别:

指令 作用
proxy_cache_bypass 本次请求不读缓存,直接请求上游
proxy_no_cache 本次响应不写入缓存

通常二者会配合使用。

上游如果返回 Set-Cookie,往往意味着响应与用户状态有关。一般不应该缓存:

1
proxy_no_cache $upstream_http_set_cookie;

也可以忽略某些上游头,但要谨慎:

1
proxy_ignore_headers Cache-Control Expires Set-Cookie;

这会无视上游的缓存控制,风险很高。只有在你非常确定响应是公共内容,并且上游缓存头不可信时才考虑使用。

12.9 缓存锁 proxy_cache_lock

热点缓存过期时,如果大量请求同时进入,可能同时打到 upstream,形成缓存击穿。

1
2
3
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_lock_age 10s;

含义:第一个请求负责回源刷新缓存,其他相同 key 的请求等待或使用旧缓存,从而减少同一热点资源过期瞬间对后端的冲击。

12.10 使用过期缓存

当 upstream 出错时,可以返回过期缓存提升可用性:

1
2
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;

适合新闻详情、商品基础信息、配置类接口、首页公共模块。不适合价格强一致、库存强一致、权限判断、支付订单等场景。

12.11 完整代理缓存示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
proxy_cache_path /data/nginx/cache/public
levels=1:2
keys_zone=public_cache:200m
max_size=20g
inactive=2h
use_temp_path=off;

map $http_authorization $skip_cache_by_auth {
default 1;
"" 0;
}

map $http_cookie $skip_cache_by_cookie {
default 0;
~*"session|token|jwt|auth" 1;
}

map $arg_nocache $skip_cache_by_arg {
default 0;
1 1;
true 1;
}

map "$skip_cache_by_auth$skip_cache_by_cookie$skip_cache_by_arg" $skip_cache {
default 0;
~1 1;
}

upstream backend_public {
server 127.0.0.1:8080;
keepalive 64;
}

server {
listen 80;
server_name www.example.com;

location /public/ {
proxy_pass http://backend_public;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;

proxy_cache public_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 5m;
proxy_cache_valid 301 302 10m;
proxy_cache_valid 404 30s;
proxy_cache_valid 500 502 503 504 0s;

proxy_cache_bypass $skip_cache;
proxy_no_cache $skip_cache;
proxy_no_cache $upstream_http_set_cookie;

proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;

add_header X-Cache-Status $upstream_cache_status always;
}
}

13. 缓存清理与刷新

13.1 为什么需要缓存清理

缓存带来性能收益,也带来一致性问题。常见需求包括内容发布后立即刷新、商品下架后立即清理详情页、错误内容被缓存后立即删除、用户投诉看到旧内容、运营后台更新后需要生效。

13.2 开源 Nginx 的清理限制

开源 Nginx 默认没有内置按 URL 主动清理 proxy_cache 的官方指令。常见方案:

方案 说明
短 TTL 最简单,依赖自然过期
版本化 URL 内容变化时 URL 变化
删除缓存目录 粗暴,只适合低风险场景
第三方 purge 模块 需要额外编译维护
Nginx Plus API 商业版本能力
CDN 刷新接口 如果使用 CDN,优先使用 CDN purge

13.3 推荐策略

多数业务推荐组合:

1
2
3
4
5
6
静态资源:文件名 hash + 长缓存
HTML:短缓存或 no-cache
公共 API:短 TTL + 缓存锁 + stale
强一致接口:不缓存
热点页面:短 TTL + 后台预热
重大更新:版本化 URL 或 CDN purge

不要依赖“手工删除 Nginx 缓存文件”作为常规发布流程。缓存文件路径是根据 key hash 后生成的,手工定位和删除容易出错。

14. 防缓存穿透、击穿、雪崩

14.1 缓存穿透

缓存穿透是指大量请求查询不存在的数据,每次都无法命中缓存,持续打到 upstream。

1
2
3
/article/999999999 不存在
/product/invalid-id 不存在
/user/random-id 不存在

治理方法:对 404 设置很短 TTL,在应用层校验 ID 格式,对异常请求限流,使用 WAF 或网关规则拦截明显扫描。

1
proxy_cache_valid 404 30s;

注意:404 缓存时间不能太长,否则新内容发布后可能仍返回旧 404。

14.2 缓存击穿

缓存击穿是指一个热点 key 过期瞬间,大量请求同时回源。治理方法包括 proxy_cache_lock onproxy_cache_use_stale updating、热点资源预热、TTL 增加随机抖动。

Nginx 能做缓存锁和 stale,但 TTL 随机抖动通常需要应用层或发布系统配合。

14.3 缓存雪崩

缓存雪崩是指大量缓存同一时间过期,导致 upstream 压力暴增。常见原因包括批量预热时设置了相同 TTL、发布后大量缓存同时生成、缓存目录被清空、Nginx 重启后缓存元数据恢复期间大量 MISS。

治理方法:不同资源使用不同 TTL,应用层增加随机过期时间,热点资源提前预热,保留 stale 能力,后端限流和降级,避免高峰期清空缓存。

15. 日志与缓存观测

15.1 增加缓存状态日志

建议在 access log 中加入缓存状态、upstream 时间和请求时间:

1
2
3
4
5
6
7
8
9
10
11
log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" '
'rt=$request_time '
'uct=$upstream_connect_time '
'uht=$upstream_header_time '
'urt=$upstream_response_time '
'cache=$upstream_cache_status '
'host=$host upstream=$upstream_addr';

access_log /var/log/nginx/access.log main_ext;

字段含义:

字段 含义
$request_time 从收到请求到响应完成的总耗时
$upstream_connect_time 与 upstream 建立连接耗时
$upstream_header_time upstream 返回响应头耗时
$upstream_response_time upstream 完整响应耗时
$upstream_cache_status 缓存状态
$upstream_addr 实际 upstream 地址

15.2 计算命中率

简单统计:

1
awk '{for(i=1;i<=NF;i++) if($i ~ /^cache=/) print $i}' /var/log/nginx/access.log | sort | uniq -c

如果日志格式中有 cache=HITcache=MISS,可得到类似:

1
2
3
12000 cache=HIT
3000 cache=MISS
200 cache=BYPASS

命中率可粗略按下面方式计算:

1
HIT / (HIT + MISS + EXPIRED + BYPASS)

但要注意:BYPASS 是否计入分母取决于分析目标;登录用户本来就不应该命中公共缓存;命中率高不代表一定正确,可能缓存了不该缓存的内容;命中率低也不代表一定不好,可能接口本身不适合缓存。

15.3 常见日志分析目标

目标 分析方式
看缓存是否生效 统计 $upstream_cache_status
找慢请求 $request_time 排序
判断后端慢还是客户端慢 对比 $request_time$upstream_response_time
找 upstream 连接慢 $upstream_connect_time
找大响应 $body_bytes_sent
找异常状态码 统计 4xx、5xx
找热点 URL $request_uri 聚合
找缓存污染 检查 Cookie、Authorization 与 HIT 是否同时出现

16. worker 与连接数调优

16.1 worker_processes

推荐:

1
worker_processes auto;

auto 会根据 CPU 核数设置 worker 数量。对于大多数场景,这是合理默认值。

不建议盲目设置成远大于 CPU 核数。过多 worker 可能增加上下文切换,并不能提升性能。

16.2 worker_connections

配置:

1
2
3
events {
worker_connections 4096;
}

理论最大连接数大致为:

1
worker_processes * worker_connections

但实际还受系统文件描述符限制、worker_rlimit_nofile、upstream 连接、keepalive 长连接等因素影响。反向代理场景中,一个客户端请求可能同时占用客户端连接和 upstream 连接,所以实际可承载客户端并发可能明显低于理论值。

16.3 worker_rlimit_nofile

配置:

1
worker_rlimit_nofile 65535;

同时需要系统层配合:

1
ulimit -n

systemd 管理的 Nginx 还要关注 service 配置:

1
2
[Service]
LimitNOFILE=65535

如果只改 Nginx,不改系统限制,可能仍然出现 too many open files

16.4 accept_mutex

新版本 Nginx 在支持 EPOLLEXCLUSIVE 的系统上,惊群问题已经缓解。accept_mutex 是否需要开启要结合版本和系统。常见默认即可,不建议作为初学阶段重点调优项。

16.5 keepalive_timeout

客户端 keepalive:

1
2
keepalive_timeout 65;
keepalive_requests 1000;

含义:

  • keepalive_timeout:客户端长连接空闲保留时间。
  • keepalive_requests:单个 keepalive 连接最多处理请求数。

设置过短会导致连接复用效果差,TLS 握手和 TCP 建连更多;设置过长会导致空闲连接占用 worker 连接数,高并发场景可能挤占资源。

场景 建议
普通 Web 站点 30-75 秒
API 高频调用 30-60 秒
连接资源紧张 10-30 秒
长连接业务 单独 location 或服务治理

17. upstream keepalive 优化

反向代理到后端时,如果每次请求都重新建立 TCP 连接,会增加延迟和后端压力。

配置 upstream keepalive:

1
2
3
4
5
6
7
8
9
10
11
12
13
upstream backend_api {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
keepalive 64;
}

server {
location /api/ {
proxy_pass http://backend_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}

关键点:

  • keepalive 64 是每个 worker 保留的空闲 upstream 连接数,不是总连接数。
  • 必须使用 proxy_http_version 1.1
  • 通常要清空 Connection 头,避免发送 close
  • 后端也要支持 keepalive,并配置合理的空闲超时。

常见错误是只在 upstream 中写了 keepalive,但 location 里缺少 proxy_http_version 1.1proxy_set_header Connection "",导致 upstream keepalive 没有真正生效。

18. 缓冲区与临时文件

18.1 proxy_buffering

proxy_buffering 控制 Nginx 是否缓冲 upstream 响应。默认通常是开启:

1
proxy_buffering on;

开启后链路是:

1
upstream -> Nginx 缓冲 -> client

好处是后端可以更快释放连接,慢客户端不会长期占用 upstream 连接,也有利于代理缓存。缺点是大响应可能写入临时文件,实时流式响应会被缓冲影响实时性。

18.2 适合关闭缓冲的场景

下面场景可能需要关闭:SSE、流式接口、大模型流式输出、实时日志推送、长轮询。

1
2
3
4
5
location /stream/ {
proxy_pass http://stream_backend;
proxy_buffering off;
proxy_cache off;
}

18.3 proxy_buffers

常见配置:

1
2
3
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;

不要无脑调大。缓冲区按连接占用内存,高并发下会放大内存压力。估算方式是:

1
每连接缓冲占用 * 并发连接数 = 潜在内存占用

如果每连接缓冲 128KB,1 万并发理论上可能占用 1.28GB,还不包括其他连接、请求、缓存和系统开销。

18.4 临时文件目录

当响应大于内存缓冲区时,Nginx 可能写临时文件:

1
2
proxy_temp_path /data/nginx/proxy_temp 1 2;
proxy_max_temp_file_size 1024m;

如果磁盘 IO 慢,可能拖慢响应。需要关注临时目录所在磁盘性能、是否与缓存目录和日志目录共用磁盘、是否有足够空间、是否存在大量大响应。

19. Linux 内核参数基础

19.1 参数不是越大越好

内核参数调优要与机器规格、流量模型、Nginx 配置和后端能力匹配。下面参数是常见方向,不是无脑模板。

查看参数:

1
2
3
sysctl net.core.somaxconn
sysctl net.ipv4.ip_local_port_range
sysctl net.ipv4.tcp_tw_reuse

临时设置:

1
sysctl -w net.core.somaxconn=65535

永久设置:

1
2
vim /etc/sysctl.conf
sysctl -p

19.2 常见参数

参数 说明 关注点
net.core.somaxconn listen 队列上限 高并发建连场景
net.ipv4.tcp_max_syn_backlog SYN 队列长度 突发连接、SYN 半连接
net.ipv4.ip_local_port_range 本地临时端口范围 Nginx 主动连接 upstream 时重要
net.ipv4.tcp_tw_reuse TIME_WAIT 连接复用 新内核行为需核实
net.ipv4.tcp_fin_timeout FIN_WAIT 超时 连接释放速度
net.core.netdev_max_backlog 网卡收包队列 高包量场景
fs.file-max 系统级文件句柄上限 大量连接和文件访问

示例:

1
2
3
4
5
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 10240 65535
net.core.netdev_max_backlog = 250000
fs.file-max = 2097152

19.3 临时端口耗尽

Nginx 作为反向代理主动连接 upstream 时,会使用本地临时端口。如果 upstream keepalive 不生效、短连接很多,可能出现端口压力。

检查:

1
ss -ant | awk '{print $1}' | sort | uniq -c

大量 TIME-WAIT 可能表示短连接很多。

优化方向:

  • 开启 upstream keepalive。
  • 扩大 ip_local_port_range
  • 后端连接池治理。
  • 避免过短 keepalive。
  • 检查是否频繁 reload 或连接被上游关闭。

20. 压测工具与方法

20.1 压测前准备

压测不是只跑一个命令。压测前要明确:

  • 压测目标是什么:QPS、延迟、吞吐、带宽、缓存命中率还是连接承载。
  • 压测对象是什么:静态资源、代理接口、缓存接口、HTTPS 入口还是大文件下载。
  • 压测环境是否接近生产。
  • 压测客户端是否足够强,是否成为瓶颈。
  • 是否区分冷缓存和热缓存。
  • 是否会影响生产环境。
  • 是否有监控和日志记录。

20.2 wrk 示例

安装后执行:

1
wrk -t4 -c200 -d60s http://static.example.com/assets/app.js
参数 含义
-t4 4 个压测线程
-c200 200 个并发连接
-d60s 持续 60 秒

关注输出:Requests/sec、Latency avg、Latency max、Transfer/sec、Socket errors、Non-2xx responses。

20.3 hey 示例

1
hey -n 100000 -c 200 http://api.example.com/public/news/1

hey 适合快速 HTTP 压测,输出延迟分位数比较直观。

20.4 ab 示例

1
ab -n 10000 -c 100 http://static.example.com/assets/app.js

ab 简单易用,但能力较弱。学习阶段可以使用,生产压测建议结合 wrkhey 或专业压测平台。

20.5 curl 单点验证

缓存验证:

1
2
curl -I http://api.example.com/public/news/1
curl -I http://api.example.com/public/news/1

重点看:

1
2
X-Cache-Status: MISS
X-Cache-Status: HIT

压缩验证:

1
curl -I -H 'Accept-Encoding: gzip' http://static.example.com/assets/app.js

缓存头验证:

1
2
curl -I http://static.example.com/assets/app.js
curl -I http://static.example.com/index.html

Range 验证:

1
curl -I -H 'Range: bytes=0-1023' http://static.example.com/download/big.zip

期望看到 206 Partial ContentAccept-Ranges: bytesContent-Range

21. 指标解读

21.1 请求指标

指标 含义 优化方向
QPS 每秒请求数 增加缓存、减少后端等待、优化连接
平均延迟 请求平均耗时 排查慢 upstream、网络、磁盘
P95/P99 高分位延迟 更能反映用户体验和抖动
错误率 4xx/5xx 占比 区分客户端错误和服务端错误
吞吐 每秒传输数据量 带宽、压缩、大文件治理
命中率 缓存命中比例 key、TTL、绕过规则、资源热度

平均延迟容易掩盖问题。生产中更应关注 P90、P95、P99 和最大延迟。

21.2 系统指标

指标 命令 关注点
CPU topmpstat gzip、TLS、日志、正则
内存 free -mvmstat 缓冲区、连接数、页缓存
磁盘 IO iostat -x 1 缓存目录、临时文件、日志
网络 sar -n DEV 1 带宽、包量、丢包
连接 ss -ant ESTAB、TIME-WAIT、SYN-SENT
文件句柄 lsofulimit -n open files 上限

21.3 Nginx 日志指标

推荐同时观察:

1
2
3
4
5
6
7
8
$request_time
$upstream_connect_time
$upstream_header_time
$upstream_response_time
$upstream_cache_status
$status
$body_bytes_sent
$request_length

判断示例:

现象 可能原因
$request_time 高,$upstream_response_time 客户端慢、下载大文件、网络慢
$upstream_connect_time upstream 建连慢、端口压力、网络问题
$upstream_header_time 后端处理慢
$upstream_cache_status 一直 MISS key 不稳定、绕过规则命中、TTL 太短
HIT 仍然慢 磁盘 IO 慢、响应体太大、客户端慢
499 增多 客户端主动断开,可能服务慢或用户取消
502/504 增多 后端异常、超时、连接失败

22. 性能优化配置模板

下面模板适合作为学习和实验起点,不是直接复制到所有生产环境的万能配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;

error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events {
worker_connections 8192;
multi_accept on;
}

http {
include mime.types;
default_type application/octet-stream;

log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" '
'rt=$request_time '
'uct=$upstream_connect_time '
'uht=$upstream_header_time '
'urt=$upstream_response_time '
'cache=$upstream_cache_status '
'host=$host upstream=$upstream_addr';

access_log /var/log/nginx/access.log main_ext;

sendfile on;
tcp_nopush on;
tcp_nodelay on;

keepalive_timeout 60;
keepalive_requests 1000;

gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css text/xml application/javascript application/json application/xml image/svg+xml;

open_file_cache max=10000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors off;

proxy_cache_path /data/nginx/cache/public levels=1:2 keys_zone=public_cache:200m max_size=20g inactive=2h use_temp_path=off;

upstream backend_api {
server 127.0.0.1:8080;
keepalive 64;
}

server {
listen 80;
server_name example.com;

root /data/www/app;
index index.html;

location /api/public/ {
proxy_pass http://backend_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;

proxy_cache public_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 1m;
proxy_cache_valid 404 30s;
proxy_cache_valid 500 502 503 504 0s;
proxy_cache_lock on;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;

add_header X-Cache-Status $upstream_cache_status always;
}

location /api/ {
proxy_pass http://backend_api;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
add_header Cache-Control "no-store" always;
}

location /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}

location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}

location / {
try_files $uri $uri/ /index.html;
}
}
}

23. 常见错误与排查

23.1 静态资源 404

排查方向:

  • rootalias 是否拼接正确。
  • 文件是否真实存在。
  • Nginx 用户是否有权限读取。
  • 前端发布路径是否正确。
  • try_files 是否把请求转到错误位置。
  • 是否被 open_file_cache_errors 短时间缓存了 404。

命令示例:

1
2
ls -l /data/www/app/assets/app.js
sudo -u nginx test -r /data/www/app/assets/app.js && echo ok

23.2 gzip 不生效

排查方向:

  • 请求是否带 Accept-Encoding: gzip
  • MIME 类型是否在 gzip_types 中。
  • 响应大小是否超过 gzip_min_length
  • 是否已经是压缩格式。
  • 是否命中正确 location。

23.3 缓存一直 MISS

排查方向:

  • proxy_cache 是否配置在当前 location。
  • proxy_cache_pathkeys_zone 名称是否一致。
  • 缓存目录权限是否正确。
  • 是否命中 proxy_cache_bypass
  • 是否命中 proxy_no_cache
  • 上游是否返回 Cache-Control: no-storeprivateSet-Cookie
  • 请求 URL 是否每次都带随机参数。
  • 缓存 key 是否包含不稳定头。

23.4 缓存了不该缓存的内容

这是严重问题。排查方向:

  • 登录态 Cookie 请求是否被缓存。
  • Authorization 请求头是否被缓存。
  • Set-Cookie 响应是否被写入缓存。
  • 缓存 key 是否缺少用户、语言、租户、地区等维度。
  • 是否错误使用 proxy_ignore_headers
  • 是否把 /api/ 全部纳入缓存。

紧急处理流程:

1
2
3
4
5
立即关闭相关 location 缓存
-> 清理缓存目录或切换新 keys_zone/path
-> 排查缓存 key 和绕过规则
-> 检查是否有数据泄露
-> 重新灰度开启

23.5 worker_connections 调大后仍然报错

排查方向:

  • ulimit -n 是否足够。
  • systemd LimitNOFILE 是否足够。
  • worker_rlimit_nofile 是否配置。
  • 是否有大量 upstream 连接。
  • 是否存在连接泄漏或慢客户端。
  • 是否端口耗尽。

23.6 压测 QPS 上不去

排查方向:

  • 压测客户端是否成为瓶颈。
  • CPU 是否打满。
  • 网络带宽是否打满。
  • 磁盘 IO 是否高。
  • access log 是否同步写入过多。
  • 后端 upstream 是否慢。
  • 缓存是否命中。
  • TLS 握手是否成为瓶颈。
  • gzip 压缩是否消耗 CPU。

24. 实验清单

24.1 静态资源缓存实验

目标:理解不同资源的缓存头。

步骤:

  1. 准备 index.htmlassets/app.hash.jsassets/style.hash.css
  2. 配置 /assets/ 长缓存。
  3. 配置 index.html 不缓存。
  4. 使用 curl -I 查看响应头。
  5. 修改 JS 文件名,模拟前端发布。
  6. 验证旧资源保留和新资源访问。

验收:

  • /assets/ 返回长缓存头。
  • /index.html 不被强缓存。
  • 前端路由刷新能回到 index.html
  • API 请求不会被 SPA 兜底错误处理。

24.2 gzip 实验

目标:理解 gzip 压缩收益和 CPU 成本。

步骤:

  1. 准备一个较大的 JS 或 JSON 文件。
  2. 开启 gzip。
  3. 使用 curl 分别请求压缩和非压缩版本。
  4. 对比文件大小。
  5. 使用 wrk 压测不同 gzip_comp_level
  6. 观察 CPU 和吞吐变化。

验收:

  • 文本资源返回 Content-Encoding: gzip
  • 图片资源不被 gzip。
  • 压缩等级提高后 CPU 上升。
  • 能说明压缩收益与 CPU 成本的关系。

24.3 proxy_cache 实验

目标:掌握代理缓存命中、过期、绕过和日志观测。

步骤:

  1. 准备一个后端接口,例如 /public/time
  2. 配置 proxy_cache_path
  3. /public/ location 开启 proxy_cache
  4. 增加 X-Cache-Status 响应头。
  5. 第一次请求观察 MISS
  6. 第二次请求观察 HIT
  7. 等待 TTL 过期观察 EXPIRED
  8. 增加 ?nocache=1 绕过缓存。
  9. Authorization 请求验证不缓存。

验收:

  • 能稳定观察 MISS -> HIT -> EXPIRED
  • 能通过参数和请求头绕过缓存。
  • 能解释 proxy_cache_bypassproxy_no_cache 区别。
  • 能说明哪些接口不能缓存。

24.4 缓存锁实验

目标:理解热点缓存击穿。

步骤:

  1. 设置一个短 TTL 热点接口。
  2. 使用压测工具在缓存过期瞬间并发请求。
  3. 先关闭 proxy_cache_lock 观察 upstream 请求数量。
  4. 再开启 proxy_cache_lock 对比 upstream 压力。
  5. 观察 access log 中 cache 状态变化。

验收:

  • 开启缓存锁后,上游并发回源明显减少。
  • 能解释缓存击穿和缓存锁的关系。

24.5 sendfile 与大文件实验

目标:理解静态文件发送和 Range。

步骤:

  1. 准备一个 500MB 以上大文件。
  2. 开启 sendfile on
  3. 使用 curl 下载并观察速度。
  4. 使用 Range 请求验证断点续传。
  5. 配置 limit_rate_afterlimit_rate
  6. 对比限速前后效果。

验收:

  • 大文件可以正常下载。
  • Range 请求返回 206 Partial Content
  • 限速配置生效。
  • 能解释大文件下载对带宽和连接数的影响。

24.6 worker 与文件描述符实验

目标:理解连接数与系统限制。

步骤:

  1. 查看 worker_processesworker_connections
  2. 查看 ulimit -n
  3. 查看 systemd LimitNOFILE
  4. 使用压测工具逐步增加并发。
  5. 观察是否出现 too many open files
  6. 调整系统和 Nginx 限制后再次压测。

验收:

  • 能计算理论连接上限。
  • 能说明系统文件描述符与 Nginx 配置的关系。
  • 能定位 too many open files

25. 生产实践建议

25.1 静态资源实践

  • 带 hash 的静态资源使用长缓存。
  • HTML 入口文件不要长缓存。
  • 发布时先上传静态资源,再切换入口文件引用。
  • 保留旧版本静态资源一段时间。
  • 不要让 API 请求落到前端 SPA 兜底。
  • 字体跨域只对必要资源开放。
  • 大文件下载要评估带宽和限速策略。

25.2 代理缓存实践

  • 默认不缓存动态接口。
  • 只缓存明确的公共数据。
  • 登录态、Cookie、Authorization 默认绕过缓存。
  • Set-Cookie 响应默认不写缓存。
  • 5xx 默认不缓存,必要时使用 stale 兜底。
  • 缓存 key 必须包含影响响应的关键维度。
  • 添加 X-Cache-Status 便于调试,但生产对外暴露需评估。
  • 缓存 TTL 从短开始,验证稳定后再增加。
  • 重要业务上线缓存前必须做灰度。

25.3 性能调优实践

  • 每次只改一个关键参数,并记录结果。
  • 压测要区分冷缓存和热缓存。
  • 不要只看平均延迟,要看 P95/P99。
  • access log 中保留 upstream 时间和缓存状态。
  • 高并发前先检查文件描述符限制。
  • gzip 压缩等级不要盲目设置到 9。
  • proxy_buffers 不要无脑调大。
  • 内核参数调整要保留回滚方案。
  • 生产压测要避开真实用户高峰,避免影响业务。

26. 学习检查清单

完成本阶段后,你应该能回答下面问题:

  • Nginx 静态资源服务中 rootalias 的路径拼接差异是什么?
  • 为什么前端 index.html 不适合长缓存?
  • 为什么带 hash 的 JS/CSS 适合一年长缓存?
  • Cache-Control: no-cacheno-store 有什么区别?
  • 强缓存和协商缓存分别发生在什么阶段?
  • 304 表示什么?它是不是错误?
  • gzip 适合压缩哪些资源,不适合压缩哪些资源?
  • gzip_comp_level 为什么不是越高越好?
  • Brotli 为什么通常建议在 CDN 层优先开启?
  • sendfile 解决了什么问题?
  • tcp_nopushtcp_nodelay 分别适合什么场景?
  • open_file_cache 会缓存哪些内容?有什么副作用?
  • proxy_cache_pathkeys_zonemax_size 分别表示什么?
  • proxy_cache_key 设计错误会导致什么问题?
  • proxy_cache_bypassproxy_no_cache 有什么区别?
  • 为什么带 Cookie 或 Authorization 的请求默认不应该缓存?
  • proxy_cache_lock 解决什么问题?
  • proxy_cache_use_stale 的收益和风险是什么?
  • 缓存穿透、缓存击穿、缓存雪崩分别是什么?
  • 如何通过日志统计缓存命中率?
  • $request_time$upstream_response_time 如何帮助定位慢请求?
  • worker_processesworker_connectionsworker_rlimit_nofile 有什么关系?
  • 为什么调大 worker_connections 后仍可能出现连接问题?
  • upstream keepalive 要如何配置才真正生效?
  • proxy_buffering off 适合哪些场景?
  • 如何设计一次可靠的 Nginx 性能压测?

27. 过关标准

达到下面标准,可以认为第五阶段合格:

  • 能为前端静态站点设计合理的缓存策略,避免发布后白屏和旧资源问题。
  • 能为图片、字体、下载文件、HTML、JS、CSS 分别配置合适缓存头。
  • 能正确开启和验证 gzip,并解释压缩收益与 CPU 成本。
  • 能说明 Brotli 的价值、限制和部署方式。
  • 能配置 sendfiletcp_nopushtcp_nodelayopen_file_cache 并知道各自适用场景。
  • 能设计一个安全的 proxy_cache 方案,包含 key、TTL、绕过规则、缓存锁、stale 和观测头。
  • 能避免缓存用户态、登录态、权限态、支付态等敏感响应。
  • 能通过日志判断缓存命中、后端耗时、客户端耗时和慢请求来源。
  • 能使用 wrkheyabcurl 做基础压测和验证。
  • 能根据 CPU、内存、磁盘、网络、连接状态和文件描述符判断性能瓶颈。
  • 能解释 Nginx 参数调优与系统参数之间的关系。
  • 能形成“基线 -> 调整 -> 压测 -> 观测 -> 回滚或保留”的优化闭环。

28. 常见误区

  • 认为 Nginx 性能优化就是复制一份高并发配置模板。
  • 把所有资源都设置成长缓存,导致前端发布后用户无法更新。
  • 不使用文件名 hash,却给 JS/CSS 配置一年缓存。
  • 把登录态接口、用户信息接口、订单接口缓存到共享代理缓存。
  • 缓存 key 只使用 $uri,忽略查询参数、Host、语言、租户等维度。
  • 看到 304 就认为是异常。
  • gzip 压缩图片、视频、zip 等已压缩资源。
  • 把 gzip 压缩等级设置为 9,却不观察 CPU。
  • 以为开启 keepalive 后 upstream 连接复用就一定生效,忘记配置 HTTP/1.1 和 Connection
  • 无脑调大 proxy_buffers,导致高并发时内存压力变大。
  • 调大 worker_connections,但忘记系统文件描述符限制。
  • 只看 Nginx QPS,不看后端、磁盘、网络和客户端瓶颈。
  • 在生产高峰期清空缓存,导致大量请求同时回源。
  • 把缓存命中率作为唯一目标,忽略数据正确性和安全性。

29. 下一阶段衔接

完成第五阶段后,你已经具备 Nginx 高级应用能力,能够设计高性能静态资源服务、基础代理缓存和连接调优方案。下一阶段将进入:

1
06 生产排障、高可用与运维体系

下一阶段重点不再只是“如何配置得快”,而是解决生产环境中更复杂的问题:

  • 4xx、5xx、超时和连接异常如何系统排查。
  • access log、error log、stub_status、指标和告警如何建设。
  • reload、发布、回滚、灰度如何规范化。
  • 多节点 Nginx 如何做高可用。
  • Keepalived、LVS、云负载均衡和 DNS 如何配合。
  • 生产事故如何复盘和沉淀。

第五阶段关注性能和资源效率,第六阶段关注生产稳定性和运维治理。两者结合后,才算真正具备守住生产 Nginx 的能力。