LNMP架构实战:Nginx+MySQL+PHP-FPM动静分离搭建与故障排查
1. 环境规划与整体架构这一篇写的是 LNMP 的完整搭建和动静分离全称是 Linux Nginx MySQL PHP。很多人一上来就急着装包、改配置结果环境变量、目录权限、PHP 和 Nginx 的通信方式全乱套后面出了问题都不知道从哪排查。我这次把整个搭建过程重新捋了一遍从系统准备到动静分离验证每一步都记录下当时的思路和踩过的坑希望能给正在学习 Nginx 组网的朋友一些参考。这次实验的环境是三台 CentOS 7.9 虚拟机配置都是 2 核 4GNginx 单独一台MySQL 一台PHP 一台这样拆开是为了后面做负载均衡和动静分离时更方便看效果。实践中如果只是单纯学习全部装在一台机器上也完全没问题但建议至少准备两台一台跑 Nginx PHP一台跑 MySQL这样能顺便体会一下 PHP-FPM 远程连接 MySQL 的执行流程。组件版本我选了 Nginx 1.24.0、MySQL 5.7.44、PHP 7.4.33这套组合属于保守稳定型网上资料多遇到问题搜得到答案。如果你有生产环境的经验用 PHP 8.x 也没问题但 7.4 对初学者更友好扩展编译的坑更少。整体请求链路是浏览器请求到达 NginxNginx 根据 location 规则判断是静态资源还是动态请求。静态文件图片、CSS、JS直接由 Nginx 本地返回动态请求则通过 FastCGI 协议转发给 PHP-FPM 处理如果需要数据再连 MySQL 查询结果返回给 PHPPHP 把 HTML 交回 Nginx最后由 Nginx 响应给浏览器。这套架构核心有三层理解请求入口只有一个 Nginx统一处理静态资源按规则分发动态请求PHP 和 Nginx 之间走 FastCGI 协议不是简单的 HTTP 转发PHP 和 MySQL 之间走 MySQL 原生协议PHP 是数据库的唯一访问者理解这一条链路后Nginx 配 FastCGI、PHP-FPM 的 listen 方式、MySQL 的授权设置就都串起来了。2. Nginx 编译安装与基础配置2.1 依赖包与编译参数Nginx 官方仓库提供预编译的发行版包但为了学习编译过程、方便后续加第三方模块这里选择源码编译。编译前需要准备 gcc、make以及三个关键依赖库pcre正则支持location 的 ~ 匹配依赖它、zlibgzip 压缩、opensslHTTPS 和后面的安全功能。依赖安装yum install -y gcc gcc-c make pcre-devel zlib-devel openssl-devel下载 Nginx 源码并解压cd /usr/local/src wget https://nginx.org/download/nginx-1.24.0.tar.gz tar -zxvf nginx-1.24.0.tar.gz cd nginx-1.24.0编译配置项./configure \ --prefix/usr/local/nginx \ --userwww --groupwww \ --with-http_stub_status_module \ --with-http_ssl_module \ --with-http_gzip_static_module \ --with-pcre make -j 4 make install上述配置的意图是--prefix指定安装目录--user指定 Nginx worker 进程运行的系统用户先创建 www 用户后三个模块分别是状态监控、SSL 和静态 gzip 支持。生产环境还会有更多参数比如--with-stream用于 TCP 四层代理这里暂不展开。注意如果没有创建 www 用户就执行 configure后面的 make install 会报错。可以先useradd -M -s /sbin/nologin www创建好再编译。安装完成后目录结构是/usr/local/nginx其中conf/nginx.conf是核心配置文件html是默认站点目录logs放访问日志和错误日志。Nginx 的配置改动后要nginx -t测试再nginx -s reload这个习惯要养好。2.2 主配置文件的组织方式Nginx 主配置文件内容虽然多但核心就三个模块events连接处理、httpHTTP 协议处理、server虚拟主机。比较推荐的实践是不要把多个站点全写进一个 nginx.conf而是用include把每个站点拆成独立文件。在nginx.conf的http {}块内加入include /usr/local/nginx/conf/vhosts/*.conf;然后创建vhosts目录每个站点一个域名.conf文件这样后面部署多个 Web 项目时只需要新增一个文件主配置不用动。不过这次主题是 LNMP所以一个站点就够了。最小可用的 LNMP server 块长这样server { listen 80; server_name www.example.com; root /data/wwwroot/example; index index.php index.html; location / { try_files $uri $uri/ 404; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }这个配置的fastcgi_param SCRIPT_FILENAME特别关键它告诉 PHP-FPM 要执行哪个文件。新手经常漏掉这行结果访问 .php 文件时 FPM 返回 “File not found”后面排查章节会具体讲。3. MySQL 5.7 安装与初始化3.1 二进制包安装MySQL 源码编译耗时比较长生产用到 5.7 版本的话用官方二进制包最合适。下载解压后做软链接把属主改成 mysqluseradd -M -s /sbin/nologin mysql cd /usr/local tar -zxvf /usr/local/src/mysql-5.7.44-el7-x86_64.tar.gz ln -s mysql-5.7.44-el7-x86_64 mysql chown -R mysql:mysql /usr/local/mysql初始化数据目录/usr/local/mysql/bin/mysqld \ --initialize-insecure \ --usermysql \ --basedir/usr/local/mysql \ --datadir/usr/local/mysql/data--initialize-insecure表示 root 初始密码为空适合本次快速测试生产上建议用--initialize生成随机初始密码日志里会打印出来。接着写配置文件/etc/my.cnf[mysqld] basedir/usr/local/mysql datadir/usr/local/mysql/data socket/tmp/mysql.sock port3306 character-set-serverutf8mb4 pid-file/usr/local/mysql/data/mysqld.pid log-error/var/log/mysql-error.log启动方式用mysqld_safe或 systemd 都行最简单直接执行/usr/local/mysql/bin/mysqld_safe --usermysql 3.2 创建业务库与授权进入 MySQL 命令行/usr/local/mysql/bin/mysql -uroot -p业务库mydb和专用账号phpuserCREATE DATABASE mydb DEFAULT CHARACTER SET utf8mb4; CREATE USER phpuser192.168.1.% IDENTIFIED BY Php2024; GRANT ALL PRIVILEGES ON mydb.* TO phpuser192.168.1.%; FLUSH PRIVILEGES;这里授权范围是192.168.1.%表示允许整个网段连接。如果 PHP 和 MySQL 不在同一台机器MySQL 默认只监听 127.0.0.1还需要改配置[mysqld] bind-address0.0.0.0改完重启 MySQL 生效。安全考虑生产环境不推荐用%全通配按实际 PHP 服务器 IP 精确授权更稳。经验PHP 连接 MySQL 报 “Access denied for user” 时90% 的原因是授权网段不对或者 MySQL 的 bind-address 只监听了本机。先netstat -tlnp | grep 3306确认监听状态再看授权表问题基本就定位了。导入测试数据USE mydb; CREATE TABLE article ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); INSERT INTO article (title, content) VALUES (LNMP 动静分离测试, 这是来自数据库的动态内容);这张表后面动静分离验证时用一个页面里静态图片由 Nginx 直接返回文章正文从数据库动态查询一眼就能看出两者的响应差异。4. PHP-FPM 编译安装与对接4.1 PHP 编译要点PHP 源码编译比 Nginx 复杂因为扩展多。很多初学者碰到的情况是 PHP 装好了但连 MySQL 报 “Class mysqli not found”所以这次要在编译阶段就把--enable-mysqlnd和with-mysqli、with-pdo-mysql一起加进去。下载解压cd /usr/local/src wget https://www.php.net/distributions/php-7.4.33.tar.gz tar -zxvf php-7.4.33.tar.gz cd php-7.4.33编译前先安装扩展依赖yum install -y libxml2-devel libcurl-devel libpng-devel libjpeg-turbo-devel freetype-devel核心配置./configure \ --prefix/usr/local/php \ --with-config-file-path/usr/local/php/etc \ --enable-fpm \ --enable-mbstring \ --enable-zip \ --enable-mysqlnd \ --with-mysqlimysqlnd \ --with-pdo-mysqlmysqlnd \ --with-gd \ --with-curl \ --with-openssl \ --with-freetype \ --with-jpeg--with-config-file-path指定 php.ini 的位置--enable-fpm是生成 PHP-FPM 管理程序后面所有动态请求都靠它处理。编译安装make -j 4 make install安装后把默认配置文件放好cp php.ini-development /usr/local/php/etc/php.ini cp /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf cp /usr/local/php/etc/php-fpm.d/www.conf.default /usr/local/php/etc/php-fpm.d/www.conf4.2 配置 PHP-FPM 与启动PHP-FPM 的www.conf里有几个关键配置首先是运行用户user www group www然后是监听方式。有两种选择一是监听 TCP 端口 9000即listen 127.0.0.1:9000二是监听 Unix Socket即listen /run/php-fpm/www.sock。本地单机场景Unix Socket 性能更好因为省去了 TCP 协议栈的开销但在 Nginx 和 PHP 分机的架构下只能走 TCP。PHP 5.6、7.4、8.x 在语法上有差异但 PHP-FPM 的这套配置逻辑基本一致我这次实验用 TCPNginx 里就写fastcgi_pass 127.0.0.1:9000。配置最底部可以看一下pm参数pm dynamic pm.max_children 20 pm.start_servers 5 pm.min_spare_servers 3 pm.max_spare_servers 10dynamic表示按需动态调整 worker 进程数适合大多数 Web 项目。max_children是同时能处理的最大请求数配太低了高峰期 PHP 请求排队配太高了内存吃紧。经验值可以参考每个 PHP-FPM 进程约占用 30-50MB 内存2G 内存的服务器max_children设置在 20 到 30 之间比较安全。启动命令/usr/local/php/sbin/php-fpm验证监听是否正常netstat -tlnp | grep 9000看到0.0.0.0:9000或127.0.0.1:9000就代表 PHP-FPM 已经起来了。其实 PHP-FPM 也可以注册成 systemd 服务用systemctl start php-fpm管理方法是写一个/usr/lib/systemd/system/php-fpm.service文件。学习阶段手动启动没问题但建议学会 systemd 托管服务器重启后服务能自动拉起比手动启动靠谱得多。5. 动静分离的实现原理与配置5.1 动静分离的核心思路动静分离的意思是静态资源和动态请求走不同的处理路径。静态资源如.jpg、.css、.js、.png等交给 Nginx 自身处理不走 PHP-FPM动态接口如.php结尾的 URL 才交给 PHP-FPM 处理。这样做有直接的好处Nginx 处理静态文件的能力很强并发高时占用的内存和 CPU 很少而 PHP-FPM 每个进程内存占用偏高让 PHP 去处理几十个静态图片请求是大材小用资源消耗完全不成比例。动静分离后静态请求不占用 PHP 进程实际压测时 QPS 往往能差出几倍。另外从架构上想如果以后静态资源量大了可以把图片、CSS、JS 目录直接迁移到 CDN 或者独立的文件服务器上Nginx 里只需改一下 root 指向或加一层 proxy_pass动静分离的架构天然支持这种横向扩展。Nginx 实现动静分离主要靠location块的匹配规则# 处理动态请求匹配以 .php 结尾的 URI location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 处理静态资源常见静态文件扩展名匹配 location ~* \.(jpg|jpeg|gif|png|css|js|ico|svg|woff2?)$ { expires 7d; access_log off; add_header Cache-Control public; }~表示区分大小写的正则匹配~*表示不区分大小写的正则匹配。expires 7d告诉浏览器这些静态资源可以缓存 7 天再次访问时浏览器直接使用本地缓存连请求都不发出这对性能提升非常明显。5.2 location 匹配优先级这是面试常问、实战也经常踩坑的点。Nginx 的 location 匹配规则优先级可以概括为一句话先普通前缀匹配再正则匹配正则按书写顺序匹配到第一个就停如果正则都没匹配上则使用前面普通匹配中最长的那个。定位优先级 1. 精确匹配 2. ^~ 普通前缀匹配匹配上后就不继续查正则 3. ~ 或 ~* 正则匹配按顺序匹配命中即停 4. 普通前缀匹配取最长匹配 5. 如果没有匹配返回 404所以刚才那个配置里当请求/article/12时不会命中 .php 正则所以走普通前缀匹配Nginx 会尝试在 root 目录找/article/12对应文件找不到就try_files转 404。当请求/api/index.php时正则\.php$命中直接把请求转发到 PHP-FPM。这里最需要注意的是SCRIPT_FILENAME一定要基于真实的站点根目录拼。如果用$document_root$fastcgi_script_name那 Nginx 配置块里必须正确设置了root否则 PHP 会返回 “Primary script unknown”。5.3 动静分离实例部署与验证我这次在站点根目录/data/wwwroot/example下放了两个文件一个静态图片目录static/logo.png一个动态 PHP 文件index.php?php $mysqli new mysqli(192.168.1.20, phpuser, Php2024, mydb); if ($mysqli-connect_error) { die(数据库连接失败: . $mysqli-connect_error); } $mysqli-set_charset(utf8mb4); $result $mysqli-query(SELECT title, content FROM article ORDER BY id DESC LIMIT 1); $row $result-fetch_assoc(); ? !DOCTYPE html html head meta charsetutf-8 titleLNMP 动静分离测试/title link relstylesheet href/static/style.css /head body h1?php echo htmlspecialchars($row[title]); ?/h1 p?php echo htmlspecialchars($row[content]); ?/p img src/static/logo.png /body /html重启 Nginx/usr/local/nginx/sbin/nginx -t /usr/local/nginx/sbin/nginx -s reload然后用 curl 分别测试curl -I http://192.168.1.10/static/logo.png curl -I http://192.168.1.10/index.php第一个请求返回的响应头里应该能看到Expires或Cache-Control头表示静态资源由 Nginx 直接返回第二个返回的就是 PHP 渲染后的 HTML里应包括数据库 title 字段的内容。再看 Nginx 的 access.logtail -f /usr/local/nginx/logs/access.log请求.png时响应码是 200耗时通常为个位数毫秒请求.php时耗时通常有几毫秒到几十毫秒的差距这能直观感受到动静分离后的不同处理路径。踩坑提示PHP 页面里访问数据库时如果报 “Connection refused” 或者超时先确认 PHP 到 MySQL 的网络通不通。PHP 和 MySQL 分机部署时云安全组或本机 firewalld 的 3306 端口必须放行否则 PHP 连不上 MySQL。检查方式在 PHP 机器上执行telnet MySQL_IP 3306通则有响应不通会直接提示连接失败。5.4 静态资源缓存策略动静分离除了处理路径分离还要配合浏览器缓存策略才能达到最优效果。Nginx 里常用的缓存配置location ~* \.(jpg|jpeg|gif|png|css|js|ico|svg|woff2?)$ { expires 7d; add_header Cache-Control public, max-age604800; access_log off; }expires 7d是 Nginx 内置指令会在响应头里自动生成Expires和Cache-Control。浏览器识别到这两个头后在 7 天内访问同一个静态文件会直接用本地缓存不再发请求给服务器。这里有个细节对 HTML 页面一定不要开长缓存否则内容更新后用户看到的是旧页面。一般习惯是 HTML 不设置 Cache-Control 或设置no-cache静态资源CSS、JS 文件改名加版本号设置长时间缓存。如果静态资源经常更新可以考虑把 expires 时间设置短一点或者在上线时对 CSS、JS 文件名加上版本参数如style.css?v20240101这样文件名变了浏览器会重新请求缓存策略和版本更新不冲突。6. 常见问题排查与运维技巧6.1 经典报错502 Bad Gateway这个报错是 Nginx 收到请求后按规则转发给 FastCGIPHP-FPM但 PHP-FPM 没有正常响应。常见原因有三个第一PHP-FPM 没启动。检查netstat -tlnp | grep 9000没输出就启动 php-fpm。第二PHP-FPM 启动但监听地址不对。比如 PHP-FPM 监听的是/run/php-fpm/www.sock但 Nginx 里fastcgi_pass写的是127.0.0.1:9000两边对不上请求直接丢失。解决方法是保证两边一致如果都用本机局域网通信写成fastcgi_pass 127.0.0.1:9000就完全够用。第三PHP-FPM 的 worker 进程全部被占满。查看pm.max_children是不是配小了php-fpm日志里会报 “server reached pm.max_children”。临时调大max_children或者降低 PHP 脚本执行时间可以让问题缓解根本解法还是优化代码或加机器。6.2 经典报错404 Not Found访问 PHP 文件时 Nginx 返回 404而不是 502这通常是路径问题。Nginx 是按root URI的规则去找文件的。比如 root 是/data/wwwroot/example请求/index.phpNginx 需要在/data/wwwroot/example/index.php找到实际文件。如果文件确实存在但还 404检查是不是location块的root写错了特别是location ~ \.php$内部是否重新定义了 root。多个嵌套 location 时 root 混乱就会出现文件明明存在但找不到的诡异现象。6.3 经典报错Access denied. / File not foundPHP-FPM 返回 “File not found” 的排查思路跟 Nginx 404 类似但这个错误表示请求确实进到了 PHP-FPM但 FPM 找不到对应的 PHP 文件。最常见的原因是SCRIPT_FILENAME拼错了。$document_root取的是 Nginx 当前 location 的 root 值$fastcgi_script_name是请求的 URI。如果 Nginx 配置里没有设置 root$document_root会是空的那SCRIPT_FILENAME就成了/index.phpFPM 自然找不到。解决办法是在 PHP location 里加root /data/wwwroot/example;或者更规范地通过fastcgi_param SCRIPT_FILENAME /data/wwwroot/example$fastcgi_script_name;直写完整路径。还有一点容易被忽视Nginx 进程和 PHP-FPM 进程运行的系统用户要有权限读取站点目录文件。如果站点文件属主是 root但 Nginx worker 是 www 用户且目录权限是 700那么 Nginx 无法读取文件内容同样会导致访问异常。目录权限建议755文件权限建议644。6.4 常见问题速查表现象可能原因检查命令处理建议502 Bad GatewayPHP-FPM 未启动netstat -tlnp | grep 9000启动 php-fpm 或检查 systemd 状态502 Bad GatewayNginx 和 FPM 监听不匹配grep listen /usr/local/php/etc/php-fpm.d/www.conf统一为 TCP 9000 或 Unix Socket502 Bad GatewayPHP-FPM 进程数占满tail -f /usr/local/php/var/log/php-fpm.log调大 pm.max_children404 Not Foundroot 路径配置错误nginx -T | grep root检查 Nginx 实际生效的 rootFile not foundSCRIPT_FILENAME 拼接错误curl -I http://IP/index.php检查 fastcgi_param SCRIPT_FILENAMEAccess deniedPHP 连 MySQL 授权不足mysql -uphpuser -p -h IP重新授权或检查 bind-address静态资源不缓存缺少 expires 指令curl -I /static/logo.png在静态 location 中加入 expires 配置6.5 常用研究命令收集临时定位问题时会用到几个命令记一下方便# 测试 Nginx 配置 /usr/local/nginx/sbin/nginx -t # 查看 Nginx 运行时生效的完整配置 /usr/local/nginx/sbin/nginx -T # 实时查看访问日志 tail -f /usr/local/nginx/logs/access.log # 实时查看错误日志 tail -f /usr/local/nginx/logs/error.log # 查看 PHP-FPM 日志路径以实际配置为准 tail -f /usr/local/php/var/log/php-fpm.log # 查看 PHP-FPM 主进程状态 ps -ef | grep php-fpm # 检查端口监听 netstat -tlnp | grep -E 80|9000|3306 # 检查 MySQL 错误日志 tail -f /var/log/mysql-error.log7. 反向代理与负载均衡的扩展思路LNMP 搭建完成后最常见的进阶方向是 Nginx 反向代理和负载均衡。这个方向可以加深对 Nginx 定位的理解它不只是 Web 服务器还是一个高性能的流量入口网关。反向代理的配置核心是proxy_passserver { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }这里proxy_set_header的 X-Real-IP 和 X-Forwarded-For 用于把客户端的真实 IP 传给后端应用。不加这些头的话后端程序获取到的 IP 全是 Nginx 的 IP这对日志分析和限流非常不利。如果是多台后端应用用upstream做负载均衡upstream backend { server 192.168.1.11:8080 weight3; server 192.168.1.12:8080 weight1; keepalive 32; } server { listen 80; location / { proxy_pass http://backend; } }weight是权重默认是 1。keepalive表示 Nginx 与后端保持长连接数减少频繁建连的开销。这里后端接 Java 的 Spring Boot 或者 Go 服务都行如果是 PHP 项目一般还是走 FastCGI不走 HTTP 反代因为 PHP-FPM 本身就是 FastCGI 服务没必要再包一层 HTTP 协议。把这套反向代理思想融入 LNMP 之后回看整个架构就清楚了最外层是 Nginx承担静态资源服务、请求分发、反向代理、负载均衡中间是 PHP-FPM处理动态业务逻辑最底层是 MySQL存数据。这个架构模型今天看来可能不算最前沿但理解了它后面学习 Docker、Kubernetes 里的网关层、Ingress 概念时会有一种似曾相识的感觉核心思想是一脉相承的。我在这次搭建和排查过程中的体会是Nginx 学习最忌讳直接照抄配置死记硬背没有意义。遇到问题要有排查思路请求到没到 Nginx看 access.logNginx 转发给谁了看 error.logPHP 有没有执行看 php-fpm.logPHP 连数据库通不通看 MySQL 日志端口。一条链路一个环节一个环节地去验证很快就能定位问题。这套方法论比记住几十条配置指令更有价值。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →