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
# 安装 Nginx(官方源,见前文)
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
# 用第三方源装 PHP 7.1(CentOS 7 自带版本太旧)
yum -y install php71w php71w-cli php71w-common php71w-gd php71w-mbstring \
php71w-pdo php71w-xml php71w-fpm php71w-mysqlnd php71w-opcache --nogpgcheck

# 让 PHP-FPM 也用 www 用户(和 Nginx 保持一致)
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' # 给 root 设初始密码
mysql -uroot -p'App@123'

4. Nginx 侧接入 FastCGI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /etc/nginx/conf.d/php.example.com.conf
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
# 1) web 侧全库备份导出
mysqldump -uroot -p'App@123' -A > mysql-all.sql
scp mysql-all.sql root@172.18.1.51:/tmp

# 2) db01 上安装数据库并导入
yum install mariadb mariadb-server -y
systemctl start mariadb && systemctl enable mariadb
mysql -uroot -p'App@123' < /tmp/mysql-all.sql

# 3) 建业务账号(不用 root 连业务库),按网段授权
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
# web 侧修改应用配置(以 WordPress 为例)
define('DB_NAME', 'wordpress');
define('DB_USER', 'appuser');
define('DB_PASSWORD', 'App@123');
define('DB_HOST', '172.18.1.51'); # 从 localhost 改为 db01

改完访问网站验证。这里有个偷懒但有效的小技巧:用 grep -iR "旧地址或旧密码" /code --exclude-dir=cache 全局扫一遍代码目录,防止漏掉某个配置文件——我们当时就漏了一个 wiki 系统的 database.php。

四、Web 扩到多节点

单台 Web 是另一个单点,扩容的思路是”复制一份,数据源统一”:

1
2
3
4
5
6
7
8
9
10
11
# 新节点建用户、装 Nginx/PHP(可以先把 web01 的源配置拷过去)
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
# NFS 服务端
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

# 各 Web 节点挂载到应用的上传目录
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/fstab
mount -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
# 静态节点 web01
server {
listen 80;
server_name pic.example.com;
root /code;
location ~* .*\.(jpg|png|gif)$ {
root /code/images;
}
}

# 动态节点 web02 跑 Tomcat/PHP

方式三:在负载均衡上统一调度

入口层按请求类型分到不同 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; } # 老 IE 直接拒绝
proxy_pass http://pc;
}
}

注意:if 在 location 里有”陷阱”,多条件场景建议用 map 指令提前算好变量,逻辑更清晰。

七、这套架构的复盘

拆完之后的收益很直观:内存不再打架(数据库独占一台)、单台 Web 故障不影响整体、静态文件走共享存储和缓存后动态服务压力降了一大截。

教训也有三条:数据库迁移前把连接配置”全局 grep 一遍”、NFS 挂载要进 fstab 并测试重启后自动挂载、扩容新节点时先对照老节点做配置差异清单。这三点做到位,架构拆分就是一次平稳的升级,而不是一场事故。