PHP 504 超时定位排查指南
快速定位流程图
504 错误发生
↓
1. 确认是哪个环节超时?
├─ Nginx 层超时
├─ PHP-FPM 层超时
├─ 外部请求超时(数据库/API/文件)
└─ 业务逻辑死循环/慢查询
↓
2. 查看错误日志
├─ Nginx error.log
├─ PHP-FPM error.log
└─ 应用日志(laravel.log等)
↓
3. 定位具体代码
├─ 访问日志时间分析
├─ Xdebug 或 Trace 追踪
└─ 分段记录执行时间
各层级超时配置与排查
Nginx 层(最常见的 504 来源)
# nginx.conf 或站点配置
server {
# 读取请求超时(默认60s)
client_body_timeout 30s;
# 发送响应超时(默认60s)
send_timeout 30s;
# 代理超时(PHP-FPM)
proxy_connect_timeout 30s;
proxy_read_timeout 120s;
proxy_send_timeout 60s;
# FastCGI 超时(PHP-FPM)
fastcgi_connect_timeout 30s;
fastcgi_read_timeout 120s;
fastcgi_send_timeout 30s;
}
排查方法:

# 查看 Nginx 错误日志 tail -f /var/log/nginx/error.log # 查看访问日志,找 504 状态码 grep "504" /var/log/nginx/access.log | tail -20
PHP-FPM 层
# php.ini max_execution_time = 30 max_input_time = 60 # php-fpm.conf 或 www.conf request_terminate_timeout = 60 request_slowlog_timeout = 10 slowlog = /var/log/php-fpm/slow.log
排查方法:
# 查看 PHP-FPM 错误日志 tail -f /var/log/php-fpm/error.log # 查看慢日志(非常有用) cat /var/log/php-fpm/slow.log # 查看当前 PHP-FPM 进程状态 ps aux | grep php-fpm
应用层定位(核心步骤)
添加执行时间日志
// 在入口文件(如 index.php)开头
$start_time = microtime(true);
$start_memory = memory_get_usage();
// 在文件末尾
file_put_contents('/tmp/php_timing.log',
date('Y-m-d H:i:s') . ' | Time: ' .
(microtime(true) - $start_time) . 's | Memory: ' .
(memory_get_usage() - $start_memory) / 1024 . 'KB\n',
FILE_APPEND
);
分步记录(定位到具体函数)
// 创建一个日志辅助函数
function debug_trace($msg) {
file_put_contents('/tmp/debug.log',
date('H:i:s') . ' | ' . $msg . ' | Memory: ' .
memory_get_usage()/1024/1024 . 'MB\n',
FILE_APPEND);
}
// 在业务代码中分段记录
debug_trace('开始执行');
$result = $db->query("SELECT * FROM big_table"); // 这里可能卡住
debug_trace('数据库查询完成');
$result2 = call_external_api(); // 这里可能卡住
debug_trace('外部API调用完成');
使用 Xdebug 构建火焰图
# php.ini 配置 Xdebug xdebug.mode = profile xdebug.output_dir = /tmp/xdebug xdebug.max_nesting_level = 512
# 生成性能分析文件后,使用工具分析 # 安装 Webgrind 查看 git clone https://github.com/jokkedk/webgrind.git # 将 webgrind/index.php 放到 web 目录,它会显示调用时间和次数
常见原因及解决方案
数据库慢查询
// 优化前
$users = $db->query("SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 YEAR");
// 优化后:添加索引
DB::statement('ALTER TABLE users ADD INDEX idx_created_at (created_at)');
// 使用分页代替全量查询
$users = $db->select("SELECT * FROM users LIMIT 100 OFFSET 0");
检查数据库:
-- 查看当前正在执行的查询 SHOW PROCESSLIST; -- 查看慢查询日志 SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';
外部 API 调用超时
// 设置超时时间
$options = [
'timeout' => 10, // 10秒超时
'connect_timeout' => 5,
];
// Guzzle
$client = new GuzzleHttp\Client($options);
// Curl
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
// 最佳实践:使用异步或添加重试机制
循环执行时间过长
// 优化前:全量处理
foreach ($huge_array as $item) {
process($item); // 耗时操作
}
// 优化后:分批处理 + 分割时间
$chunks = array_chunk($huge_array, 100);
$processing_time = 0;
foreach ($chunks as $chunk) {
$start = microtime(true);
foreach ($chunk as $item) {
process($item);
}
$processing_time += (microtime(true) - $start);
// 如果用时过长,检查是否要在中间保存状态
if ($processing_time > 25) {
break; // 为避免超时,提前结束并保存进度
}
}
大文件处理
// 优化前:全部读入内存
$data = file_get_contents('large_file.csv');
process($data);
// 优化后:流式处理
$handle = fopen('large_file.csv', 'r');
while (($row = fgetcsv($handle)) !== false) {
process($row); // 逐行处理
}
fclose($handle);
监控与预防
实时监控脚本
#!/bin/bash
# monitor.sh - 实时监控 PHP-FPM 超时
while true; do
clear
echo "=== 当前 PHP-FPM 进程 ==="
ps aux | grep php-fpm | grep -v grep
echo -e "\n=== 最近1分钟超时请求 ==="
tail -50 /var/log/nginx/error.log | grep "timeout"
sleep 5
done
设置告警
// 在应用中加入超时告警
if ((microtime(true) - $start_time) > 25) {
// 发送邮件或钉钉通知
email_admin(
'Performance Alert',
"Request took " . (microtime(true) - $start_time) . " seconds"
);
}
常用排查命令汇总
# 查看 PHP 进程执行时间 ps -eo pid,etime,comm | grep php-fpm # 实时查看慢请求 watch -n 1 "grep 504 /var/log/nginx/access.log | tail -5" # 查看数据库连接 mysql -e "SHOW PROCESSLIST;" # 查看系统负载 uptime top -bn1 | head -20 # 监控网络连接 netstat -pant | grep :80
最佳实践建议
-
分层设置超时:Nginx (60s) > PHP-FPM (30s) > 数据库查询 (5s) > 外部API (2s)
-
统一超时处理:在应用入口设置全局超时拦截
// Bootstrap 文件中
set_time_limit(55); // 略小于 PHP-FPM 的 60s
register_shutdown_function(function() {
if (connection_aborted()) {
error_log('Request timed out at: ' . $_SERVER['REQUEST_URI']);
}
});
-
使用消息队列:对于长时间任务,改用异步处理(如 RabbitMQ、Redis Queue)
-
合理拆分功能:大查询改为小批量多次查询
-
定期优化:使用 OPcache、优化 SQL、使用缓存(Redis/Memcached)
通过以上方法,可以快速定位 504 超时的原因,并根据具体情况进行优化,记住关键点是:先看日志,再改配置,最后验证效果。