news 2026/10/26 6:09:00

LAMP/LNMP部署调优与Nginx/PHP-FPM故障排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LAMP/LNMP部署调优与Nginx/PHP-FPM故障排查

1. 两套架构的本质:谁在前面接客,谁在后面干活

1.1 一个请求从进门到出门要经过几道手

很多人学到 Linux 这一阶段,命令敲得挺顺,用户、权限、软件包、systemd、防火墙都能摆弄,但一到"让这台机器真正对外提供网页服务"就卡住了。LAMP 和 LNMP 就是绕不过去的那个坎。LAMP 指的是Linux + Apache + MySQL(现在多数场景用 MariaDB)+ PHP这套组合,LNMP 把 Apache 换成 Nginx,其余保持不变。它俩能跑的东西远比新手想的多:企业官网、内部管理系统、博客论坛、小型电商、接口服务、后台管理面板,绝大多数中小体量的 Web 应用,底层都是这两套之一。

我先把一个请求的完整路径拆开,后面所有的配置和排错都建立在这条链路上。浏览器发起 HTTP 请求打到服务器的 80 端口,Web 服务器(Apache 或 Nginx)接手。如果请求的是图片、CSS、JS、静态 HTML 这类文件,Web 服务器直接从磁盘读出来返回,PHP 和数据库根本不参与。如果请求的是.php文件,Web 服务器通过某种协议把请求转交给 PHP 解释器,PHP 执行代码,需要数据就去问数据库,拿到结果拼成 HTML,再沿着原路返回给浏览器。

这个分层不是摆样子,它决定了后面所有的性能调优和故障定位方式。静态资源走 Web 服务器是最短路径,速度取决于磁盘和网络;动态请求一旦进入 PHP,就变成 CPU 和内存的活;数据库再往下是磁盘 IO。哪一层堵住,现象完全不同——静态页打得开但动态页 502,问题一定在 Web 服务器到 PHP 这一段;PHP 页面能打开但一直转圈,大概率是卡在数据库查询或者网络超时上。

用餐厅打个比方:Linux 是场地、水电、后厨设备;Nginx/Apache 是前台接待,负责迎客、上凉菜、把点菜单送进后厨;PHP 是厨师,按单子做菜;MySQL 是仓库,厨师缺料就去拿。前台再多,厨师只有两个,出菜速度照样上不去。很多新手调优时只盯着 Web 服务器加并发,结果越调越慢,就是因为瓶颈根本不在这一层。

1.2 Apache 和 Nginx 到底差在哪

这两个东西的定位经常被误解成"新老替代",其实不是。它们的并发模型从根上就不一样,适配的场景也不同。

Apache 采用的是多进程/多线程模型,靠 MPM(Multi-Processing Module)决定具体形态。prefork是纯多进程,每个进程处理一个连接,彼此独立,稳定性极好,但内存吃得多;worker是多进程加多线程的混合;event是 2.4 之后的默认模式,在 worker 基础上把 keep-alive 的空闲连接交给专门的监听线程处理,缓解了长连接占进程的问题。关键点在于:mod_php 是非线程安全的,一旦 PHP 以模块形式编译进 Apache,就只能跑prefork,这意味着每个 Apache 子进程里都嵌着一份 PHP 解释器,几百个进程就是几百份解释器的内存开销。

Nginx 走的是事件驱动 + 异步非阻塞路线。一个 master 进程负责管理,若干个 worker 进程(通常等于 CPU 核数)各自跑一个事件循环,用 epoll 这种机制同时盯住成千上万个连接,谁有数据就处理谁,不会因为某个连接在等 IO 就卡住整个进程。它处理静态文件、做反向代理、扛并发连接的能力非常强,但它自己不执行 PHP 代码,必须把动态请求转给外部的 PHP-FPM 进程池。

对比维度ApacheNginx
并发模型多进程/多线程,连接独占进程或线程单线程事件循环,连接复用
内存占用较高,尤其 prefork + mod_php较低,worker 数量固定
静态文件性能中等很强
动态请求可内置 mod_php,也可转 PHP-FPM只能转 PHP-FPM
配置风格分散,支持 .htaccess 目录级覆盖集中,无 .htaccess
典型优势场景老项目兼容、共享主机、需要目录级配置高并发、反向代理、静态资源前置

.htaccess这个差异值得单独说一句。Apache 允许在网站目录下放一个.htaccess文件来覆盖上级配置,方便没有服务器权限的人改伪静态规则,代价是每次请求都要逐级查找读取这些文件,性能有损耗。Nginx 完全不认这套,所有规则必须写进主配置或站点配置里,改一次就得 reload。新手从 Apache 迁到 Nginx 最不适应的地方就是这个——原来扔个.htaccess就生效的伪静态,现在得翻译成try_files或者rewrite规则。

1.3 什么场景该选哪一套

选型不是看哪个"更先进",而是看你的实际条件。我把常见的判断依据列一下。

选 LAMP 的情况:团队里有人熟悉 Apache 配置;跑的是老系统或者某些依赖.htaccess的现成程序;需要 Apache 特有的模块(比如某些认证模块、限流模块);服务器数量少、并发压力不大,图省事。这种情况下 mod_php 一把梭完全够用,配置少、链路短、出问题好查。

选 LNMP 的情况:机器配置有限但要扛较高并发;需要做反向代理、负载均衡、静态资源缓存;前端有大量静态文件;未来可能上容器或者多实例部署。LNMP 的进程分离结构天然适合横向扩展,PHP-FPM 可以独立重启而不影响 Web 服务器,这点在线上排障时非常关键。

还有一种常被忽略的第三种组合:Nginx 在前做反向代理,Apache 在后跑 PHP。静态资源和 SSL 终结交给 Nginx,动态请求转发给 Apache 的 8080 端口。这种混合架构在历史项目迁移里很常见,不是邪道,只是维护成本略高。

提醒一句:新手最容易犯的错是"听说 Nginx 性能好,直接把 Apache 换掉",结果发现原来的伪静态全失效、上传功能报错、某些扩展不兼容,一晚上耗进去。已经跑起来的东西,没有明确的性能瓶颈就别动。

2. 开工前的准备:系统底子、依赖和安装路线

2.1 机器规格与系统选择的现实考量

动手之前先把机器定下来,不然后面全是返工。我的经验值是:纯测试环境 1 核 1G 能跑起来,但装完 MariaDB 加 PHP 之后剩余内存很紧,编译个东西都可能 OOM;稍微正经一点的练习环境建议2 核 2G 起步,能比较舒服地跑 LAMP 或 LNMP 全套。如果要跑实际业务,2 核 4G 是个比较稳妥的起点,再往上就要看具体负载了。

磁盘规划上,/var/www(网站根目录)和/var/lib/mysql(数据库数据目录)是两个会长大的地方。小项目不用单独分区,但如果用的是云主机,注意系统盘和数据盘的区分,数据库目录挂到数据盘上,以后扩容和快照都方便。日志目录/var/log也要留意,Nginx 和 Apache 的访问日志在流量上来之后增长很快,不定期切割能把磁盘写满——这个坑我踩过,凌晨磁盘 100%,网站全挂。

系统选择上,RHEL 系(CentOS 及其衍生版本)和 Debian 系(Ubuntu、Debian)是最常见的两类。主要差异在包管理器(yum/dnf对apt)、服务名(httpd对apache2)、配置文件路径、以及 RHEL 系默认开启 SELinux。SELinux 是新手在 RHEL 系上部署 Web 服务最容易撞的墙,后面会专门讲。

开工前的基础动作清单:

# 1. 更新系统 # RHEL 系 yum update -y # Debian 系 apt update && apt upgrade -y # 2. 设置主机名和时间同步 hostnamectl set-hostname web01 timedatectl set-timezone Asia/Shanghai # 时间不同步会导致日志时间错乱、证书校验失败,务必配好 # 3. 确认防火墙状态 systemctl status firewalld # RHEL 系 ufw status # Debian 系 # 4. 确认 SELinux 状态(仅 RHEL 系) getenforce

关于 SELinux,网上大量教程的第一句就是setenforce 0关掉它。我不建议在生产环境这么干,因为它挡住的很多问题(比如目录权限配置错误、进程越权访问)关掉之后就变成隐患了。学习阶段为了排除干扰可以先设成 permissive 观察,但要知道自己关了什么。正确做法是学会用semanage和restorecon打标签,这个在第 5 章会展开。

2.2 包管理器与源码编译的取舍

安装 PHP 有两条路,选错了后面会很难受。

包管理器安装(yum install php或apt install php)的好处是依赖自动解决、升级路径清晰、配置文件位置规范、出问题社区资料多。坏处是版本被仓库锁死——系统自带的软件源往往只提供比较老的版本,而且编译参数是发行版维护者定的,某些扩展要单独装包。

源码编译的好处是版本和编译参数完全可控,能精确指定要哪些扩展、不要哪些、装到哪个目录。坏处是依赖要自己一个个装、升级得重新编译、出问题没有包管理的回滚机制,而且编译过程本身很耗时(PHP 编译一次十几分钟很正常)。

维度包管理器源码编译
上手难度低高,依赖链要自己捋
版本控制受仓库限制完全可控
依赖处理自动手动,容易漏
升级维护一条命令重新编译 + 切换
排错资料多相对少
适用场景学习、常规业务需要特定版本或裁剪

我的建议很明确:学习阶段用包管理器,先把整条链路跑通。你需要理解的是请求怎么流转、配置怎么组织、出问题往哪看,这些和安装方式无关。等链路熟了,再针对"我需要 PHP 8.2 的某个特性但仓库只有 8.0"这种具体需求去源码编译或者引入第三方维护的软件源(比如 EPEL 这类扩展仓库),那时候你已经有能力判断风险了。

2.3 版本搭配上的几个硬约束

这一节是我认为新手最容易忽略、但一出问题就抓瞎的地方。各个组件之间存在版本耦合,不是随便拉最新版凑一起就行。

第一,PHP 8 已经彻底移除了mysql_*系列函数。如果你拿到的老代码里还有mysql_connect()、mysql_query(),在 PHP 7 以后会直接报函数未定义。这不是配置问题,是代码必须改成mysqli或PDO。

第二,MySQL 8 默认的认证插件是caching_sha2_password,旧版本的 PHP 客户端驱动可能不认,连接时抛"认证插件无法加载"。解决办法要么升级客户端库,要么在创建用户时显式指定IDENTIFIED WITH mysql_native_password。MariaDB 默认还是mysql_native_password,所以兼容性上省心一些,这也是很多教程推荐 MariaDB 的原因之一。

第三,PHP 版本和 OpenSSL 版本会打架。老的 PHP 7.x 在较新的系统上编译时,经常因为 OpenSSL 1.1 的 API 变化而报错。反过来,某些新 PHP 版本需要的最低 OpenSSL 版本也有要求。用包管理器能绕开这类问题,因为它给你的是已经验证过的组合。

第四,PHP 8.1 之后对"传 null 给非可空参数"的行为从警告变成了弃用提示,PHP 9 会变成错误。如果你的项目里有大量老代码,升到 8.1+ 会刷出一屏弃用警告,虽然不影响运行,但日志会被淹没,排查真问题的时候很烦。

一个相对稳的搭配思路:CentOS 7 时代的经典组合是 Apache 2.4 + PHP 7.4 + MariaDB 10.4;较新的系统上,Nginx 1.20+ / Apache 2.4 + PHP 8.0/8.1 + MariaDB 10.6 是比较常见的生产组合。不要追最快的最新版,选一个发布了一段时间、社区资料充足的版本,遇到问题能搜到答案比用上新特性重要得多。

3. LAMP 实战:Apache + PHP + MariaDB 从零跑通

3.1 装 Apache 和 MariaDB,先把静态页跑起来

我习惯把链路拆成两段来验证:先确认 Web 服务器能对外提供静态内容,再接 PHP。这样一旦出问题,责任范围很清晰。

# RHEL 系 yum install -y httpd mariadb-server systemctl enable --now httpd systemctl enable --now mariadb # Debian 系 apt install -y apache2 mariadb-server systemctl enable --now apache2 systemctl enable --now mariadb

两条systemctl命令我特意写了enable --now,意思是"设为开机自启并立刻启动"。很多教程只写start,结果重启机器后服务全没了,找了半天原因。

接下来放行端口。防火墙没开的话可以跳过,但云主机默认基本都开着:

# RHEL 系 firewalld firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --reload # Debian 系 ufw ufw allow 80/tcp ufw allow 443/tcp

验证一下:在/var/www/html/(RHEL 系)或/var/www/html/(Debian 系)下写个测试页,然后本地 curl 一下。

echo '<h1>web server ok</h1>' > /var/www/html/index.html curl -I http://127.0.0.1/

返回 200 就说明 Web 层通了。这里有个 RHEL 系特有的坑:/etc/httpd/conf.d/welcome.conf这个文件里配了一条重定向规则,会把默认站点的访问跳到一个测试页面,导致你放在DocumentRoot下的首页被忽略,浏览器里怎么刷新都是那个欢迎页。我第一次遇到时以为是缓存,清了浏览器缓存、换了浏览器、重启服务都没用,最后才发现是配置文件的问题。处理方式是把welcome.conf里那条RedirectMatch注释掉或者直接把这个文件移除,然后重启 httpd。

还有一点,DocumentRoot改到别的目录后,必须在配置里同步写<Directory>段放开权限,否则默认只有/var/www/html被授权,新目录会返回 403。

3.2 PHP 接入的两种姿势:mod_php 与 PHP-FPM

这是 LAMP 里最需要理解清楚的一环,因为它直接关系到内存模型。

方式一:mod_php。PHP 作为 Apache 的一个模块加载,Apache 处理到.php文件时直接调用内嵌的解释器执行,不经过任何网络协议,链路最短。

# RHEL 系 yum install -y php php-mysqlnd php-gd php-mbstring php-xml php-opcache systemctl restart httpd # Debian 系 apt install -y libapache2-mod-php php-mysql php-gd php-mbstring php-xml php-opcache systemctl restart apache2

装完之后写个info.php:

<?php phpinfo(); ?>

浏览器访问能出那一大页紫色表格就算通了。页面跑通之后请立刻删掉这个文件,phpinfo()会把服务器路径、PHP 版本、已装扩展、甚至环境变量全暴露出来,留在线上就是一份免费的侦察报告。

mod_php 的代价前面提过:它非线程安全,必须搭配preforkMPM,每个 Apache 子进程都带一份 PHP 解释器。我实测过一个装了常用扩展的 PHP 进程,常驻内存在 30 到 50MB 之间浮动,MaxRequestWorkers开到 150 的话,光 Apache 就要吃掉 4.5G 到 7.5G 内存。所以 mod_php 的并发上限其实是内存上限。

方式二:PHP-FPM。PHP 独立成一组进程,Apache 通过mod_proxy_fcgi把请求转发给 FPM,走的是 FastCGI 协议。

yum install -y php-fpm systemctl enable --now php-fpm

Apache 这边要加载代理模块并配置转发规则。推荐用 Unix socket 方式:

# 在 VirtualHost 或 Directory 段内 <FilesMatch \.php$> SetHandler "proxy:unix:/run/php-fpm/www.sock|fcgi://localhost" </FilesMatch>

也可以用ProxyPassMatch:

ProxyPassMatch ^/(.*\.php(/.*)?)$ unix:/run/php-fpm/www.sock|fcgi://localhost/var/www/html/

两种写法效果类似,前者更贴合 Apache 的 Handler 语义,后者在处理 PATH_INFO 类 URL 时更灵活。注意ProxyPassMatch里路径的拼接逻辑,很多 502 或者 404 就是这里少写或多写了一个斜杠。

改用 FPM 之后,Apache 的 MPM 就可以切到event模式,MaxRequestWorkers的上限不再受 PHP 内存限制,因为 PHP 的进程数由 FPM 自己的pm.max_children独立控制。这个解耦是 LNMP 架构相对 LAMP 最大的优势之一。

3.3 数据库初始化、建库建用户与字符集

MariaDB 装完默认是空密码的 root,任何人都能从本机连上,必须先跑一遍mysql_secure_installation。

mysql_secure_installation

它会依次问几件事:是否设置 root 密码(设)、是否删除匿名用户(删)、是否禁止 root 远程登录(禁)、是否删除 test 库(删)、是否重新加载权限表(是)。每一项的默认答案都是 "Y",直接回车即可。

然后建应用专用的库和账号。应用绝对不要用 root 连数据库,一旦代码有注入漏洞,root 权限意味着整个实例沦陷,而专用账号只能动自己那个库。

CREATE DATABASE app_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'app_user'@'localhost' IDENTIFIED BY '一个足够长的随机密码'; GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'localhost'; FLUSH PRIVILEGES;

字符集这里要特别说明。utf8mb4不是utf8的别名,而是真正的 4 字节 UTF-8,能存表情符号和部分生僻字。MySQL/MariaDB 里的utf8实际上是utf8mb3,只支持 3 字节,存 emoji 会报错或者变成问号。新项目一律用utf8mb4,老项目迁移时也要注意这个区别,我见过用户昵称里带表情注册失败的案例。

改动字符集还要动配置文件,否则服务端默认值还是老的:

# /etc/my.cnf.d/server.cnf 或 /etc/my.cnf [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci

另外把监听地址收一下。默认 MariaDB 只监听 127.0.0.1,如果之前为了远程连接改成了0.0.0.0,记得改回来——数据库端口暴露在公网上被暴力破解是每天都在发生的事。

[mysqld] bind-address = 127.0.0.1

3.4 基于域名的虚拟主机配置

一台机器上跑多个站点是最常见的需求。Apache 靠VirtualHost按域名分流,请求头里的Host字段决定落到哪个配置块。

<VirtualHost *:80> ServerName app.example.internal DocumentRoot /var/www/app/public <Directory /var/www/app/public> Options -Indexes +FollowSymLinks AllowOverride All Require all granted </Directory> ErrorLog /var/log/httpd/app_error.log CustomLog /var/log/httpd/app_access.log combined </VirtualHost>

几个参数值得解释。Options -Indexes关掉目录浏览,不然别人访问一个没有首页的目录,会看到完整文件列表。AllowOverride All允许.htaccess生效,这是很多 PHP 框架(比如那些用单一入口index.php处理所有路由的)必需的,作用是让所有不存在的路径都重写到入口文件。Require all granted是 Apache 2.4 的访问控制语法,2.2 时代的Order allow,deny已经废弃,抄老教程会直接 403。

测试的时候改本机 hosts 文件把域名指过去,不要图省事直接用 IP 访问——用 IP 访问时Host头是 IP,匹配不到任何ServerName,会落到第一个 VirtualHost 上,表现出来的现象是"配了三个站,访问全是第一个站的内容"。这个坑我见过太多次。

数据库连通性也要单独验一遍,别等到装完应用才发现连不上:

<?php $dsn = 'mysql:host=127.0.0.1;dbname=app_db;charset=utf8mb4'; try { $pdo = new PDO($dsn, 'app_user', '密码'); echo 'db ok'; } catch (PDOException $e) { echo 'db fail: ' . $e->getMessage(); }

注意host写的是127.0.0.1而不是localhost。这两个在 PHP 的 MySQL 驱动里行为不同:localhost走 Unix socket 文件,127.0.0.1走 TCP。如果 socket 路径不对或者权限有问题,写localhost会报"无法通过 socket 连接",改127.0.0.1反而能通。排查连接问题时,这两个要分别试一遍。

4. LNMP 实战:Nginx + PHP-FPM + MariaDB

4.1 Nginx 与 PHP-FPM 的握手方式:Socket 还是 TCP

Nginx 自己不会执行 PHP,它把请求通过 FastCGI 协议转给 PHP-FPM。这个"转"有两种通道,选哪个有实际影响。

Unix Domain Socket:FPM 监听一个文件(比如/run/php-fpm/www.sock),Nginx 直接往这个文件写数据。好处是不走网络协议栈,开销小、延迟低、不占用端口,而且权限可以用文件权限来控制。缺点是只能同机通信。

; php-fpm 池配置 listen = /run/php-fpm/www.sock listen.owner = nginx listen.group = nginx listen.mode = 0660

listen.owner和listen.group决定了这个 socket 文件归谁所有。如果 Nginx 的运行用户和这里写的对不上,就会 502。RHEL 系上 Nginx 的默认用户是nginx,Apache 是apache,Debian 系上 Nginx 和 Apache 都是www-data,PHP-FPM 的池配置里也要对应改。这种用户名不一致导致的 502 是新手最常见的翻车点之一。

TCP 端口:FPM 监听127.0.0.1:9000,Nginx 通过fastcgi_pass 127.0.0.1:9000;连过去。好处是可以跨机器、方便容器化部署、可以用ss直接看到连接状态。缺点是多了 TCP 握手和协议栈开销,同机场景下没必要。

listen = 127.0.0.1:9000

我的选择:单机部署一律用 socket,出问题也好查,ls -l看一眼文件的属主和权限就知道对不对。只有当 FPM 和 Nginx 不在同一台机器、或者跑在容器里需要跨网络命名空间时,才用 TCP。

4.2 一份能直接用的站点配置与伪静态规则

Nginx 的配置是分层的:main(全局)→events(连接处理)→http(HTTP 相关)→server(虚拟主机)→location(路径匹配)。理解了这五层,配置文件就不乱了。

server { listen 80; server_name app.example.internal; root /var/www/app/public; index index.php index.html; access_log /var/log/nginx/app_access.log; error_log /var/log/nginx/app_error.log; # 静态资源直接返回,不存在则 404 location / { try_files $uri $uri/ /index.php?$query_string; } # 交给 PHP-FPM 处理 location ~ \.php$ { fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_index index.php; include fastcgi.conf; } # 禁止访问隐藏文件 location ~ /\. { deny all; } }

try_files这一行是伪静态的核心。它的逻辑是:先找请求路径对应的文件,找不到就找同名目录,还找不到就把请求重写到/index.php并把原始参数带上。这样/article/123这种"看起来是目录其实是路由"的 URL 才能被前端控制器接住。

include fastcgi.conf和include fastcgi_params的差别值得说一句。fastcgi_params只定义了各种 FastCGI 参数,没有SCRIPT_FILENAME;fastcgi.conf在它的基础上补上了fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;。如果误用了fastcgi_params,PHP-FPM 收到的脚本路径是空的,会报 "Primary script unknown" 然后返回 404 或者 403。这个错误信息很抽象,新手往往卡很久。

location的匹配顺序也需要记一下,不然规则写多了会互相打架:

优先级语法说明
1location = /path精确匹配,命中即停
2location ^~ /path前缀匹配且命中后不再检查正则
3location ~ \.php$正则匹配,按书写顺序,第一个命中生效
4location /path普通前缀匹配,取最长的那条

实际影响是这样的:如果你写了location ~ \.php$把 PHP 转给 FPM,同时又写了一个正则location ~ ^/upload/,那么/upload/test.php会命中先写的那个。规则顺序不对,就会出现"上传目录的 PHP 被当普通脚本执行了"这种安全问题。上传目录必须单独用^~或者精确匹配拦掉 PHP 执行,这是基本的防护。

4.3 PHP-FPM 进程池参数怎么算才不炸内存

这一节是我认为 LNMP 调优里最需要动脑子、也最容易抄错的地方。pm.max_children不是越大越好,开大了直接 OOM,开小了并发一上来就 502。

先看核心参数:

pm = dynamic pm.max_children = 50 pm.start_servers = 10 pm.min_spare_servers = 5 pm.max_spare_servers = 20 pm.max_requests = 500

pm有三种模式。static固定进程数,适合负载稳定的大内存机器;dynamic按需伸缩,最常用;ondemand是请求来了才起进程,节省内存但响应有延迟,适合低频访问的场景。

pm.max_children的计算公式其实很简单:

max_children = FPM 可用内存 ÷ 单进程平均内存

假设机器 4G 内存,系统加 Nginx 加 MariaDB 加上其他杂项占用约 1.5G,留给 FPM 的是 2.5G。实测单个 PHP 进程常驻 40MB(这个值取决于装了多少扩展,装得多的能到 60MB 以上),那么2500 ÷ 40 ≈ 62,取整保守一点设成55 到 60之间。

单进程内存怎么测?压一下然后看:

ps --no-headers -o "rss,cmd" -C php-fpm | awk '{sum+=$1} END {print sum/NR/1024" MB"}'

这条命令把当前所有 FPM 进程的常驻内存加起来求平均,单位换算成 MB。注意这个值在业务高峰期会比空闲时高,要用高峰期的数据来算。

pm.max_requests是每个子进程处理多少个请求后自动重启。这个参数的作用是兜住内存泄漏——PHP 代码写得再规范,某些第三方库也可能有轻微泄漏,跑久了进程越来越胖。设个 500 到 1000,进程定期"换新",内存曲线就平了。

超时这条链要一起看,不然会出现"PHP 还在跑,Nginx 已经返回 504 了":

层级参数建议值
Nginxfastcgi_read_timeout60s
PHP-FPMrequest_terminate_timeout60s
PHPmax_execution_time30s

原则是外层超时要大于等于内层。Nginx 等 60 秒,FPM 也允许跑 60 秒,PHP 自己限制 30 秒先掐断。如果反过来,Nginx 等 10 秒就断了,PHP 那边还在傻跑 60 秒,用户看到 504,日志里却查不到 PHP 报错。

再补一个排查神器,慢日志:

request_slowlog_timeout = 5s slowlog = /var/log/php-fpm/www-slow.log

执行超过 5 秒的请求会把当时的 PHP 调用栈写进慢日志,哪个函数、哪一行卡住的,一目了然。这个比在代码里到处打时间戳高效多了。

4.4 两套环境在同一台机器上共存

有时候需要两套都装着做对比测试,或者从 Apache 迁移到 Nginx 的过渡期两边都要跑。只要端口不冲突就能共存。

最简单的做法:Nginx 占 80,Apache 改到 8080。改 Apache 的Listen指令:

Listen 8080

对应的VirtualHost也改成<VirtualHost *:8080>。防火墙要额外放行 8080。

另一种做法是端口不变,靠域名区分。但同一个端口不能同时被两个进程监听,所以实际上还是得靠前置的反向代理,或者干脆让它们监听不同 IP(如果机器有多个网卡地址)。

共存的几个注意点:日志目录要分开,别让两边的access.log混在一起;DocumentRoot指向同一个目录时,注意两边的运行用户对文件权限的要求是否一致;开机自启只开需要长期跑的那个,另一个手动启动,避免重启后端口抢不到。

提醒:切换架构的时候不要直接停掉旧服务。先把新架构在同一台机器上用不同端口搭起来,验证所有功能正常,再改端口切换。我见过直接把 Apache 停了上 Nginx,结果发现伪静态没翻译对,全站 404,回滚又花了半小时。

5. 出问题往哪看:典型故障排查实录

5.1 502 与 504:PHP-FPM 链路上的三种断点

502 Bad Gateway 是 LNMP 里出现频率最高的错误,含义很明确:Nginx 把请求转给了上游,但上游没给出有效响应。按链路顺序排查,基本三分钟内能定位。

第一种断点:FPM 进程根本没起来。systemctl status php-fpm看状态,如果是 failed,先看它为什么起不来。最常见的是配置文件语法错误,用php-fpm -t做语法检查,它会直接告诉你哪一行有问题。还有一种是启动时报 "unable to bind listening socket",说明 socket 文件已经存在且被占用,或者端口被别的进程占了,ss -lntp | grep 9000能看出来。

第二种断点:socket 路径对不上。Nginx 配置里写的是/run/php-fpm/www.sock,FPM 实际监听的是/run/php/php8.1-fpm.sock,两边不一致。这种情况 Nginx 的错误日志里会写connect() to unix:/run/php-fpm/www.sock failed (2: No such file or directory)。核对方法很简单,ls -l /run/php*看一眼实际路径,然后两边对齐。

第三种断点:权限不够。socket 文件存在,但 Nginx 用户没权限读写。错误日志里是(13: Permission denied)。检查listen.owner和listen.group是否与 Nginx 的运行用户一致,listen.mode一般设 0660。目录的 x 权限也要有,/run/php-fpm/这个目录如果对 nginx 用户没有执行权限,照样连不进去。

504 Gateway Timeout 是另一回事,它表示连上了,但上游在超时时间内没返回。三个方向:PHP 代码本身跑太久(查慢日志)、数据库查询卡住(看SHOW PROCESSLIST)、或者是pm.max_children打满导致新请求排队。最后这种情况有个特征:502 和 504 交替出现,而且并发一降就恢复正常。/status页面能直接看到 FPM 的活跃进程数,把pm.status_path开一下就能访问。

5.2 403 与 404:路径、权限和 SELinux 的连环坑

403 Forbidden 表示服务器收到了请求但拒绝提供内容。按这个顺序查:

先看目录索引。请求的路径是个目录,里面没有index.php或index.html,同时又关了目录浏览,那就是 403。补个首页文件,或者确认index指令里配的文件名和实际文件名对得上。

再看文件权限。Nginx 或 Apache 的运行用户对目标文件至少要有读权限,对路径上的每一级父目录都要有执行权限。这点特别容易漏——/var/www/app/public/index.php权限是 644 没问题,但如果/var/www/app这个目录是 700 且属主是 root,Nginx 进都进不去,照样 403。用namei -l /var/www/app/public/index.php一条命令就能把整条路径的权限列出来。

然后是 SELinux。RHEL 系上这个是最隐蔽的。文件权限看着全对,ls -Z一看标签是user_home_t而不是httpd_sys_content_t,Web 服务器就是读不了。判断方法:

getenforce # 确认是否 Enforcing ls -Z /var/www/app/public/index.php # 看标签 ausearch -m avc -ts recent # 看最近的拒绝记录

处理方式不是setenforce 0,而是打对标签:

semanage fcontext -a -t httpd_sys_content_t "/var/www/app(/.*)?" restorecon -Rv /var/www/app

如果 PHP 需要往外发请求(比如调外部接口),还要开对应的布尔值:

setsebool -P httpd_can_network_connect 1

不开这个的话,PHP 里curl会静默失败,返回空,日志里什么都没有,只有 audit 日志里能看到 AVC 拒绝。这个坑我卡过整整一个下午。

404 Not Found 相对简单,就是找不到。root路径写错(少写或多写一层目录)、try_files规则把请求重写到了不存在的文件、location优先级导致请求被别的规则截走,这三类占了绝大多数。定位方法是在 Nginx 错误日志里看实际请求的文件路径,一比就知道差在哪。

5.3 数据库连不上的四类原因

PHP 报 "Connection refused" 或者 "Can't connect to MySQL server",按这个思路拆:

第一类,服务没起或者没监听对地址。systemctl status mariadb看状态,ss -lntp | grep 3306看监听。如果监听的是127.0.0.1:3306,而你从别的机器连,当然连不上。

第二类,账号的 host 不匹配。MySQL 的账号是用户名@主机的组合,'app_user'@'localhost'和'app_user'@'127.0.0.1'在 MySQL 眼里是两个不同的账号。用localhost连的时候走 socket,用127.0.0.1连的时候走 TCP,匹配的账号可能不一样。查一下现有账号:

SELECT user, host FROM mysql.user;

第三类,认证插件不兼容。前面提过,MySQL 8 的caching_sha2_password老客户端不认,报错信息里会明确写 plugin 相关的内容。改一下:

ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY '密码';

第四类,socket 路径不一致。PHP 的mysql.default_socket或pdo_mysql.default_socket指向的路径,和 MariaDB 实际生成的 socket 路径不同。/var/lib/mysql/mysql.sock和/run/mysqld/mysqld.sock都是常见位置,用mysqladmin variables | grep socket查实际值,然后对齐。

5.4 故障速查表

把上面这些整理成一张表,出问题的时候照着往下捋,比凭感觉猜快得多。

现象最可能的原因第一手排查命令
502 Bad GatewayFPM 未启动 / socket 路径错 / 权限不足systemctl status php-fpm、ls -l /run/php*
504 Gateway Timeout脚本超时 / max_children 打满 / 慢查询tail 慢日志、SHOW PROCESSLIST
403 Forbidden父目录缺 x 权限 / SELinux 标签错 / 无首页namei -l 路径、ls -Z
404 Not Foundroot 路径错 / try_files 规则错tail /var/log/nginx/error.log
页面空白无报错PHP 致命错误被吞 / 内存超限开display_errors、看 FPM 日志
中文乱码字符集不是 utf8mb4 / 连接未指定 charset查建库语句和 DSN
上传大文件失败client_max_body_size/upload_max_filesize/post_max_size三处一起改
静态资源 404 但动态正常location 规则被 PHP 正则截走检查 location 顺序

上传大小这个坑值得单独提一句,因为它涉及三个不同层级的参数,改一个没用:Nginx 的client_max_body_size(默认只有 1M)、PHP 的upload_max_filesize和post_max_size。三个都要改,而且post_max_size要大于等于upload_max_filesize。我见过只改了 PHP 没改 Nginx 的,上传 5M 文件直接返回 413,日志里写得清清楚楚,但如果不看 Nginx 日志根本想不到这一层。

6. 上线前该做的收敛动作

6.1 Web 服务器层的信息暴露面收窄

默认配置是给开发方便用的,直接暴露在公网上会泄露很多信息。几个必改项:

隐藏版本号。Nginx 的server_tokens off;放在http块里。Apache 的在配置里加ServerTokens Prod和ServerSignature Off。不改的话,响应头里会明明白白写着版本号,攻击者可以直接对着已知漏洞来。

禁止目录浏览。Nginx 是默认关闭的(除非你显式写了autoindex on),Apache 要确认Options -Indexes。

拦掉敏感文件。.git、.env、.svn、backup.sql这些,被扫到就是大事:

location ~ /\.(git|env|svn) { deny all; return 404; } location ~* \.(sql|bak|log|conf)$ { deny all; return 404; }

返回 404 而不是 403 是个小技巧,403 等于告诉对方"这个文件存在但你没权限",404 让他以为什么都没有。

上传目录禁执行。这个前面提过,再强调一遍,很多一句话木马就是靠上传目录能执行 PHP 得手的:

location ^~ /uploads/ { location ~ \.php$ { deny all; } }

6.2 PHP 配置的收紧与 OPcache 开启

php.ini里几个面向生产的调整:

expose_php = Off display_errors = Off log_errors = On error_log = /var/log/php-fpm/php-error.log disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec,parse_ini_file,show_source upload_max_filesize = 20M post_max_size = 24M max_execution_time = 30 memory_limit = 256M

disable_functions这一项要谨慎,禁用太狠会把正常业务也干掉。我见过有人照着网上的清单全禁了,结果项目里用proc_open调外部命令的功能直接崩了。原则是先按默认跑,出问题再逐个关,或者反过来,先全关,测试所有功能,把必须用的加回来。

open_basedir是另一道防线,限制 PHP 只能访问指定目录:

open_basedir = /var/www/app:/tmp:/var/lib/php/session

配了之后,即使代码被注入了读取/etc/passwd的逻辑,也会被拦下来。代价是路径要写全,漏了哪个目录就会报 "open_basedir restriction in effect",排查时容易懵。

OPcache 是必开的,PHP 默认每次请求都要重新解析编译源码,开了之后编译结果缓存在共享内存里,性能提升非常明显,实测能到两三倍:

opcache.enable = 1 opcache.memory_consumption = 128 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 10000 opcache.validate_timestamps = 1 opcache.revalidate_freq = 60

validate_timestamps在开发环境设 1(改代码生效),生产环境如果发布流程规范,可以设 0 并配合 reload,性能再上一层;但如果是靠 FTP 直接改文件那种发布方式,设 0 会导致改了代码不生效,找半天原因。

6.3 数据库账号的最小权限实践

前面建库的时候给的是GRANT ALL PRIVILEGES ON app_db.*,对应用账号来说这个范围还可以接受(限制在自己库里),但更严格的做法是按需给:

-- 只读账号,给报表或者查询服务用 GRANT SELECT ON app_db.* TO 'app_reader'@'localhost'; -- 读写账号,给业务用 GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'localhost'; -- 建表权限单独给,只在部署时临时开 GRANT CREATE, ALTER, INDEX, DROP ON app_db.* TO 'app_deploy'@'localhost';

这样即使某个账号泄露,损失也是可控的——只读账号泄露了数据,但删不掉;业务账号被注入,也执行不了DROP TABLE。

另外几条硬规则:root 账号绝对不允许远程登录;所有空密码账号清掉;test库删掉(默认允许匿名访问的库);定期用SELECT user, host FROM mysql.user;盘一遍账号,测试期建的临时账号记得清理。

一个容易被忽略的细节:MariaDB 的mysql.user表里有plugin和authentication_string字段,从别的机器迁移过来的账号可能带着旧的认证信息,在目标机上验证不通过。迁移完记得用每个账号实际连一次,别只看 SQL 执行成功就以为没事。

7. 个人在长期使用中形成的几项习惯

搭环境和调优这件事,配置本身好抄,踩过的坑才是自己的。分享几个我坚持了很多年的做法。

第一个习惯:**每装完

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 6:08:09

模拟版图LVS报错排查与运放、电流镜匹配画法全解析

简介&#xff1a;《运放版图分解》演示文稿是西南科技大学网络教育系列课程《集成电路版图设计》的配套课件&#xff0c;聚焦运放版图分解&#xff0c;面向集成电路版图设计初学者、在校生及模拟版图工程师。资源包仅含一个演示文稿&#xff0c;体积约一点三二兆字节&#xff0…

作者头像 李华
网站建设 2026/10/26 6:05:29

MySQL数据库入门与核心技能实战指南

1. MySQL入门&#xff1a;从零开始掌握关系型数据库核心技能刚接触数据库开发时&#xff0c;我花了整整两周才搞明白如何正确安装配置MySQL。现在回想起来&#xff0c;如果当时有人能系统性地讲解这些基础知识&#xff0c;至少能节省80%的摸索时间。作为最流行的开源关系型数据…

作者头像 李华
网站建设 2026/10/26 6:03:43

AI写作工具如何解决毕业论文三大痛点

1. 毕业论文写作的痛点与AI解决方案去年指导本科生论文时&#xff0c;有个场景让我印象深刻&#xff1a;凌晨两点收到学生微信&#xff0c;附带着第11版修改稿和三个流泪表情包。这反映了一个普遍现状——90%的毕业生在论文写作中都会遭遇三重暴击&#xff1a;文献综述像无底洞…

作者头像 李华
网站建设 2026/10/26 6:03:40

AI编剧脑如何提升短视频脚本创作效率与效果

1. 短视频创作的新战场&#xff1a;从拍摄内卷到内容智造去年帮朋友优化短视频账号时发现个有趣现象&#xff1a;同一个拍摄团队&#xff0c;用几乎相同的设备和人手&#xff0c;A账号单条视频平均播放量稳定在50万左右&#xff0c;B账号却只能做到5万。拆解了上百条视频后终于…

作者头像 李华
网站建设 2026/10/26 6:03:40

零基础学大模型:核心概念、应用开发到私有化部署全解析

1. 零基础学大模型&#xff1a;先搞清楚这些核心概念1.1 大模型到底"大"在哪里&#xff1a;参数、数据与算力很多人一听到"大模型"三个字&#xff0c;第一反应是&#xff1a;是不是参数多就叫大模型&#xff1f;对了一半&#xff0c;但不全对。我在给零基础…

作者头像 李华
网站建设 2026/10/26 6:03:38

CAN模型详解:向量级协同如何提升广告点击率预估

做广告点击率预估这些年&#xff0c;我遇到最多的一个争论就是&#xff1a;用户历史行为序列到底该怎么用。直接平均池化吧&#xff0c;信息丢得太狠&#xff1b;上个注意力机制吧&#xff0c;DIN 那套用下来又总觉得差口气。直到我把 CAN&#xff08;Feature Co-Action for Cl…

作者头像 李华