☰
Nginx图片中转代理实战:从防盗链到缓存命中率97%的调优指南
2026/10/14 5:05:55 网站建设 项目流程

接手这个图片中转改造项目的时候,起因其实有点狼狈。业务方的图片全部存放在第三方图床上,最初前端是直接拿源站链接去加载的,结果上线没两天就被迫返工:源站开了防盗链,非白名单域名直接返回 403;图片在本端没有任何缓存,每天上千万次请求全部打到源站,带宽账单肉眼可见地涨;再加上源站节点覆盖有限,跨地域访问的延迟忽高忽低,用户端的加载体验完全不可控。换了几版方案后,最终定下来就是这套 Nginx 图片中转代理——用 Nginx 做一层统一入口,把取图、缓存、鉴权、下发全部收敛到这一层,业务方只面对自己的域名,源站是什么状态根本不用关心。

这套方案跑了大半年,整体表现超出预期:单机 QPS 从最初不到两千提升到四万以上,缓存命中率稳定在 97% 左右,回源带宽下降了 90% 以上。如果你正在做图片类业务,或者手里有个大量依赖外部图床、对象存储图片的服务,这篇文章应该能帮你省不少试错时间。文章不预设你已经是 Nginx 高手,但至少要知道 location、proxy_pass 这些基本概念,遇到不懂的参数我会尽量把原理讲透。

1. 为什么需要一层图片中转:直连源站的四道坎

先别急着写配置,得先搞清楚一个问题:图片本来就放在源站上,浏览器直接去取不行吗?在很多场景下真的不行。我遇到过的问题是典型的三件套:防盗链、带宽失控、访问不可控。但除了这些业务层面的问题,还有技术层面的隐患,逐个说。

第一个是防盗链。源站为了控制流量成本,通常会校验 Referer、User-Agent 甚至 URL 签名。浏览器直接加载时,Referer 是业务自己的域名,源站不认,403 是常态。就算你把 Referer 伪装成源站认可的域名,一旦源站改了校验策略,全线资损,运维根本来不及反应。而在中转层后面,回源请求是完全由 Nginx 发起的,Referer、Host 这些头都可以统一控制,源站只认得这一台代理服务器,业务域名换成什么都不影响。

第二个是带宽成本。直连模式下,每个用户每一次刷新页面,请求都会穿透到源站。CDN 或者对象存储的计费模式通常按流量和请求次数双重计费,100KB 的图片,一万次请求就是 1GB 流量。如果同一个封面被一万个用户看到,直连就是一万次回源,而加一层代理做缓存,理论上只需要回源一次。这里省下的不是小数目,尤其是图片这类高频率、可重复读的资源。

第三个是访问控制。直连模式下,谁能访问图片、谁能改图片、什么时间可以访问,这些规则分散在前端代码里。前端代码一改动就要重新发版,而且前端规则很容易被绕过。把入口收敛到 Nginx 后,鉴权、限流、URL 签名校验全部集中在一个地方,改规则只需要重新加载配置或者热更新,不用动业务代码。

第四个容易被忽略:协议与格式适配。源站提供的可能是 HTTP,但业务要求全站 HTTPS;或者源站只会输出原始 JPEG,你需要支持 WebP 缩略图。浏览器直连没法完成这些转换,而这些恰恰是 Nginx 最擅长的领域。

那是不是所有项目都需要这层中转?也不是。如果你的图片总量很小、访问量一天几千次、没有任何访问控制需求,直接上云厂商的 CDN 更省事,加一层代理反而多一跳链路。中转层适合的场景有这么几个:多源站聚合(不同业务线的图片散落在不同地方,统一一个出口);高频读低频改(图片上传后基本不变,天然适合缓存);需要统一处理(加水印、改尺寸、格式转换);以及源站有严格的访问控制,但你又不想让业务方感知这些复杂性。

2. 架构设计:入口、缓存、回源与图片处理四个模块

方案定了之后,我先在纸上画了四个模块:客户端入口、缓存层、回源层、图片处理层。每一层解决一类问题,配置上也是互相独立的,这样出问题时能快速定位是哪个环节。

2.1 入口路由设计

入口承载的是域名和路由规则,也就是"客户端请求长什么样"这件事。我的做法是给中转层单独分配一个域名,比如 img.example.com,所有图片请求都走它。不要复用业务主域名,因为缓存配置、限流规则、SSL 证书策略可能差异很大,混在一起会让 nginx.conf 变得极其难维护。

路径设计上,我采用了/fetch/前缀作为所有中转请求的统一入口,源站地址和源站路径全部放在后面的 URL 里。这样做的好处是入口规则只有一条,坏处是 URL 会变长,而且直接把源站信息暴露在 URL 里。对图片这种资源来说,URL 泄露源站域名其实问题不大,只要源站本身有防护,中转层反而是一层额外的屏障。如果实在介意,可以把源站映射成代号,比如/fetch/a/xxx.jpg对应某个源站,通过 map 翻译回来。

入口这一层最重要的原则是:只放行允许代理的源站,其他一律拒绝。这是从开放代理事故里学到的教训,后面专门讲。

2.2 缓存存储设计

缓存是整个方案的心脏,配置参数虽然只有一行,但每一项都值得推敲。

proxy_cache_path /data/cache/img levels=1:2 keys_zone=img_cache:100m max_size=40g inactive=30d use_temp_path=off;
  • levels=1:2控制缓存文件的目录层级,1:2 表示先建一层一级目录(一位十六进制字符),再建一层二级目录(两位十六进制字符),最终文件落在形如/data/cache/img/0/ab/xxxxxxxx的路径上。这样做的目的是避免成千上万个文件堆在同一个目录里,目录项查找会随文件数线性退化,分层后每个目录的文件数被限制在几百个以内。
  • keys_zone=img_cache:100m是在共享内存里建立缓存索引,100MB 大约可以索引 80 万个缓存条目。内存索引的作用是快速判断某个 key 是否存在、是否过期,不需要去磁盘上翻文件。这个值要根据业务体量估,图片量级大就调大,别省。
  • max_size=40g是磁盘缓存的上限,超过之后 Nginx 的 cache manager 进程会自动清理最久未访问的文件。
  • inactive=30d表示文件 30 天内没有被访问过就删除,注意这个跟下面的proxy_cache_valid是两套逻辑,后面会细说。
  • use_temp_path=off这个参数容易被忽略。Nginx 默认先把回源响应写入临时目录,再改名移动到缓存目录。如果临时目录和缓存目录不在同一个文件系统,改名就退化成一次完整拷贝,性能损失很大。关掉它,临时文件直接写在缓存目录里,跨目录原子改名,性能好很多。

2.3 回源策略设计

回源是缓存未命中时去源站取图的过程,这一层的核心是控制好超时、重试和连接复用。

upstream image_origin { server origin-a.example.com:443 weight=3; server origin-b.example.com:443 backup; keepalive 32; }

keepalive 32很关键。Nginx 默认对上游是短连接,每次请求都新建 TCP 连接,SSL 还要重新握手,回源延迟会显著拉高。开启 keepalive 后,连接池里常驻 32 条长连接,握手成本被摊平,实测回源 P99 延迟能降低 30% 到 50%。注意这里配了weight和backup,目的是在多源站场景下把流量按权重分配,其中一台挂了自动切换,不需要改业务代码。

超时参数我建议单独调,不要用默认值:连接超时 2 秒,读取超时 15 秒,发送超时 5 秒。图片请求的特点是响应体不大,但偶尔源站会卡顿,读取超时给到 15 秒是为了给慢源站留余地,连接超时则要短,源站连不上就赶紧失败切下一个,别让用户一直在转圈。

2.4 图片处理模块

图片处理用 Nginx 自带的image_filter模块,它能在响应阶段对图片做缩放、裁剪、旋转和 JPEG 质量调整。先明确一点:这个模块对源站响应做的是后处理,不改变缓存的内容。所以我的做法是原始图走原始图入口,缩略图走缩略图入口,两边各自缓存,不要在同一入口里同时做原图和缩略图,否则一次请求只出一个结果,另一条链路空转。

location ~ ^/resize/(\d+)x(\d+)/(.*)$ { image_filter resize $1 $2; image_filter_jpeg_quality 80; image_filter_buffer 8m; ... }

image_filter resize是等比缩放,宽高都小于等于指定值;如果需要强制裁剪,可以用image_filter crop。image_filter_buffer 8m指定解码图片的最大内存,默认只有 1M,超过这个大小的图片直接返回 415,线上遇到的很多"图片打不开"就是这里没调。这个模块依赖 GD 库,编译 Nginx 时要带上--with-http_image_filter_module。

3. 核心配置逐段拆解:一份可以直接落地的 nginx.conf

理论讲了半天,现在给一份简化但仍能直接上线的配置。我按逻辑分段拆开,每段都说明为什么这么写。

3.1 全局与缓存层

worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 16384; use epoll; } http { sendfile on; tcp_nopush on; tcp_nodelay on; open_file_cache max=20000 inactive=60s; open_file_cache_valid 120s; open_file_cache_errors on; open_file_cache_min_uses 2; proxy_cache_path /data/cache/img levels=1:2 keys_zone=img_cache:100m max_size=40g inactive=30d use_temp_path=off; proxy_cache_key $uri$is_args$args; proxy_cache_lock on; proxy_cache_lock_timeout 5s; proxy_cache_lock_age 10s; proxy_cache_revalidate on; proxy_cache_background_update on; proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504 updating; ... }

open_file_cache系列参数用于缓存打开过的文件句柄,图片命中缓存时不用每次都做一次 open 系统调用,对高 QPS 场景帮助很大。worker_connections 16384配合worker_rlimit_nofile 65535是为了把单 worker 的并发连接数拉上去,否则四核机器跑到两万连接就到瓶颈了。

缓存策略这块,proxy_cache_lock on解决的是缓存惊群:当热点图片缓存过期,瞬间有几百上千个请求同时到,如果没有锁,所有请求都会穿透到源站。开启后同一时刻同一个 key 只有一个请求会回源,其他请求等它写缓存,等待超过proxy_cache_lock_timeout的才放行回源兜底。

proxy_cache_background_update on是 Nginx 1.11.10+ 的特性,说人话就是:当缓存过期但还有人在用旧版本时,Nginx 先用旧的内容响应请求,同时在后台发起回源更新,用户无感知,回源的压力也被平摊到后台。这个特性对图片这种"旧图可用,新图更好"的场景非常契合。

3.2 转发与回源

map $request_uri $upstream_target { ~*^/fetch/([^/]+)/(.*)$ $1/$2; default ""; } server { listen 443 ssl http2; server_name img.example.com; # ... ssl 证书配置省略 location /fetch/ { if ($upstream_target = "") { return 400; } proxy_pass https://$upstream_target; 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_connect_timeout 2s; proxy_read_timeout 15s; proxy_send_timeout 5s; proxy_cache img_cache; proxy_cache_valid 200 302 24h; proxy_cache_valid 301 1h; proxy_cache_valid 404 1m; proxy_cache_valid 500 502 503 504 0s; proxy_cache_valid any 1m; expires 7d; add_header X-Cache-Status $upstream_cache_status; } }

proxy_pass https://$upstream_target看起来简单,但这里有两个隐蔽的坑。第一,$upstream_target这种变量式的 upstream 地址,机器需要能解析域名,把 DNS 解析缓存配好;第二,传给源站的 Host 头默认会是img.example.com,如果源站按 Host 域名做防盗链或虚拟主机识别,就会把你拒之门外,所以一定要确认源站能接受这个 Host。我这里设置了proxy_set_header Host $host,意思是保持 Nginx 收到的域名,如果源站对 Host 敏感,环境变量里实际传递的值需要按源站要求调整,这一条在实际部署时是最容易踩的坑之一。

缓存有效的时长我用表格拆开看:

状态码缓存时长理由
200/30224h正常图片,长缓存收益最大
3011h重定向结果变化频率低,但避免永久缓存
4041m源站短暂缺失时,不让资源永久变"坏图"
5xx0s源站故障不能缓存,否则故障会持续被放大
其他1m兜底策略

proxy_cache_valid any 1m是兜底。注意它的优先级低于前面具体的状态码规则,Nginx 会优先匹配更具体的状态码。这行配置花了我一次线上事故的代价才想明白:当时只配置了proxy_cache_valid 200 24h,结果源站某天返回了一大批 404,这些 404 直接被按 24 小时缓存下来,图挂了整整一天。后来把异常状态码单独拎出来,该短缓存的短缓存,该不缓存的不缓存。

3.3 图片处理与访问控制

location ~ ^/resize/(\d+)x(\d+)/(.*)$ { set $origin_path /$3; image_filter resize $1 $2; image_filter_jpeg_quality 80; image_filter_buffer 8m; proxy_pass https://$upstream_target; ... }

图片处理入口和原图入口共用同一个缓存池,但缓存 key 不同。缩略图的 key 是完整 URI,原图的 key 也是完整 URI,互不干扰,同一个源图可以根据不同尺寸分别生成多个缩略图缓存,请求方拿到哪个尺寸就用哪个,源站只需要存一份原图。

访问控制我用了三层:第一层限制请求方法只允许 GET/HEAD;第二层按源站域名白名单放行,非白名单地址直接 403;第三层对业务方 APP 接口下发的图片 URL 做签名校验,这个放进下一章详细讲。

if ($request_method !~ ^(GET|HEAD)$) { return 405; }

3.4 监控日志

没有监控的中转代理就是盲人开车。我在日志格式里额外记录了两个字段:缓存状态和请求耗时。

log_format img '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" ' 'cache=$upstream_cache_status reqtime=$request_time'; access_log /var/log/nginx/img_access.log img;

$upstream_cache_status是整个方案的晴雨表,取值包括 HIT、MISS、EXPIRED、REVALIDATED、UPDATING、STALE、BYPASS。日常巡检时坐在服务器上跑一条 awk 就能看到命中率分布:

awk '{for(i=1;i<=NF;i++) if($i ~ /^cache=/){sub(/^cache=/,"",$i); c[$i]++}} END{for(k in c) printf "%s %d\n", k, c[k]}' /var/log/nginx/img_access.log | sort

命中率是 HIT 占总请求的比例,我定的及格线是 95%。如果低于这个数,先查缓存 key 是不是被参数污染了,这个坑概率最大,具体怎么排查放到下一节。

4. 缓存命中率从 70% 拉到 97%:四个参数坑与调优实录

方案上线第一周,命中率只有 70% 出头,和预期的 95% 差了十万八千里。当时心里是很慌的,因为命中率上不去,回源流量降不下来,中转层的存在意义就打折扣。后来一层层排查,揪出来四个问题,每一个都是只写业务代码时根本想不到的细节。

4.1 缓存键污染:Query String 是罪魁祸首

最早看曝光日志时发现,同一个图片 URL 频繁出现 MISS,但看起来明明是同一张图。把访问记录拉出来一看,问题清楚了:同一个图片,有的请求带?x-oss-process=image/resize,w_100,有的带?t=1690000000,还有的带营销系统追加的?from=feed。而我的缓存键用的是$uri$is_args$args,意思是完整的路径加完整的参数串。参数串多一个字符,就是一个全新的缓存条目。同一个图被一百个不同的参数组合访问,就会在缓存里放一百份,每份的量都只有一点点,自然谁也活不长。

我的处理方案是:缓存键归一化,只保留真正影响图片内容的参数。

set $cache_key "$uri?w=$arg_w&h=$arg_h&q=$arg_q"; proxy_cache_key $cache_key;

这里只保留w、h、q三个白名单参数,其他参数一律不进缓存键。效果立竿见影:同一张原图的不同缩略图请求,只要尺寸参数一致,就命中同一条缓存。带随机营销参数的请求完全不影响缓存复用。如果你的入口需要处理更多参数,可以用 Lua 在access阶段做更精细的归一化,但纯 Nginx 解决 90% 的场景足够。

4.2 异常响应被缓存:404 的坑

那次 404 缓存事故之后,我把状态码缓存策略调整成了上一节表格里的样子。这里再强调一下原理:proxy_cache_valid 200 302 24h和proxy_cache_valid 404 1m是可以在一个 location 里共存的,Nginx 按响应状态码匹配规则。我遇到的问题是很多人图省事只写proxy_cache_valid 200 24h,结果一旦源站出问题返回 5xx 或 404,Nginx 会拿默认的any 1m甚至完全不缓存来对待,行为差异很大。

线上我的硬性要求是:5xx 一律0s(不缓存),404 只给 1 分钟。原因很朴素:源站故障通常是暂时的,缓住了 5xx,等于把故障的持续时间人为拉长;404 短缓存是防止频繁回源打到坏链,但也不能太长,否则运维修复后用户还在看旧错误。

4.3 热点图片过期后的惊群问题

缓存键干净之后,命中率到了 90%,但监控里发现一个诡异的现象:某些热门图片的缓存过期瞬间,源站 CPU 会冲出一个尖峰,然后迅速回落。这是典型的缓存惊群。比如某个封面图被 5000 个并发请求读,它刚好在 10 秒前过期,5000 个请求几乎同时判定为 MISS,同时回源,源站直接被压垮。

解决方式就是前面配置里提到的proxy_cache_lock on。它的原理是给缓存 key 加锁,命中锁的请求回源并写缓存,其他请求原地等待,直到锁释放后直接读刚写好的缓存。proxy_cache_lock_timeout 5s是等待上限,超过之后即使锁没有释放也放行回源,避免因为一个慢请求拖死所有请求。proxy_cache_lock_age 10s的意思是,第一个请求 10 秒还没写完缓存,锁就失效,让下一个请求顶上重新回源。

这几个参数配合proxy_cache_use_stale ... updating一起用,效果是:等待锁的请求不再空等,而是先拿旧缓存内容返回用户,后台更新完成后新用户自然看到新图。用户无感,源站承压也小。

4.4 缓存过期与主动清理:图片更新了怎么办

最后一个问题是内容更新。业务方偶尔会替换同一 URL 下的图片(比如修正一张打折图的价格标签),但我的缓存 TTL 是 24 小时,替换后用户看到的还是旧图。这个问题没法靠参数完全解决,我用的组合方案是三层:

  • 第一层,业务约定:凡是内容会变的图片,URL 必须带版本号,/v2/、/20240501/这种,从根上避免同名覆盖。
  • 第二层,主动 purge:引入 Nginx 的ngx_cache_purge模块,配置一个内网可访问的清理入口,源站内容变更时主动调用清理接口。
  • 第三层,折中 TTL:业务上确实无法加版本号的图片,把 TTL 从 24h 降到 2h,虽然命中率会掉一点,但内容时效性保住了。
location ~ /purge(/.*) { allow 127.0.0.1; allow 10.0.0.0/8; deny all; proxy_cache_purge img_cache $1$is_args$args; }

这里 $1 获取的是 purge 路径后面的原始 URL,调用时用/purge/fetch/xxx.jpg就可以把对应缓存清掉。需要注意 purge 入口必须做内网 IP 限制,否则任何人都能让你缓存失效,攻击成本太低了。

把上面四个问题处理完,命中率稳定在 96% 到 97.5% 之间,回源流量缩到原来的十分之一。这个数字我盯了很久,它直接等于每月的带宽账单。

5. 鉴权与安全边界:中转层如何避免变成开放代理

图片中转代理有个暗面,容易被忽视:如果你让用户传什么就转发什么,你就等于给全世界开了一个开放代理。流量会被刷、服务器会被当跳板、源站也会被拖下水。这一章聊我怎么做安全边界的。

5.1 源站白名单是第一道闸门

用 map 维护允许代理的源站列表,注意这个列表要放在 http 块里,因为它是一个全局映射。

map $upstream_host $allowed { default 0; images.example.com 1; static.example.net 1; }

然后在/fetch/location 里做双重校验:先解析 URI 拿到源站地址,再判断白名单,不在列表里直接 403。

if ($upstream_target = "") { return 400; } if ($allowed = 0) { return 403; }

这个白名单看起来土,但胜在直观可控。加一个源站就是一行 map,删一个源站也是一行 map,出问题不需要翻大段的正则。

5.2 URL 签名:防止链接被任意篡改

白名单解决了"允许转发到哪些源站",但没解决"谁有权触发这个转发"。如果 URL 没有签名,任何人拼一个/fetch/xxx就能打你的缓存,你的中转层就变成了替别人回源的免费通道。我采用的方案是 Nginx 自带的secure_link模块做签名鉴权。

签名生成很简单,业务后端用密钥对"过期时间戳 + 完整路径"算一个 MD5,然后 base64 编码。大致逻辑:

expires=$(date -d '+10 minutes' +%s) uri="/fetch/images.example.com/photo/2024/05/01.jpg" secret="your_secret_key" token=$(echo -n "${expires}${uri}${secret}" | openssl md5 -binary | openssl base64 | tr '+/' '-_' | tr -d '=') echo "https://img.example.com${uri}?expires=${expires}&token=${token}"

Nginx 侧校验:

location /fetch/ { secure_link $arg_token,$arg_expires; secure_link_md5 "$secure_link_expires$uri$secret"; if ($secure_link = "") { return 403; } ... }

secure_link模块的工作方式:它取 URL 里的token和expires,用配置的secure_link_md5规则重新计算一遍签名,比对一致且时间戳未过期就算通过。签名里的$uri是完整路径,意味着这个链接只能访问它签的这一个资源,改路径签名立刻失效。实际使用中,过期时间我给业务方两个建议档位:静态资源 10 到 30 分钟;需要长期访问的资源,签名有效期给到 30 天,配合缓存效果更好。

5.3 限流与并发控制

即使有签名,依然要防资源耗尽。单个用户疯狂请求大图,会把带宽吃光;一批恶意请求打 MISS,也会把源站打挂。我的做法是两把锁:

limit_req_zone $binary_remote_addr zone=img_req:10m rate=30r/s; server { location /fetch/ { limit_req zone=img_req burst=50 nodelay; } }

rate=30r/s表示单个 IP 平均每秒 30 次请求,burst=50 nodelay允许短时间突发 50 个请求且不排队直接处理。对正常用户来说,30 QPS 足够有余量;对爬虫和攻击者来说,这个阈值很快就能拦住。如果你的业务方诉求更复杂,建议按 URL 维度再限一层,防止某个热点资源被盯上。

5.4 缓存文件权限与目录安全

最后提一个很多人会忽略的小点:缓存目录不要放在/tmp下,也不要用太宽松的权限。缓存目录里是源站返回的原始内容,如果 Nginx 以低权限用户运行(强烈建议),缓存目录的属主就要匹配这个用户,权限设为 700 或 750。之前见过有人图方便把缓存目录设成 777,结果服务器被写入了乱七八糟的文件,排查了很久才发现是这一步的问题。

6. 压测基线与资源调优:性能天花板在哪里

配置调完、上线稳定后,我用压测确认了这套方案的性能基线,也顺便把硬件参数和内核参数拉到合理位置。这里分享两组数据,供你做容量规划时参考。

6.1 压测场景设计

压测不能只测一个纯缓存命中的理想场景,那会给你一个过于乐观的数字。我用的工具是wrk,压测了三组场景,图片体量平均 100KB:

# 场景一:纯缓存命中 wrk -t8 -c200 -d120s --latency https://img.example.com/hot/001.jpg # 场景二:9:1 混合(90% 命中 + 10% 回源),用一批不同 URL 模拟 wrk -t8 -c200 -d300s --latency https://img.example.com/mixed/rand_$n.jpg # 场景三:纯回源(禁用缓存或压一批从未访问过的新 URL) wrk -t8 -c100 -d60s --latency https://img.example.com/new/rand_$n.jpg

压测前要先把热点 URL 预热一遍,否则场景一测出来的其实是回源性能,连场景都不对。

6.2 实测数据与解读

压测环境的配置是 4 核 8G 的云主机、SSD 数据盘、Nginx 主线程 auto。结果大概是这样:

场景并发QPS平均延迟说明
纯缓存命中20045000+3-4ms全部走内存索引 + 磁盘读,瓶颈在网卡
9:1 混合200250008-12ms回源请求拖慢了整体均值和尾部延迟
纯回源100260035-60ms取决于源站响应速度和连接复用效果

需要特别说明,纯命中场景 4.5 万 QPS 看着很高,但这是小图、短连接压测条件下的数字。如果你的图片平均 500KB 以上,瓶颈会转移到磁盘 IO 和带宽,数字会显著下降。真实业务中,我通常按混合场景的 50% 作为长期稳定容量,因为生产环境还有日志写盘、purge 操作、异常流量这些额外开销。

6.3 性能相关的系统调优清单

  • sendfile on、tcp_nopush on、tcp_nodelay on:这几个是标配。sendfile让数据从磁盘到网卡的拷贝绕过用户态内存,省一次拷贝;tcp_nopush让 Nginx 尽量攒满包再发,减少小包数量;tcp_nodelay对延迟敏感的小响应直接发,不等待。
  • 内核参数:fs.file-max调大到 100 万以上,net.core.somaxconn调大到 65535(配合 listen 的 backlog),net.ipv4.ip_local_port_range放宽到1024 65535。最后一项是回源时要大量发起出站连接,端口范围不够会直接报错,这个坑藏得很深,一般压测不到那个量级发现不了。
  • 磁盘:缓存盘优先选 SSD,纯命中场景下 HDD 随机读 4K 的 IOPS 撑不住几万 QPS。机械盘不是不能用,但 max_size 要保守一些,并且要预留至少 20% 的空余空间。
  • 关闭 nginx 的 access_log 不要全关,但高流量时可以只记录非命中请求,也就是 if 条件日志跳过 HIT,能显著减轻写盘压力。

6.4 扩展方向:多级缓存与集群化

单机方案上线后,如果流量继续涨,我建议的演进路径是:先把缓存拆到独立磁盘,避免日志写盘和缓存读写相互干扰;然后加一层内存缓存做前置(可以用 Nginx 的共享内存做小图缓存,或者直接前置一层 SSR 缓存/CDN);最后再上集群,多台 Nginx 通过一致性 hash 分发请求,保证同一个 key 尽量落在同一台机器上,避免每台机器各回各源导致命中率集体下降。

一致性 hash 在 Nginx 层实现比较繁琐,我的建议是把 Nginx 的 IP 哈希负载均衡放在上游接入层,本质上也能达到"同一个 key 尽量去同一台 Nginx"的效果。到那个规模,就需要开始规划缓存增强策略和集群监控了,那是另一个话题。

最后说点实在的。这套 Nginx 图片中转代理方案,技术含量不算高,每一块都是成熟模块的组合,但它最大的价值在于把"图片访问"这个看似简单的事情真正工程化了。我在实际运维中体会最深的一点是:缓存不是什么高深算法,而是对业务访问模式的深刻理解。你越清楚用户怎么访问、源站怎么响应、内容什么时候会变,你的缓存配置就会越精准,命中率自然就上去了。

再分享一个小技巧作为收尾:上线初期先不要急着追求高命中率,把$upstream_cache_status和$request_time完整记到日志里,跑两周之后,你手里会有一份准确的访问模式画像,这时候再去调 TTL、调缓存键、调限流参数,每一步都会有的放矢,而不是靠猜。这套方案能跑得稳,就是靠这种"先把观测做全面,再动手优化"的思路撑起来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询