LNMP 架构实战:从单机搭建到数据库分离与动静分离 公司官网最早是一个”大杂烩”单机:Nginx、PHP-FPM、MySQL 全挤在一台 4G 内存的服务器上。某次推广活动流量上来,MySQL 被系统 OOM Killer 干掉,网站挂了半小时。那次之后我们做了一轮架构拆分——这篇记录从单机 LNMP 到多节点分离的全过程。
一、先理清 LNMP 怎么跑 LNMP = Linux + Nginx + MySQL + PHP,请求链路是这样的:
1 2 3 浏览器 → Nginx(静态文件直接返回) └→ .php 请求经 FastCGI 交给 PHP-FPM 执行 └→ PHP 连 MySQL 取数据 → 生成 HTML 返回
关键点是 Nginx 自己不执行 PHP ,它把 .php 请求通过 FastCGI 协议转给 PHP-FPM 进程池处理。所以 Nginx 和 PHP-FPM 的”运行用户”必须一致,否则 PHP 读不到 Nginx 放的文件,这也是新手最常见的权限问题来源。
二、单机搭建 1. Nginx 与统一用户 1 2 3 4 5 6 7 8 yum install nginx -y groupadd www -g 666 useradd www -u 666 -g 666 -s /sbin/nologin -M sed -i '/^user/c user www;' /etc/nginx/nginx.conf systemctl start nginx && systemctl enable nginx
2. PHP-FPM 1 2 3 4 5 6 7 8 yum -y install php71w php71w-cli php71w-common php71w-gd php71w-mbstring \ php71w-pdo php71w-xml php71w-fpm php71w-mysqlnd php71w-opcache --nogpgcheck sed -i '/^user/c user = www' /etc/php-fpm.d/www.conf sed -i '/^group/c group = www' /etc/php-fpm.d/www.conf systemctl start php-fpm && systemctl enable php-fpm
3. MySQL(MariaDB) 1 2 3 4 yum install mariadb-server -y systemctl start mariadb && systemctl enable mariadb mysqladmin password 'App@123' mysql -uroot -p'App@123'
4. Nginx 侧接入 FastCGI 1 2 3 4 5 6 7 8 9 10 11 12 13 14 server { listen 80 ; server_name php.example.com; root /code; index index.php index.html; location ~ \.php$ { root /code; fastcgi_pass 127.0.0.1:9000 ; fastcgi_param SCRIPT_FILENAME $document_root $fastcgi_script_name ; include fastcgi_params; } }
写两个测试文件验证:info.php(phpinfo)确认 PHP 通了;mysqli.php 确认 PHP 能连数据库:
1 2 3 4 5 6 7 8 9 10 11 <?php $servername = "localhost" ; $username = "root" ; $password = "App@123" ; $conn = mysqli_connect ($servername , $username , $password ); if (!$conn ) { die ("Connection failed: " . mysqli_connect_error ()); } echo "PHP 连接 MySQL 正常" ; ?>
两个页面都通了,LNMP 基础环境就算立住了。
这里有个常见的白页问题:phpinfo() 打开一片空白,八成是 fastcgi_pass 指向的 PHP-FPM 没起,或者 SCRIPT_FILENAME 拼错导致 PHP 找不到文件。先看 Nginx error.log,再确认 systemctl status php-fpm。
三、数据库分离:把 OOM 的根因拆掉 单机的隐患在于:PHP-FPM 和 MySQL 抢内存,MySQL 往往是那个被系统杀掉的(OOM Killer 优先杀内存占用大的进程)。把数据库拆到独立服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 mysqldump -uroot -p'App@123' -A > mysql-all.sql scp mysql-all.sql root@172.18.1.51:/tmp yum install mariadb mariadb-server -y systemctl start mariadb && systemctl enable mariadb mysql -uroot -p'App@123' < /tmp/mysql-all.sql mysql -uroot -p'App@123' mysql> grant all on *.* to appuser@'172.18.1.%' identified by 'App@123' ; mysql> flush privileges;
两头命名要保持一致:建库、建用户的操作细节,走之前跟开发对一遍——因为下一步就要改应用的数据库地址。
1 2 3 4 5 define ('DB_NAME' , 'wordpress' );define ('DB_USER' , 'appuser' );define ('DB_PASSWORD' , 'App@123' );define ('DB_HOST' , '172.18.1.51' );
改完访问网站验证。这里有个偷懒但有效的小技巧:用 grep -iR "旧地址或旧密码" /code --exclude-dir=cache 全局扫一遍代码目录,防止漏掉某个配置文件——我们当时就漏了一个 wiki 系统的 database.php。
四、Web 扩到多节点 单台 Web 是另一个单点,扩容的思路是”复制一份,数据源统一”:
1 2 3 4 5 6 7 8 9 10 11 scp -rp root@172.18.1.7:/etc/yum.repos.d/* /etc/yum.repos.d/ yum install nginx -y && yum -y install php71w ...(同上) scp -rp root@172.18.1.7:/etc/nginx /etc/ scp -rp root@172.18.1.7:/etc/php-fpm.d /etc/ tar czf code.tar.gz /code && scp code.tar.gz root@172.18.1.8:/tmp tar xf /tmp/code.tar.gz -C /
清单核对:Nginx 配置、PHP 配置、代码、运行用户、数据库地址——五项全对,节点才算”一样”。后面接负载均衡(前一篇讲过的 upstream),多节点就生效了。
五、上传目录挂 NFS:静态资源的一致性 多节点之后下一个问题立刻来:用户上传的图片只落在接收请求的那台机器上,轮询到另一台就 404。方案是加一台 NFS,把上传目录共享给所有 Web 节点:
1 2 3 4 5 6 7 8 9 10 11 12 yum install nfs-utils -y cat /etc/exports/data/blog 172.18.1.0/24(rw,sync ,all_squash,anonuid=666,anongid=666) mkdir -p /data/blog && chown -R www:www /data/systemctl restart nfs-server yum install nfs-utils -y mount -t nfs 172.18.1.31:/data/blog /code/wordpress/wp-content/uploads/ echo "172.18.1.31:/data/blog /code/wordpress/wp-content/uploads nfs defaults 0 0" >> /etc/fstabmount -a
挂载前先把原来本地的文件拷进共享目录(选资源最全的那台做源),否则用户会看到”老图片全裂了”这种诡异现象。
六、动静分离:三种落地方式 动静分离的意义:静态请求(图片、CSS、JS)由 Nginx 直接返回,动态请求(PHP/JSP/接口)才转后端。这样静态资源不吃后端资源,动态服务挂了静态页面也不受影响。
方式一:单机做(最简单的起步) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server { listen 80 ; server_name www.example.com; root /code/wordpress; location ~* \.(png|jpg|gif|css|js|mp4)$ { root /code/wordpress/images; expires 7d ; access_log off ; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000 ; fastcgi_param SCRIPT_FILENAME $document_root $fastcgi_script_name ; include fastcgi_params; } }
方式二:静态一台、动态一台 静态资源单独给一台机器(或 CDN 源站),Nginx 上按后缀分流:
1 2 3 4 5 6 7 8 9 10 11 server { listen 80 ; server_name pic.example.com; root /code; location ~* .*\.(jpg|png|gif)$ { root /code/images; } }
方式三:在负载均衡上统一调度 入口层按请求类型分到不同 upstream,这是多节点场景的最终形态:
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 upstream static { server 172.18.1.7:80 ; } upstream java { server 172.18.1.8:8080 ; } server { listen 80 ; server_name pic.example.com; location / { root /code; index index.html; } location ~* \.(jpg|png|gif)$ { proxy_pass http://static; proxy_set_header Host $http_host ; } location ~ \.jsp { proxy_pass http://java; proxy_set_header Host $http_host ; } }
附:按客户端类型分流 同一个域名按终端类型调度到不同资源,用 $http_user_agent 判断即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 upstream android { server 172.18.1.7:9090 ; }upstream iphone { server 172.18.1.7:9091 ; }upstream pc { server 172.18.1.7:9092 ; }server { listen 80 ; server_name m.example.com; charset utf-8 ; location / { if ($http_user_agent ~* "Android") { proxy_pass http://android; } if ($http_user_agent ~* "Iphone") { proxy_pass http://iphone; } if ($http_user_agent ~* "MSIE") { return 403 ; } proxy_pass http://pc; } }
注意:if 在 location 里有”陷阱”,多条件场景建议用 map 指令提前算好变量,逻辑更清晰。
七、这套架构的复盘 拆完之后的收益很直观:内存不再打架(数据库独占一台)、单台 Web 故障不影响整体、静态文件走共享存储和缓存后动态服务压力降了一大截。
教训也有三条:数据库迁移前把连接配置”全局 grep 一遍”、NFS 挂载要进 fstab 并测试重启后自动挂载、扩容新节点时先对照老节点做配置差异清单。这三点做到位,架构拆分就是一次平稳的升级,而不是一场事故。