前言
今天花了一整个下午排查这个博客的性能问题。症状很简单——每次首次打开页面,至少要等 5-10 秒。静态资源?秒加载。问题 100% 出在服务端。
这篇文章完整记录了从发现问题到最终解决的全过程,包括走过的弯路。
环境背景
先交代一下这个博客的部署架构:
- 框架:Laravel 10 + SQLite
- 运行环境:WSL2 Ubuntu 22.04
- Web 服务器:Nginx + PHP-FPM
- 文件位置:Windows 磁盘
D:\projects\my-blog(在 WSL 中挂载为/mnt/d/...) - 访问方式:
https://your-domain.com/blog
注意到关键信息了吗?PHP 文件存储在 Windows 磁盘上,而 PHP 运行在 WSL2 内。
第一阶段:前端排查(走弯路)
最开始怀疑是前端资源加载慢。打开 F12 一看,页面引用了 7 个 CDN 资源:
- Bootstrap CSS/JS
- highlight.js CSS/JS
- marked.js
- DOMPurify
- medium-zoom
于是动手把这些 CDN 全部下载到本地 public/vendor/ 目录,修改 frontend.blade.php 模板,把 CDN URL 替换为 {{ asset('vendor/...') }},并给所有 <script> 标签加上 defer 属性。
结果:静态资源确实快了,但页面加载时间毫无变化。
然后看了一眼浏览器的时间线瀑布图——文档请求本身就用了 8.7 秒,所有静态资源在 11-35ms 内加载完毕。问题根本不在前端。
第二阶段:后端代码优化
接着排查后端代码,发现了几个性能问题:
1. Setting Model 的 N+1 查询
Setting::getValue() 每次调用都会执行一次 DB 查询。一个典型页面调用 3 次以上(获取站名、分页数、描述等)。加了缓存:
public static function getValue($key, $default = null)
{
$settings = Cache::remember('app.settings', 3600, function () {
return static::pluck('value', 'key')->toArray();
});
return $settings[$key] ?? $default;
}
2. TagCloud 每次请求都加载全部文章
buildTagCloud() 方法会从数据库加载所有文章来统计标签。同样加了缓存:
$tagCloud = Cache::remember('blog.tag_cloud', 3600, function () {
// ... 原有的标签统计逻辑
});
3. Session 驱动用的是 file
在 WSL2 中,file session 意味着每次请求都要通过 9P 协议读写 Windows 磁盘上的 session 文件。改为 cookie。
4. 日志级别是 debug
LOG_LEVEL=debug 产生大量日志写入。改为 error。
结果:这些优化是好的,但并非性能瓶颈的根因。
第三阶段:找到真凶——OPcache + 9P 协议
这一步才是关键发现。
查看 OPcache 配置:
opcache.validate_timestamps = On
opcache.revalidate_freq = 2
这意味着什么?每 2 秒,OPcache 会检查所有已缓存的 PHP 文件是否有更新。一个 Laravel 项目有 100+ 个 PHP 文件(框架本身 + vendor 依赖)。
而这些文件全部存储在 /mnt/d/——WSL2 通过 9P 协议访问 Windows 文件系统。9P 的文件 stat 操作比原生 ext4 慢 10-50 倍。
算一笔账:
- 100 个文件 × stat 操作 × 9P 延迟 = 每次请求额外花费好几秒在文件检查上
修复
创建 /etc/php/X.Y/fpm/conf.d/99-performance.ini:
[opcache]
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.enable_file_override=1
其中 validate_timestamps=0 是最关键的——告诉 OPcache 永远不要检查文件是否更新。一旦 PHP 文件被编译缓存,就一直用缓存版本,直到 PHP-FPM 重启。
重启 PHP-FPM 后……
第四阶段:改炸了
优化完信心满满地刷新页面——浏览器显示出了 index.php 的原始 PHP 源代码。
好家伙,把站改炸了。
排查过程又开始了:
- PHP-FPM 运行正常 ✓
- Nginx 配置语法正确 ✓
- 直接访问
.php文件没问题 ✓ - 但
/nblog/路由返回 405 Method Not Allowed
Nginx 配置的坑
这个博客跑在 /nblog/ 子路径下,Nginx 用的是 alias 而不是 root。alias + try_files + index 的组合在 Nginx 中有一个经典的坑:
location /nblog {
alias /mnt/d/projects/my-blog/public;
index index.php;
try_files $uri $uri/ @nblog;
}
当访问 /nblog/ 时:
try_files检查$uri(即/nblog/)alias把它映射到public/目录——目录存在!- 匹配成功,Nginx 找到
index.php - 但它把
index.php作为静态文件返回,而不是转发给 PHP-FPM
于是就看到了原始 PHP 源码。
折腾了 7 个版本的 Nginx 配置
从 _patch_nginx.py 写到 _patch_nginx7.py,尝试了各种组合:
- 嵌套 PHP location ❌
- 去掉
$uri/❌ - 去掉
index指令 ❌ - 各种 rewrite 规则 ❌
每次都是 nginx -t 通过,nginx -s reload,然后 wget 测试——依然 405。
最后的真相
在 index.php 里加了调试日志:
file_put_contents('/tmp/laravel_debug.log',
date('Y-m-d H:i:s') . " METHOD=" . $request->getMethod() . "\n",
FILE_APPEND);
刷新页面——日志文件根本不存在。index.php 压根没被执行。
等等……我不是设了 opcache.validate_timestamps=0 吗?
就是这个! 我修改了 index.php 加了调试代码,但 OPcache 缓存的还是旧版本的 index.php。之前做 php artisan optimize 生成了路由缓存,后来 route:clear 清除了路由缓存文件,但 OPcache 里缓存的 PHP 字节码还是旧的(包含旧路由逻辑的版本)。
一个 sudo service phpX.Y-fpm restart 就解决了一切。
而那 7 个 Nginx 补丁?其实第一个版本就是对的,Nginx 配置从头到尾都没问题。是 OPcache 的旧缓存在作怪。
最终结果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 页面加载时间(热缓存) | 8.7 秒 | 0.4 秒 |
| 提升倍数 | — | ~22 倍 |
经验总结
1. 先看瀑布图再优化
浏览器截图直接告诉我:文档请求 8.7 秒,静态资源 11ms。根本不需要在前端上浪费时间。
2. WSL2 + /mnt/d/ = 9P 性能陷阱
任何跨文件系统操作都会变慢;频繁的文件 stat、session 读写、日志写入——全部受影响。如果项目必须放在 Windows 磁盘上,务必关闭 OPcache 文件校验。
3. validate_timestamps=0 是双刃剑
关了它性能飞升,但代价是:修改 PHP 代码后必须手动重启 PHP-FPM。不然你改的代码根本不会生效——我自己就因为这个调试了好久。
4. 调试时要记得你做过什么优化
我花了大量时间调试 Nginx 配置,实际上 Nginx 一直是好的。真正的问题是我自己设的 validate_timestamps=0 导致 OPcache 不加载新代码。改了 7 个版本的 Nginx 补丁,不如一个 service phpX.Y-fpm restart。
5. 完整的优化清单
- OPcache
validate_timestamps=0(最大收益) - OPcache 内存调至 256MB
- CDN 资源本地化
- JS 添加
defer - Setting 查询加
Cache::remember - TagCloud 查询加
Cache::remember - Session driver 改为 cookie
- Log level 改为 error
下午 debug 了三小时,核心修复只需要一行配置。但没有前面那三小时的排查,你不会知道该改哪一行。
💬 评论 (0)
暂无评论,来说第一句话吧~
发表评论