各位同学,大家好!
今天咱们不开那个传统的、板着个脸的“技术研讨会”,咱们来聊点实用的。我是你们的领路人,在这个服务器和带宽比老婆还难伺候的年代,咱们得学会怎么给服务器“减负”。
咱们今天的话题是:《PHP如何利用Nginx缓存静态资源,让服务器睡个好觉》。
听到“PHP”和“缓存”,你们脑子里可能闪过Redis、Memcached,或者 opcache。对,这些都是好东西,但今天咱们要聊的,更贴近你的日常——Nginx 反向代理缓存。这玩意儿就像是在你的服务器门口安了个“保安”,有人来敲门,保安先问:“这文件你上次来过吗?”如果来过,保安直接把上次打包好的东西扔给他;如果没来过,保安才进屋叫醒睡得正香的PHP。
这就叫“把CPU留给动态计算,把IO留给静态数据”。
咱们先把那些教科书式的废话收起来,直接上干货。假设你是一个拥有几百万用户的站点开发人员,每天凌晨三点,你都会在梦里惊醒,因为服务器CPU飙到了100%。你一查日志,好家伙,全是这帮用户在刷新你的 style.css 和 logo.png。
第一部分:PHP的尴尬处境——别让老黄牛拉豪车
咱们先来剖析一下这个“罪魁祸首”。PHP是什么?PHP是一门优雅的、解释型的、快如闪电的语言。但是,它也是有弱点的。PHP是“即时编译”的,它每次执行一个请求,都要重新解析、编译、执行。这就好比你吃方便面,PHP每次都要自己煮面、加调料、包装,虽然熟练,但要是有人天天只吃面饼,不换口味,你的厨师(PHP进程)也要累吐血。
当用户请求一个静态资源,比如 https://example.com/img/avatar.jpg 时,流程是这样的:
- 用户浏览器:“我要那个头像。”
- Nginx:“收到。把请求转发给PHP。”(因为Nginx默认不知道这是个静态文件,它只知道这是个请求)。
- PHP-FPM:“醒醒!干活了!读取文件系统,读
img/avatar.jpg,读到了,吐出来,再关掉连接。” - Nginx:“收到数据,返回给用户。”
在这个流程里,PHP完成了它不该完成的任务:读文件、输出二进制流、断开连接。如果这个图片有1MB,服务器就要经历一次1MB的IO读取、1MB的数据传输。如果来了1000个用户,就是1000次PHP进程唤醒,1000次文件读取。
这叫什么?这叫“大炮打蚊子”。你养了一条神犬(PHP),结果它只被用来咬拖鞋(静态资源),浪费!资源!
第二部分:Nginx——那个多管闲事的管家
这时候,Nginx站了出来。它说:“嘿,PHP老弟,你别去干苦力了,我顶上!”
Nginx本身就是一个高性能的、内存驱动的、异步的、非阻塞的Web服务器。它的强项是什么?处理并发连接,转发请求。它的底层机制让它读取静态文件的速度比PHP快得多,因为它不需要启动一个解释器来解释代码,它直接在文件系统里找。
但是,默认情况下,Nginx只负责“转发”。它像个不知疲倦的快递员,不管快递里是啥,通通转发。
为了改变这一点,我们要开启Nginx的“反向代理缓存”功能。这不是让Nginx存静态文件(那是open_file_cache的事),而是让Nginx变成一个“智能缓存代理”。
第三部分:配置实战——给Nginx安个大脑
好,咱们开始干。要实现这个功能,咱们得配置Nginx。别怕,代码不难,就是几行配置。
你需要一个“缓存仓库”。咱们在 nginx.conf 的 http 块里定义这个仓库。
http {
# 1. 定义缓存目录和规则
# proxy_cache_path:这是核心命令
# /var/cache/nginx/static_cache:这是缓存文件存放在服务器硬盘上的路径
# levels=1:2:这是目录结构。意思是缓存文件会存放在三级目录下,防止单目录文件过多导致性能下降(文件系统太活跃会卡)。
# keys_zone=my_cache:10m:这是在内存里开一个“索引区”,名字叫 my_cache,大小 10MB。
# max_size=1g:硬盘上最大缓存 1GB,满了之后,得通过LRU算法清理旧的。
# inactive=60m:如果一个文件在这个时间内没被访问,就把它从缓存里干掉。
# use_temp_path=off:直接写入磁盘,不搞中间临时文件,快一点。
proxy_cache_path /var/cache/nginx/static_cache levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;
server {
listen 80;
server_name example.com;
root /var/www/html;
# 2. 在 location 块里激活缓存
location ~* .(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
# 启用缓存,指定使用刚才定义的 my_cache
proxy_cache my_cache;
# 设置缓存键。这是Nginx判断“是不是同一个文件”的依据。
# 这里用 `$request_uri`,也就是完整的URL。如果你文件名带参数,比如 `logo?v=1.0`,缓存键里就会包含 `?v=1.0`。
# 这样你改版了,改了v,Nginx就会认为是新文件,重新去请求PHP。
proxy_cache_key $request_uri;
# 设置缓存多久。200/301/302 状态码缓存 30 分钟。
# $upstream_response_time 是 Nginx 用来计算时间的,虽然有点影响性能,但为了准确性建议加上。
proxy_cache_valid 200 302 301 30m;
# 如果是404,缓存5分钟。防止用户访问不存在的文件频繁打爆硬盘。
proxy_cache_valid 404 5m;
# 3. 关键操作:告诉浏览器,这玩意儿你可以自己存着!
# 这样用户第二次来,直接从浏览器缓存拿,根本不用请求Nginx。
# public:允许公共缓存。
# max-age=31536000:告诉浏览器,这文件一年内别来烦我,我存着呢。
add_header Cache-Control "public, max-age=31536000, immutable";
# 4. 转发请求
# 如果Nginx缓存里没有,就去请求 PHP。
# try_files $uri @php_backend;
# 但因为我们用了 proxy_cache,通常我们会直接 proxy_pass。
proxy_pass http://127.0.0.1:9000; # 这里是你PHP的地址
}
# 5. 如果 Nginx 缓存里没有,或者是404,怎么办?
location @php_backend {
# 这个 location 并不是必须的,如果你不想用 try_files,可以直接在这里写 proxy_pass。
# 关键点:这里不要加 proxy_cache 相关指令,否则就会死循环了。
proxy_pass http://127.0.0.1:9000;
}
}
}
这段代码写了什么?它干了三件事:
- 建仓库:在硬盘上开了个地方存文件。
- 立规矩:告诉Nginx,遇到图片、CSS、JS,去仓库里找,找不到再去叫PHP。
- 发圣旨:告诉浏览器,“这文件留着,一年内别问我要了”。
第四部分:为什么这么做?——深入底层逻辑
现在,我们来聊聊这背后的魔法。
1. 缓存键
你可能注意到我写了 proxy_cache_key $request_uri。这个参数非常关键。
假设你的网站有个静态资源:/static/style.css。
-
缓存键 =
/static/style.css
当用户A请求这个文件,Nginx在内存索引里查到了,命中缓存,直接从硬盘读出扔给A。用户B请求同样的文件,也是命中缓存。 -
如果文件名带了参数呢?
用户A请求:/static/style.css?v=1.0
用户B请求:/static/style.css?v=2.0如果你的缓存键是
$request_uri,那么:- 用户A的缓存键:
/static/style.css?v=1.0 - 用户B的缓存键:
/static/style.css?v=2.0
结果是什么?Nginx发现这两个键不一样,于是A和B都会被送到PHP那里去生成。
为什么? 因为你的PHP文件里可能写了逻辑判断:如果参数是2.0,就返回新版样式。如果你把两个版本混在同一个缓存里,浏览器拿到旧版样式就露馅了。 - 用户A的缓存键:
所以,动态参数必须区分缓存键。当然,你也可以用 $cookie_version 来做缓存键,把版本号放到Cookie里,这样只要Cookie不变,缓存就有效。
2. 缓存状态
你开启缓存后,怎么知道它到底有没有在生效?这是运维的命门。
我们在配置里加一句:add_header X-Cache-Status $upstream_cache_status;
这行代码会给HTTP响应头加个标签。当用户访问你的网站时,F12看Network面板,你会看到三种状态:
HIT:恭喜!这文件Nginx存着呢,直接从磁盘读的。这就是咱们的目标。MISS:哎呀,Nginx没存过,去PHP那边重新生成的,然后把结果存起来。BYPASS:有些特殊请求(比如带Token的),我们强制绕过缓存,直接去PHP。
你监控的时候,盯着 HIT 的比例。如果这个比例常年很低,说明缓存策略有问题(比如缓存时间太短,或者全是动态文件)。
3. 浏览器缓存 vs 服务器缓存
这里有个概念必须理清。
- 服务器缓存(Nginx):是服务器端的缓存。用户第一次来,服务器生成并保存;第二次来,服务器直接从自己硬盘给用户。这能减少服务器CPU和带宽消耗。
- 浏览器缓存:是客户端的缓存。用户第一次来,服务器告诉浏览器“存起来,一年别忘”;第二次来,用户直接从自己电脑里拿,根本不经过服务器。这能减少网络传输。
咱们上面的配置里,两个都开了。
add_header Cache-Control ... 是告诉浏览器。
proxy_cache 是告诉Nginx。
这叫双保险。浏览器是最后一道防线,Nginx是中间防线。用户量一大,如果没有Nginx缓存,每个用户第一次都要请求PHP,瞬间就能把PHP搞崩;有了Nginx,只有“第一次访问”的人会请求PHP,后面的人全是Nginx和浏览器在配合。
第五部分:高级技巧与陷阱
光会复制粘贴配置是不够的,资深专家得知道坑在哪。
陷阱一:Nginx和PHP的路径不一致
这是新手最容易踩的坑。你的PHP代码里,__DIR__ 是指PHP代码所在的目录,不是Nginx root目录。
如果你用 try_files $uri @php_backend;,Nginx找不到文件,会转发给PHP。PHP再去找文件。如果路径配错了,PHP就会报404。
- 正确姿势:使用绝对路径或者配置
root指令,确保Nginx知道文件在哪,PHP也知道文件在哪。或者,对于静态资源,直接让Nginx处理,不要转发给PHP。
陷阱二:图片的修改时间
如果你的PHP程序有功能,可以后台修改图片(比如加水印、裁剪),然后保存为新文件,比如 avatar_new.jpg。这时候,如果你用 $request_uri 做缓存键,而且前端没有强制刷新,浏览器可能会请求旧的 avatar.jpg。
如果Nginx里有 avatar.jpg 的缓存,它就会把旧的返回,哪怕服务器上已经不存在这个文件了(或者存的是新文件)。
- 解决方案:确保你的静态资源命名规则能体现版本号变化,或者利用
ETag和Last-Modified。Nginx在proxy_cache_valid里其实已经处理了部分逻辑,但如果文件被删除了,Nginx会存404。
陷阱三:缓存穿透
如果有人恶意扫描你的网站,请求一堆不存在的图片链接。Nginx会去PHP查,查不到返回404,但Nginx也会把404结果缓存起来。
如果这个404请求量巨大,Nginx会把这些404响应写进缓存目录,占满硬盘。
- 解决方案:
proxy_cache_valid 404 5m;。给404设置很短的过期时间,比如5分钟。5分钟后,再有人请求不存在的文件,Nginx再去问PHP。这样就保护了服务器。
第六部分:PHP里的那些“坏习惯”
咱们光优化Nginx还不够,PHP开发者自己也得改改坏毛病。
坏习惯1:绝对路径
很多PHP开发者喜欢写:
include_once '/var/www/html/inc/header.php';
如果你换了服务器,或者用Docker部署,这路径就挂了。而且,每次include,PHP都要在硬盘上找这个绝对路径。虽然OS有文件系统缓存,但不如相对路径或者项目根目录的常量来得快。
坏习惯2:动态加载CSS/JS
别在HTML里写:
<link rel="stylesheet" href="style.php?theme=dark">
这是给自己找罪受。把CSS编译成 style.css 放在Nginx目录下,用Nginx缓存,性能提升是指数级的。如果你非要用PHP处理CSS,至少加上 Expires 头。
坏习惯3:万物皆数据库
有时候为了方便,有人把文章的评论列表、甚至是网站的基础配置,都存在数据库里。然后每次加载首页,都要连库查一下“网站名称是什么”、“最新新闻是什么”。
拜托,这些是静态数据!存Redis可以,存文件也行。别动不动就 SELECT * FROM config。
第七部分:性能对比——数据不会骗人
为了让大家信服,咱们来做个模拟实验。
场景:一个日均PV(页面浏览量)10万的网站,里面有100个静态资源(图片、JS、CSS)。每个文件平均200KB。
方案A:没有缓存(纯PHP输出)
- 10万次访问 = 10万次PHP请求。
- PHP处理每个请求的时间假设是 10ms(这已经很快了)。
- 总耗时:10万 * 10ms = 1000秒 = 16.6分钟。
- 带宽消耗:10万 * 200KB = 20GB。
- 结果:CPU飙升,数据库压力巨大,用户体验差(加载慢)。
方案B:只开启Nginx静态文件服务(不缓存)
- Nginx直接读取文件。
- 10万次访问 = 10万次Nginx读取。
- Nginx读取文件的时间假设是 1ms(比PHP快10倍)。
- 总耗时:10万 * 1ms = 100秒 = 1.6分钟。
- 带宽消耗:20GB。
- 结果:比方案A快10倍,但服务器IO还是很高。
方案C:开启Nginx反向代理缓存 + 浏览器缓存
- 第一个用户访问:Nginx MISS,请求PHP,缓存保存。
- 第二个到第十万个用户:Nginx HIT,直接给文件。
- 总耗时:主要是第一个用户的 MISS 和生成缓存的时间。
- 带宽消耗:第一个用户20KB(只要头),后面9.9万用户0KB(浏览器缓存)。
- 结果:服务器几乎闲置,带宽消耗极低,用户体验完美。
看到没?这就是缓存的威力。它不是简单的“加速”,它是“省电”。
第八部分:如何排查缓存问题
当你部署了Nginx缓存,发现性能没有提升,怎么办?
第一步:看Nginx错误日志。
tail -f /var/log/nginx/error.log
看看有没有 open() "/var/cache/nginx/..." failed 之类的报错。是不是权限问题?是不是磁盘满了?
第二步:看缓存目录。
ls -lh /var/cache/nginx/static_cache/
看看有没有文件。如果目录是空的,说明缓存没生效。
第三步:看缓存状态头。
curl -I http://your-domain.com/style.css
看到 X-Cache-Status: HIT 了吗?如果没有,检查配置是不是加在了错误的位置。很多新手喜欢把 proxy_cache 加在 server 块里,其实应该在 location 块里。
第九部分:总结与升华
好了,讲了这么多,咱们总结一下。PHP是个好语言,但它不是万能的。你的PHP进程是昂贵的资源,应该留给那些需要实时计算、需要数据库交互、需要复杂逻辑的动态请求。
静态资源是什么?它们是死数据,是不变的真理。它们不需要每次都经过PHP的轮回,不需要每次都消耗CPU周期。
Nginx缓存,就是给你的静态资源建立了一个“永久居住证”。
- 对于浏览器,它是“请勿打扰”的牌子。
- 对于服务器,它是“省电模式”的开关。
当你把这个配置写进 nginx.conf,重启服务的那一刻,你会感觉到一种前所未有的轻松。你的服务器不再嘶吼,你的带宽账单不再跳动,你的PHP进程终于可以睡个安稳觉了。
记住,优秀的程序员不是把代码写得最复杂的,而是懂得“偷懒”——让系统自己去处理重复的事情,让基础设施自己去承担它擅长的重担。
这就是今天要讲的所有内容。去吧,去优化你的服务器,去拯救那些因为带宽不足而哭泣的运维同事。
谢谢大家!