
LNMP这套组合我前前后后部署了没有一百次也有八十次了。Linux Nginx MySQL PHP听起来就是四个东西装一起但每次帮朋友排查服务器问题发现绝大多数故障根源其实在部署阶段就埋下了。这篇文章不打算贴一堆复制粘贴就能跑的命令完事而是把我这些年实际踩过的坑、验证过的配置、以及遇到问题时的排查思路从头到尾梳理一遍。不管是刚买第一台云服务器的新手还是被线上环境502折腾过的老手这篇都能给你一些参考。1. 整体思路LNMP到底是什么以及部署前必须想清楚的三件事1.1 先理解四个组件各司其职LNMP本质上是一个请求处理流水线。用户浏览器发出请求Nginx作为门户最先接收它负责处理静态文件并且把PHP相关的动态请求转交给后面的PHP-FPM进程。PHP-FPM解析执行PHP代码过程中如果需要读写数据就通过MySQL驱动程序跟数据库打交道拿到结果后再层层返回最终由Nginx拼装成完整的HTTP响应发给用户。把这套流程理顺了部署的时候就不会迷糊。很多人一上来就急着装包结果装完发现Nginx和PHP之间讲不上话或者PHP连不上MySQL其实就是没理解这个请求链路。先分清谁是前台、谁是后台、谁负责计算、谁负责存储后面配置起来逻辑就清晰了。1.2 版本选型别盲目追新稳定性和生态才是关键有不少人喜欢装最新版觉得新版本性能强、特性多。这个观念在开发环境没问题但在生产环境就得谨慎了。LNMP是一个组合系统每个组件都有自己的发布节奏和依赖关系版本之间是否兼容、第三方扩展是否跟上这些比“新”重要得多。举个典型的例子PHP 8.x 和 PHP 7.4 的底层有变化一些老项目的代码或者扩展在新版下会直接报错。MySQL 8.0 默认的认证插件是 caching_sha2_password而老版本的PHP mysqlnd 驱动不一定支持连接数据库时会提示认证失败。做技术选型时先去查一下各组件当前的稳定版本以及主流的开源程序比如常用的CMS框架官方推荐什么版本组合再以此为准通常最稳。我的个人习惯是Nginx用官方主线版本或者稳定版PHP用当前主流的小版本比如8.1或8.2这类已经过市场验证的MySQL用5.7或8.0系列具体看项目兼容性。总的原则是没有特殊需求就不要去当小白鼠。1.3 目录规划与权限设计部署前最容易被忽略的一步很多人部署LNMP程序目录随手就放在 /root 或者 /home 下权限直接 chmod 777跑起来倒是没问题但后续的安全隐患很大。目录规划应该在安装之前就确定好而不是等站点上线后再去挪。常规的做法是这样规划网站根目录放在 /data/wwwroot/站点名 下日志放在 /data/wwwlogs/ 下数据库数据目录单独放一块数据盘比如 /data/mysql。这样做的直接好处是程序、日志、数据彼此隔离即使某个站点出问题也不会把整个系统拖垮。升级备份的时候只需要针对对应的目录操作即可。权限方面我给的最重要的一条建议运行PHP的进程用户和Nginx的进程用户尽量统一或者让Nginx用户对站点目录有读权限、对日志目录有写权限。千万不要图省事用 root 跑服务。我见过很多服务器被入侵根源就是服务以 root 身份运行一旦PHP代码里有漏洞攻击者直接拿到系统管理权限后果不堪设想。2. 核心组件安装与联动Nginx、PHP、MySQL的配置要点2.1 Nginx安装与基础配置别小看这个“门户”Nginx的安装本身不复杂通过包管理器几行命令就能装完。但装完之后别急着启动先把关键配置理清楚。我觉得最核心的几项是运行用户、worker进程数、事件模型、以及Gzip压缩。运行用户建议单独创建一个系统用户比如 www专门用来跑Nginx和PHP-FPM这样权限边界清晰。worker进程数通常设置为CPU核心数可以在 nginx.conf 里通过worker_processes auto自动适配省心。事件模型用默认的 epoll 就可以这是Linux下的高效模型生产环境不需要刻意改动。Gzip压缩是提升访问速度的有效手段特别是对纯文本类资源HTML、JS、CSS压缩率很可观。配置的时候注意两点一是gzip_http_version建议 1.1二是gzip_min_length设置一个合理阈值比如1k小文件就不压缩了省CPU。还有一个很多新手不知道的设置server_tokens off;。这个配置可以隐藏Nginx的版本号减少被针对的风险。虽然不能算绝对安全但至少避免被扫描工具直接探测出版本然后查漏洞。2.2 PHP-FPM安装与配置动态请求的核心引擎PHP-FPM是PHP FastCGI Process Manager的缩写它负责监听来自Nginx的动态请求。装好PHP之后有几个配置文件需要仔细调整都在 php.ini 和 php-fpm.conf 以及 pool 配置里。首先说php.ini里的几个关键参数memory_limit单个PHP脚本可用的最大内存默认可能是128M根据项目需求调整不要无脑调高。设置过大会让系统内存变得紧张设置过小则大图片处理、复杂计算会直接报错。upload_max_filesize和post_max_size这两个要配合设置否则文件上传功能会出问题。一般来说post_max_size要比upload_max_filesize稍大一点。date.timezone必须设置否则PHP会一直报时区警告而且时间相关函数的结果都是错的。然后是 php-fpm 的 pool 配置一般位于 /etc/php-fpm.d/www.conf。重点看这几项listen有两种方式一种是监听TCP端口如 127.0.0.1:9000另一种是监听Unix Socket如 /dev/shm/php-cgi.sock。我的经验是本机部署用Unix Socket性能更好因为省掉了TCP协议栈的开销但如果是Nginx和PHP在不同服务器上就必须用TCP。pm进程管理方式动态模式下会按需创建子进程静态模式则是固定进程数。对大多数场景动态模式pm dynamic够用。但要注意pm.max_children、pm.start_servers、pm.min_spare_servers、pm.max_spare_servers这几个参数之间的关系它们共同决定了PHP进程的数量在不同负载下的伸缩策略。max_children的估算方法我平时是这样做的先观察单个PHP进程平均占多少内存然后用服务器可用内存除以这个值得出的就是理论上限。比如单进程占80M服务器剩余2G可用那就设置max_children 25左右留一些余量给系统和其他进程。2.3 MySQL/MariaDB安装与初始化数据安全是底线安装MySQL之后第一条建议就是运行mysql_secure_installation这个脚本会引导你设置root密码、删除匿名用户、禁用root远程登录、删除测试数据库。很多初学者跳过这一步导致数据库裸奔这是非常危险的习惯尤其是服务器有公网IP的情况。字符集方面建议统一使用 utf8mb4它能完整支持中文和emoji是目前最推荐的字符集。MySQL 8.0 默认就是 utf8mb4如果是5.7需要确认配置文件里的character-set-serverutf8mb4和collation-serverutf8mb4_unicode_ci是否已设置。关于存储引擎如果遇到旧项目可能会看到 MyISAM 表新部署的项目建议直接用默认的 InnoDB。InnoDB 支持事务、行级锁、崩溃恢复对数据完整性有保障。MyISAM 在崩溃后损坏的概率相对高一些恢复起来也麻烦。如果服务器内存不大比如2G注意调整innodb_buffer_pool_size。这个参数控制InnoDB缓存池大小直接影响数据库性能。经验值设成物理内存的50%-70%。如果设太大系统内存不足时会开始使用Swap反而拖慢整体性能。另外MySQL的慢查询日志平时建议打开slow_query_log 1并设置一个阈值比如long_query_time 2。这样以后排查接口慢、页面卡的时候有数据可以看。3. 把三个组件串起来站点配置与动态请求处理3.1 配置一个标准的server块从零开始最稳的写法环境装好之后真正的重头戏是配置一个站点。我习惯在/etc/nginx/conf.d/下为每个站点单独建一个配置文件比如mysite.conf这样跟默认配置隔离管理起来也清晰。一个最基础的server块需要包含下面几项server { listen 80; server_name example.com www.example.com; root /data/wwwroot/example; index index.php index.html; access_log /data/wwwlogs/example.log main; error_log /data/wwwlogs/example_error.log; location / { try_files $uri $uri/ 404; } location ~ \.php$ { fastcgi_pass unix:/dev/shm/php-cgi.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }server_name是域名绑定root是网站根目录index指定默认索引文件。try_files这一行的意思是先尝试按请求路径找文件找不到再找目录都找不到就返回404。这里有一个经典问题如果没有配好try_files有些伪静态规则就没法生效。location ~ \.php$这个正则块把所有以.php结尾的请求转发给PHP-FPM。注意fastcgi_param SCRIPT_FILENAME这一行它告诉PHP实际要执行的脚本路径很多500问题就是这一行写错了比如用了$document_root但实际系统变量名不对导致PHP找不到文件。3.2 PHP请求转发到FPMfastcgi_pass的两种形态和坑fastcgi_pass是关键的一行两种写法都有人用。一种是我们上面写的unix:/dev/shm/php-cgi.sock另一种是127.0.0.1:9000。如果你用的是Unix Socket务必确认PHP-FPM的listen配置和这里完全一致否则Nginx会报connect() failed。这里有一个很容易踩的坑Socket文件的权限。如果PHP-FPM以www用户创建了Socket文件但Nginx的worker进程无法访问这个文件连接就会失败。排查的时候先看/dev/shm/php-cgi.sock的文件权限确认Nginx用户有访问权限。另外Socket路径不要放在普通临时目录建议放/dev/shm或者/tmp并且确保系统重启后目录存在不会出现Socket丢失的情况。如果你想用TCP方式127.0.0.1:9000 配置起来更直观排查时用telnet 127.0.0.1 9000就能验证端口是否通了。对于新手我反而推荐先用TCP方式因为好排查等熟练之后再切换到Unix Socket追求那一点点性能提升。3.3 静态文件与伪静态pathinfo和rewrite的日常问题LNMP环境下伪静态配置是使用最频繁的。许多PHP框架和CMS都依赖URL重写把请求统一引导到入口文件比如index.php。最常见的配置是这样location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php?s$1 last; } }这个写法解决的是诸如example.com/user/123这样的路由系统按照规则重写到index.php。要注意的坑是if指令在Nginx里有一些历史遗留的语义问题虽然不是不能用但尽量把if放在location内不要试图用if做复杂逻辑判断。另一个常见问题是pathinfo。比如访问example.com/index.php/1.htmlNginx默认配置下SCRIPT_FILENAME可能指向index.php/1.htmlPHP会找不到文件导致404。解决办法是在fastcgi配置中设置fastcgi_split_path_info或者用try_files $uri $uri/ /index.php$is_args$args;这种更现代一点的写法把请求安全地交给入口文件。静态文件的缓存也很重要。我的经验是给图片、CSS、JS这类文件设置一个较长的浏览器缓存过期时间同时给Nginx加上expires 30d;。但要注意如果上线了CDN或者更新了样式文件缓存太久可能导致用户看不到更新效果所以版本号文件名策略是行业里通行做法打包工具默认就能处理。4. 常见问题与排查实录4.1 502 Bad Gateway遇到这个错误先查这三处502是我被问到最多的问题字面意思是Nginx作为网关从上游PHP-FPM那里收到了无效响应。排查步骤我的习惯是固定的。第一步看PHP-FPM是不是还活着。用ps aux | grep php-fpm看进程是否存在。如果进程已经没了再查一下PHP-FPM的错误日志看是不是进程崩溃了常见原因是max_children设置太小请求高峰期进程不够用新请求无法创建。第二步如果进程活着就检查Nginx配置里fastcgi_pass是否跟PHP-FPM的listen一致。这一步经常出问题。我刚用Unix Socket的时候折腾了很久才发现是目录不对socket根本没生成在Nginx配置的位置。第三步查看系统日志和PHP错误日志比如/var/log/php-fpm/error.log。如果PHP脚本执行超时或者发生了致命错误FPM会直接断开连接Nginx这边自然就502了。定位到具体脚本看看是不是有死循环或者资源耗尽的情况。注意502和504是两个不同的概念。502表示上游服务不可用或响应无效而504表面上是超时也就是上游处理了但花的时间太久。排查思路完全不同别混为一谈。4.2 504 Gateway Timeout不是一个参数就能解决的504一般由两大类原因引起。一类是Nginx到FPM之间的通信超时配置项是fastcgi_read_timeout默认可能只有60秒。如果某个接口确实需要跑很久比如导出报表、批量处理图片你可以针对这类接口单独放开超时时间。另一类是PHP-FPM或者后端MySQL处理慢。比如MySQL的wait_timeout设置过短执行了一个大查询卡住了那前面的一切超时设置都没用。真正排查要顺着请求链往下追看是PHP卡在计算上还是卡在数据库查询上。我曾经处理过一个报表导出的崩溃问题用户一导就报504。查到最后是PHP里调用了大量的file_get_contents去访问外部接口每个接口响应都要十几秒把请求时间拖爆了。后来改成异步任务队列问题迎刃而解。所以看到504不要只盯着超时参数要从业务逻辑里找根源。4.3 404与403的典型case索引文件、目录权限、SELinux404最常见的原因有三个。一是index配置不正确。比如站点目录里只有index.php但你在Nginx配置里写的是index index.html;自然找不到。二是伪静态规则没有生效。三是路径写错root设置了但里面的子目录实际不存在。403的原因就更有意思了。最先想到的应该是目录权限。如果Nginx用户对网站根目录没有读权限或者对某子目录没有执行权限就会403。执行权限是经常被忽略的一项Linux下要进入目录需要目录有x权限。你可以用ls -ld查看目录权限确保是755而不是644。还有一个隐蔽的元凶SELinux。如果你在配置文件里开启了SELinux而且不太熟悉它大概率会遇到诡异的403或连接失败的状况。最简单粗暴的验证方式是用setenforce 0临时关闭如果问题消失说明就是SELinux策略拦截。但我不建议直接关闭而是用audit2why或者ausearch查一下审计日志把对应端口或路径的SELinux策略放行。生产环境为了安全SELinux尽量别关。4.4 资源使用的日常监控不要等到卡死了再查部署完成不是终点日常运维更考验功力。我建议装一些轻量级的监控工具比如用系统自带的top、vmstat、iostat快速查看资源使用情况另外ss -ant看当前的网络连接数。下面是我排查问题时会重点留意的几个指标整理成了表格方便对照指标正常参考区间异常时可能的信息CPU空闲率长期应高于20%若有持续高占用查是否有daemon进程或者恶意脚本内存使用率70%以内较健康超出后系统开始用swap性能急剧下降检查PHP进程数磁盘I/O等待低于5%超过10%说明磁盘是瓶颈看是否有慢查询或日志写入过猛TCP连接数视站点流量而定突然飙升可能是被攻击或并发处理不过来PHP-FPM内存占用单进程50~200M居高不下考虑是不是内存泄漏这些指标不一定每天都盯但服务器如果出现卡顿、响应变慢第一时间把数据拉出来看看能省很多排查时间。5. 部署完成后必做的加固与优化5.1 安全基线目录权限、禁PHP解析、版本号隐藏LNMP部署完之后必须做一次安全加固。首先是文件权限收紧。我的默认习惯是站点目录设为755文件设为644只有需要写入的目录比如 upload、cache、tmp单独设置为www用户可写。使用chmod之前要想清楚不要一个chmod -R 777全解决了。其次是禁掉无用目录中的PHP解析权限。最常见的是upload和data目录攻击者如果上传了图片马一旦这些目录能解析PHP就能直接执行恶意代码。做法是给这些目录单独加一个location明确禁止PHP解析location ~* ^/upload/.*\.(php|php5)$ { deny all; }然后是隐藏版本号。前面提到的server_tokens off是最基础的一层。PHP也可以设置expose_php Off这样响应头里就不会带PHP版本号。这些措施虽然挡不住高手但至少能挡住一部分自动化扫描流量。最后建议修改SSH默认端口、禁用root密码登录、使用密钥认证。这部分和LNMP强相关因为数据库和Web服务都跑在这台机器上系统自身的防线崩溃了应用层再安全也没用。5.2 几个收益超明显的小配置Opcache、日志与连接池LNMP部署完性能调优不应该一上来就买很高配置的服务器先把软件层面的优化做了很多时候效果立竿见影。PHP的opcache开启几乎是必须的。PHP是解释型语言默认每次请求都会重新解析源码文件开启opcache后解析结果会缓存在内存里省掉了重复解析的开销对CPU的消耗能明显降下来。在 php.ini 里确认opcache.enable1然后设置合理的opcache.memory_consumption比如 128M 起步观察命中率再做增减。Nginx层面的keepalive也要注意。如果你的PHP请求需要经过Nginx转发那么Nginx到后端的连接如果可以复用就能减少频繁建连的开销。PHP-FPM进程池里也有listen.allowed_clients等安全参数默认不用动但如果有多个IP来源需要加白名单。日志方面访问日志建议开启并且定期切割。Nginx自带的logrotate配置一般就够用。但要注意日志别无限增长尤其是错误日志如果某个脚本持续报错磁盘很快会被撑爆让系统进不去。我曾经遇到过因为日志把根分区写满导致整个服务不可用的惨案从那以后我定期巡检日志目录大小。5.3 备份策略与回滚演练最后一道防线LNMP部署好了投入生产备份就成了和部署同等重要的事。我的备份策略是分层级的网站代码每天增量备份、每周全量备份数据库每天凌晨全量备份备份保留最近7天和最近4周。数据库备份工具最简单的是mysqldump但要注意写法。生产环境建议用类似这样的命令mysqldump -u用户名 -p密码 数据库名 --single-transaction --quick --routines --triggers | gzip backup.sql.gz--single-transaction参数非常关键它让备份在事务中进行不锁表避免备份期间影响线上读写。--routines和--triggers把存储过程、触发器一起备份缺了它们恢复数据时会不完整。备份文件一定要异地存放不能跟数据放在同一台服务器。我碰过一台机器硬盘损坏连备份一起丢掉的悲剧。最简单的办法是备份完自动同步到另一台存储机或对象存储成本很低。另外还有一个容易被忽略的点备份之后要做恢复演练。半年测一次都行但必须测。只备份不验证真到需要恢复的那天可能发现备份文件是坏的那种打击比平时任何故障都难受。6. 一些想留给新手的经验LNMP部署这条路我走了很多弯路。以前为了追求性能把每个参数都调到极端结果稳定性出了问题后来才明白所谓的生产环境部署最重要的目标是“可预期”其次是“可维护”最后才是“性能极致”。如果你刚刚开始接触LNMP我给你的建议是先用默认配置完整跑通一遍然后再一个个去调整参数每次只改一个观察效果。不要上来就抄一个所谓“性能最强配置”因为不同的服务器配置、不同的业务形态适用的参数往往差别很大。更不要在一次部署里同时改十几个参数出了问题你根本不知道是哪个引起的。部署LNMP不是目的跑起稳定的业务才是。真正把请求链路、目录规划、权限边界、备份策略这些基础功夫做扎实了后期维护会轻松非常多。这些积累都会在你某天半夜被电话叫醒排查故障的时候变成你从容不迫的本钱。