macOS 环境下 PHP-FPM 连接 PostgreSQL 时崩溃(502 Bad Gateway)的排查与解决
问题描述
在 macOS 开发环境中,使用 Homebrew 安装的 PHP 8.2 + PHP-FPM 连接远程 PostgreSQL 数据库时,频繁出现 502 Bad Gateway 错误。
症状特征
- CLI 模式完全正常:
php artisan tinker或直接运行 PHP 脚本都能成功连接 - PHP-FPM 模式崩溃:通过 Nginx 访问时,PHP-FPM worker 进程会崩溃
- Linux 服务器正常:相同的代码在 Linux 生产服务器上运行完全正常
- 间歇性发生:有时能成功,有时失败,重启 PHP-FPM 后短暂恢复
- 并发时更容易触发:多个请求同时访问时崩溃概率更高
环境信息
- macOS: 26.2 (Apple Silicon M1)
- PHP: 8.2.30 (Homebrew)
- PostgreSQL 客户端库: libpq 18.1 (Homebrew)
- PostgreSQL 服务器: 16.10 (远程 Linux 服务器)
- OpenSSL: 3.6.0 (Homebrew)
- Web 服务器: Nginx 1.27.0
- Xdebug: 3.5.0
排查过程
第一步:收集错误信息
1.1 查看 PHP-FPM 日志
tail -f /opt/homebrew/var/log/php-fpm.log
发现大量类似的错误:
[06-Feb-2026 18:40:45] WARNING: [pool www] child 35200 exited on signal 6 (SIGABRT) after 1.099195 seconds from start
[06-Feb-2026 18:40:45] NOTICE: [pool www] child 35219 started
关键信息:Worker 进程收到 SIGABRT 信号后崩溃,PHP-FPM 自动重启新的 worker。
1.2 查看 Nginx 错误日志
tail -f /opt/homebrew/var/log/nginx/error.log
kevent() reported about an closed connection (54: Connection reset by peer)
while reading response header from upstream, upstream: "fastcgi://127.0.0.1:9082"
关键信息:Nginx 在等待 PHP-FPM 响应时,连接被重置 —— 说明 PHP-FPM worker 进程意外终止。
第二步:验证问题范围
2.1 测试简单 PHP 请求
<?php
// test_simple.php
echo "OK\n";
echo "PID: " . getmypid() . "\n";
curl http://localhost/test_simple.php
# 输出: OK, PID: 12345 ✓ 正常
结论:普通 PHP 请求正常,问题与 PostgreSQL 连接相关。
2.2 测试 CLI 模式 PostgreSQL 连接
php -r "new PDO('pgsql:host=192.168.3.18;port=5432;dbname=test', 'user', 'pass');"
# 输出: (无错误) ✓ 正常
结论:CLI 模式连接正常,问题仅在 PHP-FPM 模式下出现。
2.3 测试 PHP-FPM 模式 PostgreSQL 连接
<?php
// test_pgsql.php
$pdo = new PDO('pgsql:host=192.168.3.18;port=5432;dbname=test', 'user', 'pass');
echo "连接成功";
curl http://localhost/test_pgsql.php
# 输出: 502 Bad Gateway ✗ 崩溃
结论:问题确认为 PHP-FPM 模式下连接 PostgreSQL 时崩溃。
第三步:排除常见原因
3.1 假设:Xdebug 扩展冲突
Xdebug 在某些情况下会与其他扩展产生冲突。
# 临时禁用 Xdebug
mv /opt/homebrew/etc/php/8.2/conf.d/ext-xdebug.ini /opt/homebrew/etc/php/8.2/conf.d/ext-xdebug.ini.bak
brew services restart php@8.2
# 测试
curl http://localhost/test_pgsql.php
# 结果: 仍然 502 ✗
结论:排除 Xdebug 原因,恢复配置。
3.2 假设:OpenSSL 版本冲突
系统中同时存在 OpenSSL 1.1 和 3.x,可能存在库冲突。
# 检查 PHP 使用的 OpenSSL
php -r "echo OPENSSL_VERSION_TEXT;"
# 输出: OpenSSL 3.6.0
# 检查 libpq 链接的 OpenSSL
otool -L /opt/homebrew/opt/libpq/lib/libpq.dylib | grep ssl
# 输出: /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib
结论:PHP 和 libpq 都使用 OpenSSL 3.x,版本一致,排除此原因。
3.3 假设:libpq 版本问题
尝试切换 libpq 版本。
# 检查已安装版本
brew list --versions libpq
# 输出: libpq 18.1 17.5
# 两个版本都链接相同的 OpenSSL 3.x
otool -L /opt/homebrew/Cellar/libpq/17.5/lib/libpq.5.dylib | grep ssl
otool -L /opt/homebrew/Cellar/libpq/18.1/lib/libpq.5.dylib | grep ssl
# 结果: 都是 openssl@3
结论:libpq 是编译时链接的,运行时切换版本无效,排除此原因。
3.4 假设:PHP-FPM 进程管理模式问题
尝试不同的进程管理模式。
; 测试 static 模式
pm = static
pm.max_children = 5
; 测试 ondemand 模式
pm = ondemand
pm.max_children = 5
pm.process_idle_timeout = 10s
结论:所有模式都会崩溃,排除进程管理模式原因。
3.5 假设:并发初始化竞争条件
测试串行预热 vs 并发请求。
# 串行预热(每个 worker 单独初始化)
for i in {1..5}; do curl http://localhost/test_pgsql.php; sleep 0.5; done
# 结果: 部分成功,部分 502
# 并发请求
for i in {1..5}; do curl http://localhost/test_pgsql.php & done; wait
# 结果: 大部分 502
发现:并发时崩溃率更高,但串行也会崩溃。这提示问题与进程初始化相关。
第四步:深入分析崩溃原因
4.1 查找 macOS 崩溃报告
ls -lt /Library/Logs/DiagnosticReports/ | grep php
找到崩溃报告文件,查看关键信息:
{
"exception": {
"type": "EXC_CRASH",
"signal": "SIGABRT"
},
"asi": {
"CoreFoundation": ["*** multi-threaded process forked ***"],
"libsystem_c.dylib": ["crashed on child side of fork pre-exec"]
}
}
关键发现:multi-threaded process forked 和 crashed on child side of fork pre-exec
4.2 分析崩溃堆栈
完整的调用链:
pdo_pgsql_handle_factory ← PHP PDO 创建连接
→ PQconnectdb ← libpq 连接函数
→ select_next_encryption_method
→ pg_GSS_have_cred_cache ← 检查 GSS/Kerberos 凭证 ⚠️
→ gss_acquire_cred
→ krb5_cccol_have_content
→ krb5_init_context_flags
→ init_context_from_config_file
→ CFPreferencesCopyAppValue ← 调用 CoreFoundation API ❌
→ *** SIGABRT ***
根因定位:libpq 在连接时会检查 Kerberos 凭证,这个过程调用了 macOS 的 CoreFoundation API,而 CoreFoundation 在 fork 后的子进程中是不安全的。
根本原因
技术原理
这是 macOS fork 安全机制 与 libpq GSS/Kerberos 认证检查 之间的冲突。
1. PHP-FPM 的工作模式
┌─────────────────┐
│ PHP-FPM Master │
│ (PID 100) │
└────────┬────────┘
│ fork()
┌────┴────┬────────┬────────┐
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│Worker1│ │Worker2│ │Worker3│ │Worker4│
│PID 101│ │PID 102│ │PID 103│ │PID 104│
└───────┘ └───────┘ └───────┘ └───────┘
Master 进程 fork 出多个 worker 子进程来处理请求。
2. libpq 的连接流程
PQconnectdb()
├── 解析连接参数
├── DNS 解析
├── TCP 连接
├── SSL/TLS 协商(如果启用)
├── GSS/Kerberos 检查 ← 问题在这里
│ └── pg_GSS_have_cred_cache()
│ └── krb5_init_context()
│ └── CFPreferences API
└── 认证握手
即使不使用 Kerberos 认证,libpq 也会检查是否有可用的凭证缓存。
3. macOS 的 fork 安全限制
macOS 的 Objective-C 运行时和 CoreFoundation 框架在 fork 后的子进程中有严格的限制:
fork() 后的子进程状态:
- 只有调用 fork() 的线程被复制
- 其他线程的锁状态不确定
- Objective-C 运行时状态不一致
- CoreFoundation 内部数据结构可能损坏
如果子进程在 exec() 之前调用了这些 API:
→ 系统检测到不安全操作
→ 触发 SIGABRT 终止进程
4. 为什么 CLI 正常而 FPM 崩溃?
| 模式 | 进程模型 | CoreFoundation 调用 | 结果 |
|---|---|---|---|
| CLI | 直接运行,无 fork | 安全 | ✓ 正常 |
| FPM | fork 子进程处理请求 | 不安全 | ✗ 崩溃 |
5. 为什么 Linux 服务器正常?
Linux 没有 macOS 的 Objective-C 运行时和 CoreFoundation 框架,Kerberos 库使用标准的 POSIX API,在 fork 后的子进程中是安全的。
解决方案
方案一:在连接字符串中禁用 GSS(推荐)
在 PostgreSQL 连接配置中添加 gssencmode=disable 参数,跳过 GSS/Kerberos 认证检查。
Laravel 配置
// config/database.php
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'search_path' => 'public',
'sslmode' => 'prefer',
'gssencmode' => 'disable', // 关键:禁用 GSS 加密
],
原生 PDO 连接
// 在 DSN 中添加 gssencmode=disable
$dsn = "pgsql:host=192.168.1.100;port=5432;dbname=mydb;gssencmode=disable";
$pdo = new PDO($dsn, $user, $password);
pg_connect 函数
$conn = pg_connect("host=192.168.1.100 port=5432 dbname=mydb gssencmode=disable");
环境变量方式(适用于无法修改代码的情况)
// 在连接前设置环境变量
putenv('PGGSSENCMODE=disable');
$pdo = new PDO($dsn, $user, $password);
方案二:通过 PHP-FPM 环境变量配置
在 PHP-FPM 的 pool 配置中全局设置环境变量:
; /opt/homebrew/etc/php/8.2/php-fpm.d/www.conf
; 禁用 libpq 的 GSS/Kerberos 认证
env[PGGSSENCMODE] = disable
修改后重启 PHP-FPM:
brew services restart php@8.2
注意:PHP-FPM 环境变量不能设置为空值,否则会导致配置加载失败。
方案三:使用 .pg_service.conf 配置文件
在用户主目录创建 ~/.pg_service.conf:
[myservice]
host=192.168.1.100
port=5432
dbname=mydb
user=myuser
password=mypassword
gssencmode=disable
sslmode=prefer
然后在代码中使用 service 名称:
$dsn = "pgsql:service=myservice";
$pdo = new PDO($dsn);
方案四:区分环境配置(生产环境兼容)
如果生产环境需要使用 Kerberos 认证,可以通过环境变量区分:
// config/database.php
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
// 仅在 macOS 开发环境禁用 GSS
'gssencmode' => env('DB_GSSENCMODE', PHP_OS === 'Darwin' ? 'disable' : 'prefer'),
],
或者在 .env 文件中配置:
# .env (开发环境)
DB_GSSENCMODE=disable
# .env (生产环境)
# DB_GSSENCMODE=prefer # 或不设置,使用默认值
补充优化建议
1. 配置 pm.max_requests
让 worker 进程定期重启,避免潜在的内存泄漏和状态累积:
; /opt/homebrew/etc/php/8.2/php-fpm.d/www.conf
pm.max_requests = 500
2. 配置 pm.process_idle_timeout
空闲进程超时自动回收:
pm.process_idle_timeout = 10s
3. 使用持久连接(可选)
如果应用频繁连接数据库,可以考虑使用持久连接减少连接开销:
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_PERSISTENT => true,
]);
常见陷阱
PHP-FPM 环境变量不能为空值
在 PHP-FPM 的 pool 配置中设置环境变量时,不能使用空字符串:
; ❌ 错误 - 会导致 PHP-FPM 配置加载失败
env[PGKRBSRVNAME] = ""
; ✅ 正确 - 只设置需要的环境变量
env[PGGSSENCMODE] = disable
如果设置了空值,PHP-FPM 会报错:
[pool www] empty value
Unable to include /opt/homebrew/etc/php/8.2/php-fpm.d/www.conf
failed to load configuration file
FPM initialization failed
多个 PHP-FPM 实例端口冲突
如果遇到以下错误:
ERROR: unable to bind listening socket for address '127.0.0.1:9082': Address already in use (48)
说明有多个 PHP-FPM 进程在争抢同一端口。解决方法:
# 强制杀掉所有 PHP-FPM 进程
sudo pkill -9 php-fpm
# 等待几秒
sleep 2
# 重新启动
brew services start php@8.2
# 验证配置
php-fpm -t
brew services restart 可能不会重启 master 进程
brew services restart 有时不会完全重启 master 进程,导致配置修改不生效:
# 查看进程启动时间
ps aux | grep php-fpm
# 如果 master 进程启动时间很早,需要强制重启
sudo pkill -9 php-fpm
brew services start php@8.2
验证修复
创建测试脚本
<?php
// test_pgsql.php
header('Content-Type: text/plain');
echo "=== PostgreSQL 连接测试 ===\n";
echo "PID: " . getmypid() . "\n";
echo "SAPI: " . php_sapi_name() . "\n";
echo "Time: " . date('Y-m-d H:i:s') . "\n\n";
$dsn = "pgsql:host=192.168.1.100;port=5432;dbname=mydb;gssencmode=disable";
try {
$start = microtime(true);
$pdo = new PDO($dsn, 'user', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
]);
$elapsed = round((microtime(true) - $start) * 1000, 2);
echo "✓ 连接成功 ({$elapsed}ms)\n";
$stmt = $pdo->query("SELECT version()");
echo "✓ PostgreSQL: " . substr($stmt->fetchColumn(), 0, 50) . "...\n";
$pdo = null;
echo "✓ 连接已关闭\n";
} catch (PDOException $e) {
echo "✗ 错误: " . $e->getMessage() . "\n";
}
并发测试
# 10 个并发请求
echo "=== 并发测试 ==="
for i in {1..10}; do
curl -s "http://localhost/test_pgsql.php" &
done
wait
echo "=== 测试完成 ==="
预期结果:所有请求都应该返回"连接成功",无 502 错误。
影响范围
受影响的环境
| 组件 | 版本 | 说明 |
|---|---|---|
| macOS | 10.15+ | Apple Silicon 和 Intel 均受影响 |
| PHP | 8.0+ | 通过 Homebrew 安装,使用 PHP-FPM 模式 |
| libpq | 13+ | 包含 GSS 支持的版本 |
| OpenSSL | 3.x | Homebrew 默认版本 |
不受影响的环境
- Linux 服务器:无 CoreFoundation 限制
- Docker 容器:通常基于 Linux
- macOS CLI 模式:无 fork,直接运行
- macOS + Apache mod_php:不使用 fork 模式(但已不推荐)
- 不使用 PostgreSQL 的项目:不涉及 libpq
相关资源
- PostgreSQL libpq 连接参数文档
- Apple Developer: Forking Best Practices
- PHP Bug #81708: PDO_PGSQL crashes on macOS
- libpq GSS encryption mode
总结
这是一个 macOS 特有的问题,由以下因素共同导致:
- PHP-FPM 使用 fork 模式创建 worker 进程
- libpq 在连接时检查 Kerberos 凭证,即使不使用 Kerberos 认证
- Kerberos 库调用 macOS CoreFoundation API
- macOS 的 fork 安全机制检测到不安全操作,触发 SIGABRT
解决方案是在连接配置中添加 gssencmode=disable 参数,禁用 GSS 加密认证检查。这个配置:
- ✓ 对不使用 Kerberos 认证的环境没有任何副作用
- ✓ 可以安全地应用于开发环境
- ✓ 可以通过环境变量区分开发/生产环境
- ✓ 不影响 SSL/TLS 加密(由
sslmode参数控制)