问题描述

在 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 forkedcrashed 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

相关资源

总结

这是一个 macOS 特有的问题,由以下因素共同导致:

  1. PHP-FPM 使用 fork 模式创建 worker 进程
  2. libpq 在连接时检查 Kerberos 凭证,即使不使用 Kerberos 认证
  3. Kerberos 库调用 macOS CoreFoundation API
  4. macOS 的 fork 安全机制检测到不安全操作,触发 SIGABRT

解决方案是在连接配置中添加 gssencmode=disable 参数,禁用 GSS 加密认证检查。这个配置:

  • ✓ 对不使用 Kerberos 认证的环境没有任何副作用
  • ✓ 可以安全地应用于开发环境
  • ✓ 可以通过环境变量区分开发/生产环境
  • ✓ 不影响 SSL/TLS 加密(由 sslmode 参数控制)

标签: PHP, macOS, PostgreSQL, PHP-FPM, Homebrew, libpq, GSS, Kerberos, 502错误, SIGABRT

添加新评论