前言
最近在宝塔面板部署了一个ThinkPHP6的卡盟系统,遇到了一个极其诡异的问题:关闭防火墙时网站秒开,开启防火墙后首页正好卡30秒才能加载完成。静态资源(图片、JS、CSS)都能正常快速加载,只有PHP动态请求慢得离谱。
这个问题前后折腾了我整整一天,从Nginx配置改到PHP-FPM,从日志写入排查到框架初始化,最后才发现问题竟然出在一个最不起眼的地方。今天把整个排查过程和解决方案分享出来,希望能帮到遇到同样问题的朋友。
问题现象
先完整描述一下我遇到的所有现象,如果你也遇到了一模一样的情况,那这篇文章就是为你写的:
- 防火墙开启:首页加载时间正好30秒,静态资源加载正常,最终能看到页面内容
- 防火墙关闭:网站秒开,所有请求都在几百毫秒内完成
- 极简PHP测试页:新建一个只输出
echo "test";的PHP文件,无论防火墙是否开启都秒开 - 数据库查询:调试信息显示SQL查询数为0,但页面就是卡30秒
- 错误日志:Nginx和PHP都没有报错,只有PHP慢日志记录了30秒的超时
第一阶段:误以为是PHP-FPM通信问题
最开始我遇到的是502错误,和大多数人一样,第一反应就是PHP-FPM和Nginx的通信出了问题。
尝试的解决方案
- 修改PHP-FPM监听方式:把默认的Unix Socket改成TCP端口
127.0.0.1:9074 - 同步修改Nginx配置:把
fastcgi_pass从unix:/tmp/php-cgi-74.sock改成127.0.0.1:9074 - 放行本地回环流量:在宝塔防火墙里添加规则,放行
127.0.0.1的所有通信
结果
502错误解决了,但新的问题出现了:网站不再报错,但首页正好卡30秒才能加载完成。
第二阶段:怀疑是日志写入阻塞
看到页面卡30秒,我又开始怀疑是ThinkPHP的日志写入导致的阻塞,因为调试信息显示流程卡在了think/log/Channel.php。
尝试的解决方案
- 关闭调试模式:把
.env里的APP_DEBUG改成false - 优化日志配置:关闭文件锁,只记录错误和警告日志
- 禁用日志写入:把日志驱动改成
test,不写入磁盘
结果
页面还是卡30秒,没有任何改善。这时候我意识到,日志写入只是表象,不是根本原因。
第三阶段:开启PHP慢日志,定位真正的阻塞点
在尝试了所有能想到的方法都无效后,我决定开启PHP慢日志,这是定位PHP性能问题的终极武器。
宝塔开启PHP慢日志的方法
- 进入宝塔面板 → 软件商店 → PHP 7.4 → 设置
- 点击左侧「日志」标签,找到「慢日志」区域
- 开启慢日志,设置超时时间为1秒
- 保存并重启PHP服务
慢日志揭示的真相
开启慢日志后,我刷新了一下页面,立刻看到了这样的记录:
[28-May-2026 08:41:53] [pool www] pid 392951
script_filename = /www/wwwroot/www.oniuniu.com/public/index.php
[0x00007ff2a7018ea0] __construct() /www/wwwroot/www.oniuniu.com/vendor/topthink/think-orm/src/db/PDOConnection.php:594
[0x00007ff2a7018df0] createPdo() /www/wwwroot/www.oniuniu.com/vendor/topthink/think-orm/src/db/PDOConnection.php:555
[0x00007ff2a70189d0] connect() /www/wwwroot/www.oniuniu.com/vendor/topthink/think-orm/src/db/PDOConnection.php:1705
...
所有30秒超时的请求,全部卡在了PDO数据库连接的构造函数上!
问题根源
看到慢日志的那一刻,我恍然大悟。完整的问题逻辑链是这样的:
- 路由阶段触发数据库查询:我的
route.php第27行调用了sys_config()函数,这个函数会去数据库查询系统配置 - 防火墙拦截数据库端口:宝塔防火墙默认没有放行MySQL的3306端口,开启防火墙后,PHP无法连接到数据库
- PDO默认连接超时30秒:PHP会一直等待数据库响应,直到30秒超时
- 超时后继续执行:连接超时后,程序抛出错误,但继续执行后面的代码,所以最终能看到页面,但已经过了30秒
- 关闭防火墙就好:关闭防火墙后,数据库连接成功,页面秒开
完整解决方案
1. 宝塔防火墙放行MySQL端口(核心解决)
这是最直接也是最关键的一步:
- 进入宝塔面板 → 安全 → 系统防火墙
- 点击「添加端口规则」
- 按以下内容填写:
- 协议:TCP
- 端口:3306
- 来源:127.0.0.1(只允许本地连接,更安全)
- 策略:放行
- 备注:MySQL本地连接
- 点击右上角「清理缓存」
2. 优化数据库配置
打开项目根目录的.env文件,确保数据库配置正确:
DB_CONNECTION = mysql
DB_HOST = 127.0.0.1 # 一定要用127.0.0.1,不要用localhost
DB_PORT = 3306
DB_DATABASE = 你的数据库名
DB_USERNAME = 你的数据库用户名
DB_PASSWORD = 你的数据库密码
DB_CHARSET = utf8mb4
注意:一定要用127.0.0.1而不是localhost,因为localhost会走Unix Socket,而127.0.0.1走TCP端口,和防火墙规则匹配。
3. 设置PDO连接超时(避免30秒卡死)
即使数据库连接失败,也不要让用户等30秒。修改config/database.php,添加PDO超时参数:
return [
'default' => env('DB_CONNECTION', 'mysql'),
'connections' => [
'mysql' => [
'type' => 'mysql',
'hostname' => env('DB_HOST', '127.0.0.1'),
'database' => env('DB_DATABASE', ''),
'username' => env('DB_USERNAME', ''),
'password' => env('DB_PASSWORD', ''),
'hostport' => env('DB_PORT', '3306'),
'charset' => 'utf8mb4',
'prefix' => '',
// 添加这两行,设置连接超时为3秒
'params' => [
\PDO::ATTR_TIMEOUT => 3,
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
],
],
],
];
4. 最佳实践:避免路由阶段查询数据库
在路由加载阶段就查询数据库是非常不好的实践,会导致所有请求都必须先连接数据库。建议把数据库查询移到控制器里,并添加缓存:
// 优化前(路由阶段查询)
Route::get('/', function () {
$config = sys_config('site_name');
return view('index', ['config' => $config]);
});
// 优化后(控制器查询+缓存)
Route::get('/', 'IndexController@index');
// IndexController.php
public function index()
{
$config = cache('site_config', function () {
return sys_config('site_name');
}, 3600); // 缓存1小时
return view('index', ['config' => $config]);
}
总结
这次踩坑让我深刻体会到,排查问题一定要从现象出发,不要盲目猜测。以下是我总结的几点经验:
- PHP慢日志是神器:遇到PHP性能问题,第一时间开启慢日志,它能直接告诉你哪一行代码卡住了
- 不要忽略防火墙:很多看似复杂的问题,其实只是防火墙拦截了某个端口
- 数据库连接要加超时:永远不要让用户等待30秒才看到错误
- 避免在路由阶段执行耗时操作:路由应该只负责请求分发,业务逻辑放在控制器里
希望这篇文章能帮到遇到同样问题的朋友。如果你觉得有用,欢迎点赞收藏,也可以在评论区留言交流。
