☰
LNMP架构从单机到三节点分离:全流程部署与踩坑实录
2026/10/14 1:20:19 网站建设 项目流程

翻到一份自己归档的架构笔记,编号789,标题写得特别直白:“单台构建lnmp(blog、zh)—>lnmp分离、网站架构(全)”。这基本就是我当年把一个个人站点从一台服务器硬扛,一路折腾到三节点分离的全过程记录。现在回头看,LNMP这套东西的搭建逻辑、分离思路、踩坑点,放到今天依然有参考价值。

我先把这套东西要跑什么说清楚:一个小型博客站点,外加一套中文内容管理系统。流量不算大,但对稳定性有要求——数据库不能随便丢,页面不能随便打不开。最开始图省事,所有东西塞进一台机器,Nginx、PHP-FPM、MySQL全在一起。后来出了一次比较典型的故障,才决定做LNMP分离。这篇把每个阶段的关键操作、配置、命令和排查经验完整写出来。适合两类人:一是想把LNMP手工一步步搭起来、搞清楚每个组件在干嘛的新手;二是站点已经跑在单机上、准备拆分却不知道怎么下手的同学。

1. 动手前的架构规划:先搞清楚这套环境到底要跑什么

1.1 站点画像:博客加内容管理系统,需求比想象中更实际

我先看了标题里的“blog、zh”,对应的是两个站点:一个博客,用来发布技术文章;一套中文内容管理系统,主要做栏目管理和内容发布。这两个站点共同点是:页面上动态内容多、都需要查数据库、都有后台登录入口、都有文件上传需求。

这个画像决定了后面的很多选择。数据库不能只装不优化,因为两个站点的每次页面打开都带着SQL查询;PHP需要支持文件上传和图像处理,否则文章配图传不上去;Nginx要考虑伪静态和缓存,不然动态页的响应速度会很感人。还有一个容易忽略的:两套程序跑在同一台机器上,PHP扩展需求不同,但PHP-FPM只能配一套全局环境,所以扩展集合必须取并集,后面装扩展的时候少装哪个都会出问题。

1.2 工具选型:为什么是手工搭建,而不是一键脚本或面板

当时摆在我面前的有三条路:一键安装脚本、集成面板、手工搭建。我把它们的真实表现做了个对比:

方案优点代价适合场景
一键脚本快,一个命令装完版本锁死,路径不透明,出问题很难排查临时环境、不想深究的纯使用
集成面板可视化,操作直观自带一堆用不上的组件,目录结构自己说了不算非专业维护者、重管理界面
手工搭建每个组件都知道装在哪、改了什么慢,需要理解基础原理想搞懂架构、后续要拆分扩容的人

我最终选手工搭建,理由很简单:我知道后面一定会做分离部署。一键脚本和面板都倾向于把所有组件“焊死”在一台机器上,拆分的时候迁移工作量反而更大。手工搭虽然慢,但每个软件的安装位置、配置文件、启动方式都心中有数,后面要搬到新机器上,照着同样的方式装一遍即可。

1.3 基础规划:IP、目录、账号,先定规矩再动手

分离架构不是从拆开那天才开始的,是从单机阶段就要预留思路。我当时的规划很简单:所有机器都使用内网私有IP,默认不开任何对外的高危端口。

单机阶段只有一台机器:10.0.0.10,Nginx、PHP-FPM、MySQL全在这一台。分离后的规划是:10.0.0.10只保留Nginx,承担外部流量入口和静态文件处理;10.0.0.11专门跑PHP-FPM,处理所有PHP动态请求;10.0.0.12放MySQL,数据库只允许内网应用节点访问。站点代码目录统一放在 /data/www 下面,blog和cms各一个子目录,数据库账号单独创建,权限按“最小可用”来给。这些规矩看着琐碎,但在拆分那天帮了大忙——因为所有文件的路径、端口、账号都是明确的,迁移时不需要临时猜。

2. 单机阶段实操:LNMP三件套从零跑到两套站点上线

2.1 Nginx安装与虚拟主机配置

我这里以常见的Linux服务端环境为例,Nginx可以直接用系统自带的包管理器安装,也可以装官方源里的版本。所谓“手工搭建”不是非要全部源码编译,而是指每个组件都由自己明确安装、明确配置,不依赖封装好的全家桶脚本。

# RHEL系 yum install -y nginx # Debian系 apt install -y nginx

安装之后,第一件事是配置虚拟主机。我的习惯是所有站点配置放在 /etc/nginx/conf.d/ 下,每个站一个文件,命名用站点域名,方便维护。博客站点的配置大概是这样的:

server { listen 80; server_name blog.example.com; root /data/www/blog; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }

这里有几个点需要特别说明。try_files $uri $uri/ /index.php?$query_string 这行是伪静态的核心,它让Nginx先找真实文件,找不到就把请求交给index.php处理,博客类程序的URL重写才有意义。fastcgi_pass 指向的是 127.0.0.1:9000,因为单机阶段PHP-FPM就在本机,用回环地址就行。SCRIPT_FILENAME 必须用 $document_root 而不是写死的路径,否则换了站点目录就会404。

2.2 PHP-FPM:扩展取舍与监听方式

PHP的安装我用系统对应组件包,然后额外安装 php-fpm 以及两个站点共同需要的扩展。对这种场景,最基础的扩展集合包括:php-fpm、php-cli、php-mysqlnd、php-curl、php-gd、php-mbstring、php-xml、php-zip。

yum install -y php-fpm php-mysqlnd php-curl php-gd php-mbstring php-xml php-zip

mysqlnd 很关键,老版本PHP连MySQL经常因为认证问题翻车,新版直接用mysqlnd驱动会省很多事。gd和zip主要用于内容管理系统的图片处理和文件管理。

装好之后,PHP-FPM默认监听方式有两种:Unix Socket和TCP端口。单机阶段,我选择Socket方式,也就是 listen = /run/php-fpm/www.sock,因为本机内部通信走Socket比走TCP少一层网络栈开销,性能更好、延迟更低。对应Nginx配置里的fastcgi_pass 也要改成 unix:/run/php-fpm/www.sock。

但这里必须提前说:如果一开始就打算做LNMP分离,那么PHP-FPM最终一定要切回TCP监听,因为跨机器传输没法用Socket。单机阶段先用Socket没问题,不过配置文件里要保留切换的注释说明,后面改起来不迷路。

2.3 MySQL初始化、建库与最小权限账号

MySQL的初始化是单机阶段最容易被跳过的一步。很多一键脚本其实已经帮你做了,但手工搭建时必须自己走一遍:初始化数据目录、设置root密码、关闭默认的匿名账号和测试库、配置bind-address。

mysqld --initialize-insecure systemctl start mysqld mysqladmin -u root password '你的root密码'

单机阶段MySQL只需要监听本机,所以 bind-address = 127.0.0.1 就够了,外部连不上数据库,安全上先兜一层底。建库和账号,我不会直接用root让程序连库。两个站点用两个独立的应用账号,密码单独设置:

CREATE USER 'blog'@'localhost' IDENTIFIED BY 'StrongP@ss_2024'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP ON blog_db.* TO 'blog'@'localhost'; FLUSH PRIVILEGES;

MySQL 8默认的认证插件是 caching_sha2_password,PHP这边只要是较新的mysqlnd驱动都能正常连。如果PHP版本太老,连接时会报 authentication plugin 错误,解决办法是把账号认证方式改成 mysql_native_password。这个坑当年我踩过,内容管理系统一连接就报错,查了半天才发现是认证插件的问题。

2.4 两套站点的目录布局与伪静态落地

目录规划我前面提过,统一放在 /data/www 下:/data/www/blog 是博客程序,/data/www/cms 是内容管理系统。两个站点的配置文件分开,分别有自己的server块、日志文件和访问规则。内容管理系统通常不止需要PHP解析,还需要伪静态规则,栏目页、文章页都要rewrite到入口文件,配置核心同样是 try_files 配合入口文件路径。

目录权限也是很多人栽的地方。PHP-FPM默认以某个用户运行,Nginx又以另一个用户运行,两者都要能读站点文件,但只有PHP运行用户需要写权限的地方是上传目录和缓存目录。如果图省事把整个站点目录 chmod 777,后台上传虽然能成功,但安全上等于给攻击者开了一扇门。我的做法是:代码目录属主设为PHP运行用户,目录权限755,只把 /data/www/blog/wp-content/uploads 这类上传目录设为755并单独持有属主,静态缓存目录同样单独控制。权限最小,又能满足运行需要。

2.5 单机部署中四个高频坑

第一个是Nginx的 index 配置缺失。只配了 index.php 没配 index.html,访问目录会直接403。

第二个是fastcgi_param 配置不完整。Nginx官方提供的 fastcgi_params 文件里不一定有 SCRIPT_FILENAME,需要手动补上,否则所有PHP请求都会返回空白页或404。

第三个是PHP-FPM的socket权限问题。用Unix Socket方式时,Nginx的工作用户如果没有socket文件的读权限,会频繁出现502。需要确认PHP-FPM配置中 listen.owner 和 listen.group 与Nginx运行用户一致。

第四个是上传大小限制。PHP默认 upload_max_filesize 只有2M,post_max_size 只有8M,文章配图稍大就会被静默丢弃。我通常改成 upload_max_filesize=20M、post_max_size=25M,并同步调整Nginx的 client_max_body_size,否则后端放宽了,前端Nginx还是拦着。

3. 单机架构的真实缺口:一次数据库占满CPU引发的拆机思考

3.1 压测数据:并发一到就卡,问题一目了然

单机阶段表面看一切正常,但我用压测工具顺手做了个简单测试:对博客首页连续发请求,观察机器负载。结果很能说明问题:

请求方式并发数总请求数观测结果
静态文件202000CPU占用低,响应稳定
动态首页5500响应变慢,出现排队
动态首页202000MySQL进程CPU高,PHP-FPM大量等待

这里面的逻辑很清晰:静态文件由Nginx直接处理,根本不进PHP和MySQL,性能很稳定;但动态请求一旦上来,每个请求都要PHP解析、查数据库、渲染页面,MySQL就成了瓶颈。单机上CPU、内存是共享的,MySQL一忙,PHP-FPM跟着等,Nginx也跟着排队,整个站点一起卡。

3.2 故障复盘:数据库抖动直接带崩全站

真正让我下决心拆机器的一次故障:当时正给内容管理系统批量导入文章,可能某条SQL没有索引,或者数据量一上来查询计划变了,数据库瞬间出现慢查询,进程数飙升。结果CPU占用接近100%,PHP-FPM请求大量积压,网站直接打不开,连SSH登上去敲命令都迟钝。

这次故障让我意识到单机架构的根本问题:组件之间没有任何隔离。数据库是整条链路中最底层、最容易出问题的环节,而在单机上数据库一抖,所有组件跟着一起遭殃。你说临时加索引、清缓存能解决问题吗?能,但下次换个姿势又会再犯。架构上的问题,不能靠操作去补。

3.3 LNMP分离的三种路线,以及为什么先拆数据库

LNMP分离不是只有一种拆法,我当时认真对比了三条路线:

路线做法优点缺点
只拆数据库Nginx+PHP留在原机,MySQL移到新机改动最小,见效快PHP与Nginx仍互相抢占资源
Web与PHP分离Nginx一台、PHP+MySQL共一台静态处理独立数据库仍和其他服务混跑
三节点全分离Nginx、PHP-FPM、MySQL各一台隔离最彻底,扩展最灵活节点多,运维成本高

我最终选了三节点全分离。倒不是因为它最“高级”,而是考虑到后面要加新站点、可能引入缓存层,与其拆一半以后又动一次,不如一步到位,把三层职责彻底分开。拆分的顺序上,我从数据库开始,因为它是数据最核心、最容易成为瓶颈的一层;数据库独立之后,PHP应用层再拆走,最后Nginx只剩下流量分发和静态文件,职责最清晰。

3.4 分离前的成本与收益核算

有人可能会问:一个小站真的有必要三台机器吗?这个问题我也认真想过。从纯算力角度看,单机确实够用,但费用不只是CPU核数。三台机器可以按自己的节奏做备份、维护、重启,数据库节点独立后,站点代码随便改、Nginx随便reload,都不需要担心影响数据库连接。一台机器上做任何操作都提心吊胆,那种隐性成本比多一台机器的钱贵得多。当然,如果你的站点确实只有极低流量,或者只是在学习阶段,那就先别拆,单机好好做着,等出现我上面说的“一查就卡”的情况再动手不迟。

4. 分离实施全记录:三台机器各司其职

4.1 数据库节点迁移:导出、传输、导入、校验四步走

数据库迁移是整个分离工程里风险最大的一步,必须按部就班来。我先在10.0.0.12上把MySQL装好,初始化方式和单机阶段一样,唯一区别是 bind-address 要改成 10.0.0.12 或 0.0.0.0,因为PHP节点要从网络访问它。

第一步,在原机器上导出两个库:

mysqldump --single-transaction -u root -p blog_db > blog_db.sql mysqldump --single-transaction -u root -p cms_db > cms_db.sql

--single-transaction 很重要,它基于事务做一致性快照,导出过程中不会锁表,博客还能正常写入。第二步,把SQL文件传到新节点:

scp blog_db.sql root@10.0.0.12:/tmp/

大库建议先压缩再传输,mysqldump出来的文本冗余很高,gzip之后往往能小一个数量级。第三步,在10.0.0.12上导入:

mysql -u root -p blog_db < /tmp/blog_db.sql

导完先别急着切流量,先在数据库节点上看表数量和行数是否正常。第四步校验一致性:在新节点查一下关键表的总行数,跟导出时的记录对比;如果是内容管理系统,直接登录后台看文章列表是否完整。

这里有一个坑:mysqldump默认导出内容包含建表语句,但不一定包含视图、函数、触发器。如果站点用了存储过程或触发器,导出时要加上 --routines --triggers。我这两个站没用这些,但养成习惯带上比较保险。

4.2 PHP应用节点迁移:代码同步与连接串改动

数据库节点准备好之后,开始迁移PHP应用层。在10.0.0.11上安装PHP-FPM和所有扩展,版本最好和原机器完全一致,否则代码里的函数兼容性可能出问题。这时PHP-FPM的监听方式必须从Unix Socket改成TCP端口,便于Nginx远程访问:

listen = 9000 listen.allowed_clients = 10.0.0.10

listen.allowed_clients 只允许10.0.0.10这个Nginx节点连过来,其他IP一律拒绝,这是跨机器部署之后最基本的访问控制。

代码同步我用rsync:

rsync -avz --delete /data/www/blog/ root@10.0.0.11:/data/www/blog/ rsync -avz --delete /data/www/cms/ root@10.0.0.11:/data/www/cms/

--delete 参数会把目标端多余的文件删掉,保证两端代码目录完全一致。如果上传目录里已经有大量图片,同步时会比较慢,后面还会遇到“两站点文件不一致”的问题,放在4.4专门讲。

代码同步完成后,最关键的一步是修改数据库连接配置。博客和内容管理系统的数据库连接文件位置不一样,本质都一样:数据库地址从 127.0.0.1 改成 10.0.0.12,用户名和密码改成新节点上创建的应用账号。改完先在10.0.0.11本地用php命令行测一下数据库连接,确认PHP能连上远端MySQL,再继续往下走。

4.3 Nginx网关节点改造:从本地PHP切换到远程PHP

三节点都准备好后,最后动Nginx。在10.0.0.10上,把两个站点的fastcgi_pass从本机的unix socket或127.0.0.1:9000,改成10.0.0.11:9000:

location ~ \.php$ { fastcgi_pass 10.0.0.11:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }

改完之后执行 nginx -t 检查配置语法,然后reload。这里有个容易被忽略的点:Nginx节点上的站点代码目录不能删。因为静态文件是由Nginx读本地磁盘处理的,如果本地没有代码文件,图片、CSS、JS直接404。所以分离之后,Nginx节点上保留一份完整的站点代码,PHP节点上也有一份完整代码,两边通过rsync保持同步,这就是“代码双写”的现实需求。

切完之后,验证顺序有个好习惯:先用内网或hosts方式把域名解析到10.0.0.10测试,确认页面打开、图片能加载、后台上传正常,再切换真实流量。全部正常后,最后才把10.0.0.10上原有的PHP-FPM和MySQL停掉,停之前再确认一遍,避免停早了导致应用直接挂掉。

4.4 站点文件共享问题:uploads目录的三种同步方案

分离之后最典型的坑是文件上传。博客和内容管理系统都有上传目录,用户在后台传图片,这些图片被PHP应用节点写入10.0.0.11的本地磁盘。但Nginx在10.0.0.10上处理静态请求,它去读自己的本地目录,发现文件不存在,图片就404了。这个问题不处理,分离上线第一天后台就会开始报错。

三种方案我分别评估过:

方案原理优点缺点
NFS共享存储Web节点和PHP节点挂载同一个远程目录实时一致,一写即读多一个依赖,NFS服务挂了全站图片挂
rsync定时同步PHP节点定期把uploads推到Web节点实现简单,稳定有延迟,刚传的图可能短暂404
静态请求全部转给PHP节点Nginx对图片等静态请求也反向代理到10.0.0.11文件读写都在同一节点,实时一致静态文件走网络,性能下降

我当时用了折中方案:uploads目录做rsync定时同步,间隔几分钟一次。代价是用户刚上传的图片可能有几分钟的“404空窗期”,但对博客和内容管理系统来说,这个延迟可以接受。后续如果图片量变大、强一致成为硬需求,再考虑引入对象存储或共享存储。这里我不想给一个放之四海皆准的答案,不同站点的图片实时性要求完全不同,搞清楚取舍逻辑才是最关键的。

4.5 切换、验证与一键回滚

分离部署最大的风险不是装不好,而是切换失败后回不去。我的经验是,所有配置改动之前,先做一份可回滚的备份:Nginx配置目录打包、PHP-FPM配置打包、原机器上的MySQL数据不删留着。然后按顺序验证:本地hosts先解除,跑通一个站点,再跑通第二个;确认无误后放开正式访问。

万一需要回滚,操作也简单:Nginx的fastcgi_pass改回127.0.0.1:9000,reload后立即恢复;PHP节点上的数据库连接串改回127.0.0.1,代码同步回去,原机器MySQL重新启动。整体回滚时间控制在十分钟以内。这个“回滚预案先行”的习惯,让整个切换过程压力小了很多——反正最坏情况就是退回单机,站点不会一直挂。

5. 分离之后不能省的运维功课:备份、监控、安全、恢复

5.1 备份体系:数据库与代码双保险

分离后的备份必须跟架构同步调整,不能再用单机时代的“整机快照”思路。数据库在独立节点上,备份可以做得更干净。我的crontab里有两个任务:一个数据库备份脚本,每天凌晨用mysqldump导出两个库并压缩,保留最近7天;一个代码同步脚本,把两个节点的代码和上传目录定期拉到一个备份目录或备份机器上。

#!/bin/bash # 数据库备份,保留7天 BACKUP_DIR=/backup/mysql DATE=$(date +%F) mysqldump --single-transaction -u backup -p'备份账号密码' blog_db | gzip > $BACKUP_DIR/blog_db_$DATE.sql.gz mysqldump --single-transaction -u backup -p'备份账号密码' cms_db | gzip > $BACKUP_DIR/cms_db_$DATE.sql.gz find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete

这个脚本看着简单,实际执行有两个细节:一是mysqldump不要用root账号,单独建一个只有SELECT、LOCK TABLES权限的备份账号,避免root密码暴露在脚本里;二是要把系统的cron邮件关掉或配置好,否则每天备份失败只能靠邮件标题判断,邮件一多就没人看了。

5.2 监控指标:三个节点分别盯什么

分离之后,最怕的是某个节点悄悄出问题,等到用户报障才发现。不是非要上多复杂的监控系统,最简单的方式也能覆盖大部分风险。我按节点整理了要盯的指标:

节点核心指标异常信号
Nginx节点连接数、请求延迟、磁盘空间磁盘满导致日志写不进去,服务异常
PHP节点CPU、PHP-FPM进程数、慢请求数进程数打满或慢日志暴涨,说明代码里有慢脚本
MySQL节点CPU、慢查询数、连接数、磁盘空间慢查询数量激增,优先排查索引;连接数超上限直接拒绝服务

我现在还会加一个最简单的存活监控:定时任务每两分钟curl一下两个站点首页,检测HTTP状态码,连续失败就记录并通知。这个比任何复杂的监控都更能回答“网站到底能不能打开”这个问题。

5.3 安全加固:端口、账号、权限的最小暴露面

分离之后网络面更大了,端口管控反而要更细。我的原则是:只开放必要端口,其余全部在内网。具体来说,Nginx节点对外只放行80/443;PHP节点只接受来自Nginx节点的9000端口连接,不对公网开放;MySQL节点只接受来自PHP节点网段的3306连接,不对公网开放。MySQL绑定要指定内网地址或者明确来源IP,不要图省事直接设成0.0.0.0。

应用账号也要重新梳理。单机阶段账号写的是 localhost,分离之后必须改成指定来源网段的授权,比如 'blog'@'10.0.0.%',否则PHP从远程连接会被拒绝。另外,所有节点的SSH都做加固:禁用root密码登录、使用密钥认证、修改SSH默认端口这些都是基础操作,不做的话等于把大门敞着。

5.4 整机故障恢复演练:从零拉起一个节点

架构分离之后,最值得做一次的是故障恢复演练。我自己挑了一个周末,模拟“数据库节点整机宕机”的情况,按恢复清单一步步来:准备一台新机器、安装MySQL、从备份文件导入数据、修改应用账号授权、重启PHP-FPM验证连接。整套流程走一遍之后,最大的收获不是“我会恢复了”,而是发现备份脚本里有个小bug——有个库的备份路径写错了,每天其实都在备份同一个库。这种问题不演练根本发现不了。

恢复演练的具体流程可以记成一张检查单,每个节点的恢复步骤固定下来:比如MySQL节点恢复,就是装MySQL、导备份、改授权、验证账号;PHP节点恢复,就是装PHP-FPM、同步代码、改数据库连接串、验证应用。每做过一次,后续遇到事故就只是按单执行,而不是半夜边查文档边操作。说实话,这种“纸上谈兵”的演练,比买一堆花哨的监控面板有用得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询