ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP-FPM监听配置:UDS与TCP原理、性能对比及故障排查指南

PHP-FPM监听配置:UDS与TCP原理、性能对比及故障排查指南 干了好些年 Web 运维和 PHP 开发被问得最多的一个问题就是php-fpm 的listen配置项到底该写/var/run/php-fpm.sock还是127.0.0.1:9000一搜论坛两拨人经常吵得不可开交Unix Domain Socket 党说性能高TCP Socket 党说简单直观好排错。今天不站队把这两种方式背后的原理、性能差异、权限管理、故障排查全部掰开揉碎讲清楚最后再给出一套可以直接“抄作业”的切换步骤。这篇东西适合刚入门 PHP 环境搭建的新手也适合被 502、Connection refused 折磨过的老运维。先说结论这不是一个“谁取代谁”的问题而是一个“在什么场景下选哪个”的问题。你只有真正理解了这两种 Socket 的工作方式才能在任何一台陌生服务器上快速定位问题。1. 先别急着选两种通信方式到底在做什么1.1 一个文件路径和一个 IP 端口为什么能出现在同一个配置项里很多人在第一次看到listen /var/run/php-fpm.sock时都会懵listen 后面不是应该跟 IP 和端口吗怎么是一个文件路径这其实涉及两种完全不同的进程间通信方式127.0.0.1:9000是TCP Socket。Nginx 要通过 TCP/IP 协议栈把请求数据封装成网络包经过 loopback 回环接口发送给 PHP-FPM 监听的 9000 端口。你可以把它理解成两个人打电话即使电话就在隔壁房间也要走一遍完整的拨号、建立连接、传输、挂断流程。/var/run/php-fpm.sock是Unix Domain SocketUDS。它不走网络协议栈直接在操作系统内核层面通过一个文件系统路径作为通信端点来传递数据。相当于两个人站在门口用对讲机喊话中间不需要经过交换机和线路。这两种方式的本质区别决定了它们在性能、权限、跨主机能力上的不同表现。1.2 这些配置你肯定见过各种 listen 值到底是什么意思在 PHP-FPM 的 pool 配置文件常见路径是/etc/php/8.2/fpm/pool.d/www.conf、/etc/php-fpm.d/www.conf或/usr/local/etc/php-fpm.d/www.conf里listen指令可以接受几种类型的值; 监听所有 IPv4 和 IPv6 的 9000 端口很少这么配 listen 9000 ; 只监听 loopback 地址的 9000 端口外部无法直接访问 listen 127.0.0.1:9000 ; 监听本机 IPv6 loopback 地址 listen [::1]:9000 ; 监听 Unix Domain Socket listen /var/run/php-fpm.sock ; 监听相对路径的 Unix Domain Socket基于临时目录 listen php-fpm.sock我看到很多人一上来就抄网上的配置根本不知道自己改的是哪种。最典型的情况是PHP-FPM 在跑Nginx 也在跑但 Nginx 里fastcgi_pass写的是127.0.0.1:9000而实际上 PHP-FPM 只监听了/var/run/php-fpm.sock于是就看到一堆connect() failed (111: Connection refused)这就是典型的“两边各说各话”。2. 一步步掰扯UDS 和 TCP 的真实差别在哪里2.1 性能差异UDS 确实赢但你可能感知不到UDS 最大的优势是省掉了 TCP/IP 协议栈的处理过程。每一次请求都不需要经过 TCP 三次握手、不需要做校验和、不需要走路由和队列数据直接从用户态空间拷贝到内核态再拷贝给对端进程。在高并发场景下这种开销的节省是可观的。我见过一个压测数据同样一台机器、同样一组 PHP 接口用 UDS 比用 TCP loopback 的 QPS 能高出 10% 到 20%尤其是短连接场景更明显。但这里有个容易被忽略的前提只有在 PHP-FPM 和 Nginx 位于同一台主机时UDS 才生效。如果你的架构是 Nginx 在一台服务器、PHP-FPM 在另一台UDS 完全不可用因为 Unix Domain Socket 无法跨主机通信。这一点很多人踩坑在本地单机用 UDS 没问题把 PHP-FPM 容器化部署后还想用同一个 sock 路径结果发现根本访问不到。拿生活中的例子来说UDS 就像公司内部的对讲机只能在同一个园区里用TCP 就像电话只要有号码天南海北都能拨通。2.2 权限管理sock 文件需要“养”TCP 端口只需要“占”这是很多新手的第一个坑。127.0.0.1:9000这种方式只需要保证端口没有被占用、防火墙没有拦截而且因为监听在 loopback外部网络访问不到几乎没有权限问题。但 UDS 不一样。它是以文件形式存在的那就必须遵守文件权限规则。PHP-FPM 启动时会创建这个 sock 文件Nginx 进程必须对这个文件有读写权限否则就会出现connect() failed (13: Permission denied)直接报 502。常见配置是listen /var/run/php-fpm.sock listen.owner www-data listen.group www-data listen.mode 0660这里的listen.mode 0660表示文件所有者和所属组可以读写其他人不行。如果你的 Nginx 是用nginx用户跑的而 PHP-FPM 的 sock 文件所有者和组都是www-data那 Nginx 就访问不了需要把 Nginx 进程用户加入www-data组或者让两者统一。还有一个我非常想强调的细节很多发行版的/var/run实际是指向/run的软链接而/run是 tmpfs系统重启后会清空。所以你的 PHP-FPM 如果配置的是/var/run/php-fpm.sock重启之后目录还在但 sock 文件需要重新创建这本身没问题因为 PHP-FPM 启动时会自动创建。但如果你的脚本或监控工具提前检查“sock 文件是否存在”则可能在 PHP-FPM 启动前就误报故障。2.3 故障排查方式完全不同看到报错就能猜到是哪一种根据我自己的排查经验这两种方式出问题的症状和排查入口完全不一样对比项Unix Domain SocketTCP Socket常见报错connect() failed (2: No such file or directory)、(13: Permission denied)connect() failed (111: Connection refused)、(110: Connection timed out)排查首选命令ls -l /var/run/php-fpm.sock、ss -xlss -tlnp | grep 9000、telnet 127.0.0.1 9000常见原因sock 文件路径写错、PHP-FPM 未启动、权限不足端口被占用、PHP-FPM 未监听、防火墙拦截、监听地址不匹配能否跨主机不能可以看到No such file or directory第一反应就是 PHP-FPM 没启动或者 Nginx 里配置的路径和 PHP-FPM 实际创建的路径不一致。看到Connection refused第一反应则是“端口上没有进程在监听”这时候优先确认 PHP-FPM 是否真的在监听 9000 端口。2.4 到底怎么选一张图理清场景做了这么多项目我的个人经验是单机部署、Nginx 和 PHP-FPM 在同一台机器优先用 Unix Domain Socket。使用 Docker Compose 管理 PHP-FPM 容器且 Nginx 容器通过内部网络访问时建议用 TCP比如php-fpm:9000因为 sock 文件不能跨容器直接共享除非你显式挂载共享卷。使用 Kubernetes、多实例负载均衡、或者 PHP-FPM 独立部署在多台机器上必须用 TCP。本地开发调试时TCP 更方便因为你可以直接用telnet 127.0.0.1 9000来测试连接也能配合各种图形化工具查看端口状态。不要盲目跟风。UDS 虽好但在容器化、分布式场景下强行用它带来的麻烦远大于性能收益。3. 实操记录两种配置的完整切换过程3.1 从 TCP 切换到 UDS Unix Domain Socket 假设当前 PHP-FPM 配置是listen 127.0.0.1:9000我想换成/var/run/php-fpm.sock。第一步修改 PHP-FPM pool 配置文件。以 Ubuntu 上 PHP 8.2 为例# 先备份别上来就改 cp /etc/php/8.2/fpm/pool.d/www.conf /etc/php/8.2/fpm/pool.d/www.conf.bak # 修改 listen 和相关权限配置 vim /etc/php/8.2/fpm/pool.d/www.conf在文件里找到下面这几行listen 127.0.0.1:9000 ;listen.owner www-data ;listen.group www-data ;listen.mode 0660改成listen /var/run/php-fpm.sock listen.owner www-data listen.group www-data listen.mode 0660注意配置文件里listen.owner前面的分号是注释符如果你想启用就必须去掉。另外要确保/var/run或/run目录对 PHP-FPM 进程有写权限一般发行版默认没问题但如果你自定义了/var/run/php-fpm子目录就要提前创建并设置属主。第二步修改 Nginx 配置。在server块或location ~ \.php$里# 原来的写法 # fastcgi_pass 127.0.0.1:9000; # 改成 fastcgi_pass unix:/var/run/php-fpm.sock;第三步检查配置并重启# 检查 PHP-FPM 配置语法 php-fpm8.2 -t # 检查 Nginx 配置语法 nginx -t # 重启服务 systemctl restart php8.2-fpm systemctl reload nginx第四步验证是否生效# 查看 Unix Domain Socket 监听状态 ss -xl | grep php-fpm # 输出类似这样 u_str LISTEN 0 511 /var/run/php-fpm.sock 12345如果你看到u_str LISTEN说明 PHP-FPM 已经在通过 sock 文件监听了。这时候再访问一个 PHP 页面确认不再 502就说明切换成功。3.2 从 UDS 切换回 TCP 127.0.0.1:9000 这个反向操作也很常见比如你要把 PHP-FPM 容器化或者需要别的机器通过内网访问。同样先备份然后修改www.conflisten 127.0.0.1:9000 ; 保留原来的权限配置也没关系TCP 模式下不生效 ;listen.owner www-data ;listen.group www-data ;listen.mode 0660Nginx 里改回fastcgi_pass 127.0.0.1:9000;然后重启验证systemctl restart php8.2-fpm nginx -t systemctl reload nginx # 查看 TCP 端口监听 ss -tlnp | grep 9000看到LISTEN 0 511 127.0.0.1:9000就说明监听成功。这里要特别提醒只监听127.0.0.1意味着只能本机访问跨机器访问是不通的。如果公司内网有机器需要连接这台 PHP-FPM必须监听0.0.0.0:9000或具体内网 IP同时配置防火墙。还要修改listen.allowed_clients这个参数只在 TCP 模式生效默认允许所有客户端访问如果该行被注释。为了安全可以设成listen.allowed_clients 127.0.0.1但注意这个参数在 PHP-FPM 7.0 之后其实已经被标记为废弃了真正的访问控制应当由防火墙或网络层来做不要过度依赖它。3.3 一个小细节监听 IPv6 时容易踩的坑如果你在配置里写了listen [::1]:9000那么127.0.0.1的 IPv4 连接是连不上的Nginx 要用fastcgi_pass [::1]:9000;或者更简单一点统一用127.0.0.1:9000避免 IPv4/IPv6 混淆。我在调试的时候见过有人在 Nginx 里写fastcgi_pass localhost:9000而 PHP-FPM 监听了127.0.0.1:9000看起来没问题但localhost在某些系统上解析成 IPv6 的::1于是连接被拒绝。这类玄学问题根源就是对“localhost 到底解析成哪个 IP”没有概念。4. 高能预警这些报错信息你必须看得懂4.1 最经典的 502connect() failed (111: Connection refused)这是我遇到最多的错误Nginx 错误日志里长这样2025/01/15 10:22:31 [error] 12345#0: *678 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.1.10, server: example.com, request: GET /index.php HTTP/1.1, upstream: fastcgi://127.0.0.1:9000看到Connection refused你不要先去怀疑代码直接按下面的顺序查确认 PHP-FPM 进程是否存活ps aux | grep php-fpm如果没有任何进程说明 PHP-FPM 崩了或被停掉了。确认端口是否真的在监听ss -tlnp | grep 9000如果没有任何输出说明 PHP-FPM 虽然活着但没有监听 9000 端口很可能是配置里写了 sock 路径Nginx 却还在用 TCP。确认 Nginx 的fastcgi_pass是否和 PHP-FPM 的listen完全一致一个写端口一个写 sock必报此错。如果是跨机器连接还要确认防火墙是否放行端口、目标机器是否监听的是0.0.0.0或可访问的网卡 IP而不是只监听127.0.0.1。4.2 全网都在问的端口冲突error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket addre最近这个报错在不少社区热搜里出现虽然它不是 PHP-FPM 的错误而是某个基于 Go 的服务比如本地模型服务启动时报的但原理和 PHP-FPM 端口冲突完全一致error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket addre这行英文直译是“每个套接字地址只能用一次”在 Windows 上通常表现为bind: An attempt was made to access a socket in a way forbidden by its access permissions。核心原因就一个11434 端口已经被某个进程占用了新的进程无法 bind 同一个端口。遇到这种情况我的标准排查路线是# 1. 看谁占着这个端口 ss -tlnp | grep 11434 # 2. 如果本机没有输出可能是 IPv6 地址冲突加 -A 查看 ss -tlnpa | grep 11434 # 3. 找到 PID 之后确认进程是不是你需要保留的服务 ps -fp PID # 4. 如果确认是可以关闭的旧进程就 kill 掉如果不是就换端口 kill -9 PID这个报错还有一个特殊来源程序自己起了多个实例。比如你写了一个服务代码里明确绑定 11434然后你又手动启动了第二个进程第二个进程就会报这个错。这时候不是端口被别的程序占而是你自己重复启动了。还有一种情况是TIME_WAIT状态引起的。TCP 连接关闭后端口会进入TIME_WAIT状态默认持续时间约 60 秒如果服务立即重启且没有设置SO_REUSEADDR就有可能在短时间内报 bind 失败。但作为 PHP-FPM 这种成熟的软件一般不会犯这种低级错误更多还是配置和进程管理问题。4.3 权限问题sock 文件 permission denied最容易被小白的配置坑如果 Nginx 错误日志里出现connect() failed (13: Permission denied) while connecting to upstream而ss -xl又能看到 sock 文件在监听那大概率就是权限问题。你可以执行ls -l /var/run/php-fpm.sock正常输出类似srw-rw---- 1 www-data www-data 0 Jan 15 10:22 /var/run/php-fpm.sock注意开头的s表示这是 Socket 文件。后面的rw-rw----表示所有者和所属组有读写权限其他人没有。如果 Nginx 的 worker 进程用户是nginx而这个 sock 文件属主和属组都是www-datanginx 用户就没有权限自然连不上。解决办法有几种把 Nginx 用户加入 www-data 组usermod -aG www-data nginx然后重启 Nginx。把listen.owner和listen.group都改成nginx。把listen.mode改成0666允许所有人访问。我不推荐这样虽然最容易解决但增加了本机其他用户读取 PHP-FPM 通信内容的风险。注意改完权限配置后不仅要重启 PHP-FPM还要让 Nginx 重新加载配置最好直接systemctl restart nginx因为进程用户组变更后需要完全重启才生效。4.4 混合杂症502 的另一个“嫌疑人”是 timeout 和 buffering如果你排除了连接问题页面还是偶尔 502可以看看 Nginx 日志里有没有upstream timed out或upstream sent too big header。upstream sent too big header的意思是 FastCGI 返回的响应头超出了 Nginx 的fastcgi_buffer_size限制。一些框架比如 Laravel、Symfony会设置很多 Cookie 和 Header一不小心就超了。解决办法是在 Nginx 的location ~ \.php$里调大 bufferfastcgi_buffers 16 16k; fastcgi_buffer_size 32k;如果是慢请求导致超时要检查fastcgi_read_timeout、request_terminate_timeout。PHP-FPM 默认的request_terminate_timeout通常是 0 或 30s如果脚本执行超过这个时间PHP-FPM 会主动杀掉进程Nginx 就会报 502。很多支付回调、大数据导出接口都会踩这个坑。5. 一个真实场景复盘11314 端口被“抢”之后我做了什么前面提到bind: only one usage of each socket addre这类报错我拿一个真实例子复盘一下完整的排查过程你以后遇到类似问题可以直接套用。大概是上个月我在一台测试服务器上部署一个本地 AI 推理服务它默认监听 11434 端口。启动时终端直接报error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket addre第一反应是“11434 被占了”。于是我执行ss -tlnp | grep 11434输出显示LISTEN 0 4096 127.0.0.1:11434 0.0.0.0:* users:((ollama,pid29813,fd3))原来是一个旧版本的 ollama 进程占着端口。我其实已经忘了之前手动启动过那这次的目标服务虽然是新版但只 bind 同一个 IP 端口所以失败。我没有直接kill -9而是先确认这个旧进程是否还在被其他任务调用。查了一下没有业务依赖于是正常停止kill 29813 sleep 2 ss -tlnp | grep 11434发现端口已经释放再启动新服务就成功了。这里我想强调一个容易忽略的点很多时候你不止遇到“端口被占”还可能遇到“端口看起来没被占但 bind 仍然失败”。这种情况要检查是不是不同协议栈的端口冲突比如你监听0.0.0.0:9000和[::]:9000是两个不同的地址如果某个服务只绑了 IPv6 的 9000在部分系统上 IPv4 的服务再 bind 9000 也会报错。解决办法是用ss -tlnpa看清楚所有地址族的监听情况。还有一个我自己踩过的坑在 Docker 容器里容器内端口和宿主机端口是两套体系。如果你在宿主机上执行ss -tlnp | grep 9000是看不到容器内 PHP-FPM 监听的要进入容器检查。反过来如果 Nginx 在宿主机上PHP-FPM 在容器里Nginx 无法通过127.0.0.1:9000访问到容器里的 PHP-FPM必须使用宿主机 IP 或php-fpm:9000这样的 Docker 内部网络地址。很多人部署 LNMP 容器环境时出现“外部能连 NginxPHP 却一直报 504”就是这个原因。6. 我的个人结论和配置建议如果现在让我去新装一套 PHP 环境单机场景下我会直接用 Unix Domain Socketlisten /var/run/php-fpm.sock listen.owner www-data listen.group www-data listen.mode 0660Nginx 对应fastcgi_pass unix:/var/run/php-fpm.sock;这样既避免了 TCP 连接建立的开销也减少了端口被扫描的风险。但如果是 Docker Compose、Kubernetes、或者 PHP-FPM 要单独拆分到多台机器的场景我会毫不犹豫改成 TCPfastcgi_pass php-fpm:9000;因为 UDS 根本无法跨主句强行用反而是在给自己挖坑。最后说一个我实践中的体会很多问题不是出在“选 sock 还是选 TCP”而是出在“改了某一侧配置忘了同步另一侧”。无论你使用哪种方式请务必在做完改动后同时检查两侧ss -xl看 UDS 是否在监听ss -tlnp看 TCP 端口是否在监听nginx -t systemctl reload nginx确保 Nginx 配置已生效随便请求一个 PHP 页面确认返回不是 502。养成这个习惯后你会发现类似问题基本都能在一分钟内定位。如果你现在正纠结要不要把 9000 改成 sock我建议你按自己的架构来不要纯为了性能做迁移。单机几千并发UDS 和 TCP 的差距不明显但如果哪天真上了高并发你再回过头把listen改成 UDS也就是改两行配置的事。最怕的是“知其然不知其所以然”一遇到问题就在 Nginx 和 PHP-FPM 两边来回改最后越改越乱。希望这篇文章能帮你真正把这两个 Socket 的脾气摸透。
返回列表