由Nginx集中代理分散的PHP集群的实践
# 1,以往
以往的 PHP 项目处理方案大多沿用了经典的 lnmp,然后几乎有 PHP 的主机都会配套一个 Nginx,接着有多少个 PHP,就会有多少个 Nginx,然后 lb 后边就得挂上这所有的 Nginx。
版本说明
本文写于 2021-01。
未在更高版本上重新验证,请以对应版本的官方文档为准;具体 PHP-FPM 版本细节请结合实际环境核对。
但事实上还可以用如下办法来将 PHP 分散管理。
# 2,现在
Nginx 主配置像配置 Java 应用一般反代转发给 PHP 服务:
upstream php-fpm {
server 127.0.0.1:9006 weight=6 max_fails=300 fail_timeout=5s;
server 127.0.0.1:9007 weight=6 max_fails=300 fail_timeout=5s;
}
server{
listen 80;
server_name 192.0.2.11;
proxy_connect_timeout 300;
proxy_read_timeout 300;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
location / {
root /data/www/liql;
index index.php index.html index.htm;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php($|/) {
root /data/www/liql;
index index.php index.html index.htm;
fastcgi_pass php-fpm;
fastcgi_index index.php;
fastcgi_split_path_info ^(.+\.php)(.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这样只需要将代码放入到 PHP 容器内即可:
$ cat liql/index.php
<?php
phpinfo();
?>
$ cat Dockerfile
FROM reg.eryajf.net/multienv/wpt_phpfpm:7.2.34
COPY . /data/www/
2
3
4
5
6
7
然后重新打镜像:
docker build -t aaa .
启动两个容器进行访问,可以发现权重很均衡的取到了两个 PHP 容器之内:
{"remote_addr": "192.0.2.12","@timestamp": "2021-01-24T17:14:27+08:00","upstream_addr": "127.0.0.1:9006","request_uri": "/","verb": "GET","httpversion": "HTTP/1.1","response": "200", "body_bytes_sent": "28892", "referrer": "", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", "http_x_forwarded_for": "", "server_name": "192.0.2.11","request_time": "0.003","upstream_response_time": "0.003","realpath_root": "","cookie": "grafana_session=02bc6ce5e1489781ff5ed00ef9fb61ca","request_body": "","nginx_version": "1.13.6","scheme": "http"}
{"remote_addr": "192.0.2.12","@timestamp": "2021-01-24T17:14:28+08:00","upstream_addr": "127.0.0.1:9007","request_uri": "/","verb": "GET","httpversion": "HTTP/1.1","response": "200", "body_bytes_sent": "28895", "referrer": "", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", "http_x_forwarded_for": "", "server_name": "192.0.2.11","request_time": "0.004","upstream_response_time": "0.004","realpath_root": "","cookie": "grafana_session=02bc6ce5e1489781ff5ed00ef9fb61ca","request_body": "","nginx_version": "1.13.6","scheme": "http"}
2

# 3,另外
在测试过程中发现,如果代理的两个容器有一个没有起来,竟丝毫不影响服务的访问,转发的方式也非常有意思,从日志中观察:
{"remote_addr": "192.0.2.12","@timestamp": "2021-01-24T17:36:27+08:00","upstream_addr": "127.0.0.1:9006","request_uri": "/","verb": "GET","httpversion": "HTTP/1.1","response": "200", "body_bytes_sent": "28898", "referrer": "", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", "http_x_forwarded_for": "", "server_name": "192.0.2.11","request_time": "0.004","upstream_response_time": "0.004","realpath_root": "/data/www/liql","cookie": "grafana_session=02bc6ce5e1489781ff5ed00ef9fb61ca","request_body": "","nginx_version": "1.13.6","scheme": "http"} {"remote_addr": "192.0.2.12","@timestamp": "2021-01-24T17:36:29+08:00","upstream_addr": "127.0.0.1:9007, 127.0.0.1:9006","request_uri": "/","verb": "GET","httpversion": "HTTP/1.1","response": "200", "body_bytes_sent": "28899", "referrer": "", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", "http_x_forwarded_for": "", "server_name": "192.0.2.11","request_time": "0.004","upstream_response_time": "0.000, 0.004","realpath_root": "/data/www/liql","cookie": "grafana_session=02bc6ce5e1489781ff5ed00ef9fb61ca","request_body": "","nginx_version": "1.13.6","scheme": "http"}1
2注意日志中的
upstream_addr字段,第一条请求被 9006 端口正常解析,第二条的解析竟然两个端口都有,也就是说,9007 端口不存在或者无法正常解析的时候,此请求会被正常的 9006 给解析掉,看错误日志里记录下了 9007 的错误:2021/01/24 17:36:29 [error] 24165#24165: *394 connect() failed (111: Connection refused) while connecting to upstream, client: 192.0.2.12, server: 192.0.2.11, request: "GET / HTTP/1.1", upstream: "fastcgi://127.0.0.1:9007", host: "192.0.2.11"1注意代码的存放位置
- 这里有两点需要注意,一个是
root指令不要放在 location 外边,这里是纯 PHP 解析,如果放到外边,请求的时候总会报一个本地找不到的错误:
2021/01/24 17:21:51 [crit] 23563#23563: *347 realpath() "/data/www/liql" failed (2: No such file or directory) while logging request, client: 192.0.2.12, server: 192.0.2.11, request: "GET / HTTP/1.1", upstream: "fastcgi://127.0.0.1:9006", host: "192.0.2.11"1这个时候把
root指令放到 location 内部就不会报这个错误了。- 另外注意,代码是存放在 PHP 所在主机上的,而非 Nginx 所在主机,因为
.php结尾的请求都是通过 fast_cgi 解析的,而非 Nginx,这里千万不要用传统的 http 代理来理解了。
所以这个地方的处理流程应该是这样的:
www.example.com | | Nginx | | 路由到www.example.com/index.php | | 加载nginx的fast-cgi模块 | | fast-cgi监听127.0.0.1:9006地址 | | www.example.com/index.php请求到达127.0.0.1:9000 | | php-fpm 监听127.0.0.1:9000 | | php-fpm 接收到请求,启用worker进程处理请求 | | php-fpm 在给定的root地址下解析PHP源码,将结果返回给nginx | | nginx将结果通过http返回给浏览器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28这样,就能很清晰地解析整个处理链路了。
本文实验探索的主要意义在于,在这种基础之下,我们就可以摒弃掉以往处理 PHP 业务场景的固化思维,从而将 Nginx 这层网关像处理 Java 应用那样一般,集中对 PHP 应用进行处理了,以实现集中化管理,更加便于横向扩展与运维。
- 这里有两点需要注意,一个是
# 3. 架构演进历程
# 3.1 传统模式:每个 PHP 配备独立 Nginx
早期的 PHP 部署模式通常是 LNMP(Linux + Nginx + MySQL + PHP),每个 PHP-FPM 实例都会配套一个 Nginx。这种模式的优势是架构简单,缺点是:
- 资源浪费:每个 Nginx 都要维护独立的 worker 进程和连接池
- 配置分散:20 个 PHP 服务就有 20 份 Nginx 配置,难以统一管理
- 负载均衡复杂:LB 层需要对接所有这些 Nginx,增加了复杂度
# 3.2 改进模式:集中式 Nginx 代理
改进后的模式使用单一 Nginx 集中代理所有 PHP 服务:
# 唯一的 Nginx 配置
upstream php_pool_1 {
server 127.0.0.1:9006 weight=6 max_fails=300 fail_timeout=5s;
server 127.0.0.1:9007 weight=6 max_fails=300 fail_timeout=5s;
}
upstream php_pool_2 {
server 127.0.0.1:9008;
server 127.0.0.1:9009;
}
server {
listen 80;
server_name phpapp.example.com;
# 根据路径转发到不同 PHP 池
location /app1/ {
fastcgi_pass php_pool_1;
include fastcgi_params;
}
location /app2/ {
fastcgi_pass php_pool_2;
include fastcgi_params;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
优势:
- 统一入口,简化运维
- 减少资源占用
- 集中管理配置
- 便于做统一的访问控制
# 4. 集中代理的配置要点
# 4.1 PHP-FPM 配置
每个 PHP-FPM 实例使用不同的端口:
; pool 1
[www]
listen = 127.0.0.1:9006
pm = static
pm.max_children = 50
; pool 2
[www]
listen = 127.0.0.1:9008
pm = static
pm.max_children = 50
2
3
4
5
6
7
8
9
10
11
# 4.2 Nginx 路径分发
根据 URI 路径或域名转发到不同 PHP 池:
server {
listen 80;
server_name app1.example.com;
location / {
fastcgi_pass 127.0.0.1:9006;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
server {
listen 80;
server_name app2.example.com;
location / {
fastcgi_pass 127.0.0.1:9008;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 4.3 超时与缓冲配置
大文件上传或慢请求需要合理配置超时时间:
location / {
fastcgi_pass 127.0.0.1:9006;
# 超时配置
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
# 缓冲配置
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
include fastcgi_params;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5. 坑与边界
常见问题一:PHP-FPM 进程耗尽
如果某个 PHP 应用有内存泄漏,长时间运行后 PHP-FPM 进程可能耗尽。解决方案是配置 pm.max_requests 强制回收进程:
pm.max_requests = 500
常见问题二:Session 共享问题
不同 PHP 池需要共享 Session,否则用户可能丢失登录状态。解决方案:
- 使用 Redis/Memcached 存储 Session
- 在 PHP 配置中指定共享存储:
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"
2
常见问题三:文件上传路径
文件上传通常存储在共享存储(如 NFS、Ceph),确保所有 PHP-FPM 节点都能访问相同的存储路径。
常见问题四:定时任务一致性
如果是多节点部署,定时任务(如 crontab)需要在所有节点运行还是只运行一次,需要根据业务逻辑设计。
# 6. 监控与告警
集中代理模式下,监控要点包括:
- 后端健康检查:监控各 PHP-FPM 端口是否可达
- 响应时间:关注 fastcgi_response_time 指标
- 错误率:监控 502、504 错误
- 连接数:监控与各 PHP-FPM 的连接数
建议配置 Upstream 状态页:
location /nginx_status {
stub_status on;
allow 127.0.0.1;
deny all;
}
location /php_fpm_status {
fastcgi_pass 127.0.0.1:9006;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
2
3
4
5
6
7
8
9
10
11
# 7. 性能优化
# 7.1 连接池复用
PHP-FPM 的连接是短连接,每次请求都会创建新连接。可以优化为持久连接:
fastcgi_keep_conn on;
# 7.2 静态资源分离
图片、CSS、JS 等静态资源不应经过 PHP-FPM,直接由 Nginx 提供:
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
root /data/www/static;
expires 30d;
add_header Cache-Control "public, immutable";
}
location / {
fastcgi_pass 127.0.0.1:9006;
# ...
}
2
3
4
5
6
7
8
9
10
# 7.3 Gzip 压缩
对响应内容进行压缩,减少传输量:
gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 1024;
2
3
# 8. 参考
# 9. 扩展:多机房部署
如果服务部署在多个机房,需要考虑流量调度:
方案一:DNS 轮询
不同地域的用户解析到不同机房的 Nginx 入口。
方案二:Anycast
使用 Anycast IP,整个网络共享同一个 IP 地址,就近接入。
方案三:Globe Load Balancer
在 Nginx 前面加一层全局负载均衡(如 Cloudflare、阿里云 GSLB),根据地域选择后端。
多机房部署的注意事项:
- 数据一致性:确保各机房数据同步(如 MySQL 主从、Redis 集群)
- 延迟考虑:跨机房调用延迟较高,架构上应尽量减少跨机房依赖
- 成本考量:跨机房带宽成本较高,需要评估流量模式
# 10. 容器化改造
在 Kubernetes 环境中,PHP 应用的部署方式会有变化:
传统部署
Nginx → PHP-FPM (Host 网络)
容器部署
Nginx (Host/Cluster) → PHP-FPM (Pod 网络)
容器环境中的关键配置:
# PHP-FPM Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-fpm
spec:
replicas: 3
template:
spec:
containers:
- name: php-fpm
ports:
- containerPort: 9000
---
# Nginx ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
default.conf: |
upstream php_backend {
server php-fpm:9000;
}
server {
location / {
fastcgi_pass php_backend;
include fastcgi_params;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
容器化优势:
- 自动扩缩容
- 故障自愈
- 环境一致性
- 资源隔离
# 11. 总结
Nginx 集中代理 PHP 集群的架构,适合以下场景:
- 多业务统一入口:统一管理、安全策略
- 简化运维:减少组件数量,统一配置
- 降低成本:复用 Nginx 资源,减少复杂度
关键配置点:
- PHP-FPM 多实例端口区分
- Nginx upstream 负载策略
- 超时与缓冲参数调优
- Session 共享机制
- 监控告警体系
架构选型建议:
- 10 个以下 PHP 服务:可用集中代理
- 10-50 个 PHP 服务:考虑分层(前端 Nginx + 后端 Nginx)
- 50+ 个 PHP 服务:考虑微服务化或 API 网关