LAMP/LNMP部署调优与Nginx/PHP-FPM故障排查
发布时间:2026/10/26 6:09:04 锦皓数字建站

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 采用的是多进程/多线程模型靠 MPMMulti-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-servicehttp firewall-cmd --permanent --add-servicehttps firewall-cmd --reload # Debian 系 ufw ufw allow 80/tcp ufw allow 443/tcp验证一下在/var/www/html/RHEL 系或/var/www/html/Debian 系下写个测试页然后本地 curl 一下。echo h1web 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-fpmApache 这边要加载代理模块并配置转发规则。推荐用 Unix socket 方式# 在 VirtualHost 或 Directory 段内 FilesMatch \.php$ SetHandler proxy:unix:/run/php-fpm/www.sock|fcgi://localhost /FilesMatch也可以用ProxyPassMatchProxyPassMatch ^/(.*\.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_userlocalhost IDENTIFIED BY 一个足够长的随机密码; GRANT ALL PRIVILEGES ON app_db.* TO app_userlocalhost; 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.13.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:host127.0.0.1;dbnameapp_db;charsetutf8mb4; 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 MariaDB4.1 Nginx 与 PHP-FPM 的握手方式Socket 还是 TCPNginx 自己不会执行 PHP它把请求通过 FastCGI 协议转给 PHP-FPM。这个转有两种通道选哪个有实际影响。Unix Domain SocketFPM 监听一个文件比如/run/php-fpm/www.sockNginx 直接往这个文件写数据。好处是不走网络协议栈开销小、延迟低、不占用端口而且权限可以用文件权限来控制。缺点是只能同机通信。; php-fpm 池配置 listen /run/php-fpm/www.sock listen.owner nginx listen.group nginx listen.mode 0660listen.owner和listen.group决定了这个 socket 文件归谁所有。如果 Nginx 的运行用户和这里写的对不上就会 502。RHEL 系上 Nginx 的默认用户是nginxApache 是apacheDebian 系上 Nginx 和 Apache 都是www-dataPHP-FPM 的池配置里也要对应改。这种用户名不一致导致的 502 是新手最常见的翻车点之一。TCP 端口FPM 监听127.0.0.1:9000Nginx 通过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连接处理→httpHTTP 相关→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_FILENAMEfastcgi.conf在它的基础上补上了fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;。如果误用了fastcgi_paramsPHP-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 500pm有三种模式。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_timeout60sPHP-FPMrequest_terminate_timeout60sPHPmax_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 占 80Apache 改到 8080。改 Apache 的Listen指令Listen 8080对应的VirtualHost也改成VirtualHost *:8080。防火墙要额外放行 8080。另一种做法是端口不变靠域名区分。但同一个端口不能同时被两个进程监听所以实际上还是得靠前置的反向代理或者干脆让它们监听不同 IP如果机器有多个网卡地址。共存的几个注意点日志目录要分开别让两边的access.log混在一起DocumentRoot指向同一个目录时注意两边的运行用户对文件权限的要求是否一致开机自启只开需要长期跑的那个另一个手动启动避免重启后端口抢不到。提醒切换架构的时候不要直接停掉旧服务。先把新架构在同一台机器上用不同端口搭起来验证所有功能正常再改端口切换。我见过直接把 Apache 停了上 Nginx结果发现伪静态没翻译对全站 404回滚又花了半小时。5. 出问题往哪看典型故障排查实录5.1 502 与 504PHP-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.sockFPM 实际监听的是/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 且属主是 rootNginx 进都进不去照样 403。用namei -l /var/www/app/public/index.php一条命令就能把整条路径的权限列出来。然后是 SELinux。RHEL 系上这个是最隐蔽的。文件权限看着全对ls -Z一看标签是user_home_t而不是httpd_sys_content_tWeb 服务器就是读不了。判断方法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 或者 Cant connect to MySQL server按这个思路拆第一类服务没起或者没监听对地址。systemctl status mariadb看状态ss -lntp | grep 3306看监听。如果监听的是127.0.0.1:3306而你从别的机器连当然连不上。第二类账号的 host 不匹配。MySQL 的账号是用户名主机的组合app_userlocalhost和app_user127.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_userlocalhost 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 PROCESSLIST403 Forbidden父目录缺 x 权限 / SELinux 标签错 / 无首页namei -l 路径、ls -Z404 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 onApache 要确认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 256Mdisable_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 60validate_timestamps在开发环境设 1改代码生效生产环境如果发布流程规范可以设 0 并配合 reload性能再上一层但如果是靠 FTP 直接改文件那种发布方式设 0 会导致改了代码不生效找半天原因。6.3 数据库账号的最小权限实践前面建库的时候给的是GRANT ALL PRIVILEGES ON app_db.*对应用账号来说这个范围还可以接受限制在自己库里但更严格的做法是按需给-- 只读账号给报表或者查询服务用 GRANT SELECT ON app_db.* TO app_readerlocalhost; -- 读写账号给业务用 GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO app_userlocalhost; -- 建表权限单独给只在部署时临时开 GRANT CREATE, ALTER, INDEX, DROP ON app_db.* TO app_deploylocalhost;这样即使某个账号泄露损失也是可控的——只读账号泄露了数据但删不掉业务账号被注入也执行不了DROP TABLE。另外几条硬规则root 账号绝对不允许远程登录所有空密码账号清掉test库删掉默认允许匿名访问的库定期用SELECT user, host FROM mysql.user;盘一遍账号测试期建的临时账号记得清理。一个容易被忽略的细节MariaDB 的mysql.user表里有plugin和authentication_string字段从别的机器迁移过来的账号可能带着旧的认证信息在目标机上验证不通过。迁移完记得用每个账号实际连一次别只看 SQL 执行成功就以为没事。7. 个人在长期使用中形成的几项习惯搭环境和调优这件事配置本身好抄踩过的坑才是自己的。分享几个我坚持了很多年的做法。第一个习惯**每装完
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。