本节摘要:动静分离把静态资源从动态代理链路里拆出来,由 Nginx 直接从磁盘发送;sendfile 让文件数据在内核态直达网卡,全程不进用户态。配合浏览器缓存头,静态流量的服务器成本趋近于零。
优化前的链路:图片请求走 proxy_pass 到应用服务器,应用从磁盘读文件拼进 HTTP 响应,再由 Nginx 转发——文件在应用里绕了一圈,应用 CPU 里 40% 花在搬运图片上。静态请求根本不需要"应用"参与。
server { listen 443 ssl; server_name static.example.com; # 静态独立域名 root /data/www/static; location / { # 找不到文件也不转发,直接404 try_files $uri =404; expires 30d; # 浏览器缓存一个月 add_header Cache-Control "public"; access_log off; # 静态流量不记日志,日志量砍七成 } } server { listen 443 ssl; server_name shop.example.com; # 动态主站 location /api/ { proxy_pass http://app_backend; } }
独立子域名还有个隐藏收益:静态流量与动态流量隔离了故障域,图片接口打挂不影响下单。cookie 隔离(静态域名不下发 session cookie)让每个请求头小几百字节,量大了也不可忽略。
传统读文件发网络要四次拷贝、两次系统调用;sendfile 之后是内核里的一次 DMA 搬运:
传统:磁盘→内核→用户态→socket内核缓冲→网卡 4次拷贝 sendfile:磁盘→内核页缓存→网卡 接近零拷贝
location / { sendfile on; sendfile_max_chunk 512k; # 单次传输分块,防大文件独占worker tcp_nopush on; # 攒满包头整批发送 }
大文件场景补一个 aio,让读盘也异步化,worker 不被慢磁盘卡住事件循环。
服务器侧优化得再好,也不如请求根本不发。静态资源的缓存策略按"变不变"分级:
| 资源类型 | 变更方式 | 缓存策略 |
|---|---|---|
| 带指纹的构建产物 | 文件名含 hash | expires 1y,永久缓存 |
| 无指纹图片 | 原地覆盖 | expires 7d 加 must-revalidate |
| HTML 入口 | 频繁更新 | no-cache,每次校验 |
location ~* \.([0-9a-f]{8})\.(js|css)$ { # 带指纹 expires 1y; add_header Cache-Control "public, immutable"; }
指纹方案的核心契约:内容变则文件名变。构建工具给每个产物打内容 hash,HTML 里引用新文件名,旧缓存自然作废。做不到内容寻址的资源别用 immutable,否则用户拿到锁死一年的旧版。

分离改造不能一夜切换,按"新域名上线、灰度引流、观察、全量"四步走。验收用改造前后的三组对比数字说话:
改造前(图片走应用链路): 应用 CPU 78%,图片 P99 340ms,出口带宽 4.2Gbps 改造后(独立域名直出): 应用 CPU 41%,图片 P99 45ms,出口带宽 3.1Gbps(叠加 gzip 前的数据) 应用服务器从 6 台回收 2 台 —— 这是本次改造写进季度总结的一行
灰度期间的回退预案同样简单:Nginx 上把静态域名的流量按比例 reproxy 回主站,五分钟内可回到旧链路。改造成败的分水岭往往不在技术,而在页面里硬编码的旧资源路径有没有清干净——迁移清单要包含模板、CDN 配置、客户端内置的兜地址三个层面。
静态直出后出现新问题:个别用户用多线程工具拉大包,单连接打满网卡,其他请求被挤慢。两个对策配合使用:
location /download/ { sendfile on; sendfile_max_chunk 256k; # 单次内核搬运上限,防止长占 limit_rate_after 20m; # 前 20MB 全速 limit_rate 512k; # 之后限速,普通用户无感 limit_conn addr 4; # 单 IP 下载连接数上限(需 4.2 的 zone) }
限速的哲学是"首屏体验优先":普通用户的前 20MB 全速拿到,抢完就走;只有异常的多线程Bulk下载才被限速与限连接数双约束。这类参数上线后要观察平均下载完成率的变化,防止误伤慢速网络的正常用户。
动静分离还改变了容量评估的口径。分离前,应用的容量上限被静态请求稀释——六台机器里近四成的算力在搬图片,压测得到的"单机 QPS"是个混合了两种完全不同负载的糊涂数字。分离后两类负载各自可测:静态侧的极限几乎只受网卡与内核协议栈约束,动态侧的压测才第一次反映了真实的业务处理能力。后续的扩容决策因此更准:流量涨了,先判断涨的是哪一类,静态涨加带宽即可,动态涨才加机器。第五章的降本战役,最终沉淀下来的是这套按流量构成做容量决策的方法。
零拷贝家族的最后一个成员是直接 IO,它在文件太大、内核页缓存反而成为负担的场景(视频站的源文件读取)绕开页缓存直取磁盘,与 sendfile 是互补而非替代关系。普通静态站点不必碰它,但知道它的存在,遇到"文件比内存还大、缓存全无效"的特殊负载时,至少知道往哪个方向查。性能优化的知识面就是这样一层层往外扩的:先把默认路径吃透,再为极端负载准备特殊武器。