ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LNMP动静分离实战:从零搭建Nginx+PHP-FPM+MySQL架构

LNMP动静分离实战:从零搭建Nginx+PHP-FPM+MySQL架构 1. 为什么LNMP动静分离值得认真过一遍我最早接触Nginx的时候其实是从反代和静态服务器入的门当时觉得Nginx不过是个配置稍微灵活点的Web Server直到后来被拉到一台只有1核1G的机器上搭一套带PHP后端的站点才真正意识到会用Nginx和用明白Nginx之间差着多远。那台机器上跑的是ApachePHP请求一多CPU直接拉满每次排查都是先重启Apache再找原因后来实在扛不住了才下决心把整套架构迁到LNMP上迁移之后同一台机器的QPS翻了好几倍CPU占用反而降了一大截。这个系列前两篇分别写了Nginx的基础安装和反向代理配置这篇就把整套LNMP环境从零到一完整过一遍重点放在动静分离。动静分离这个概念听起来很顺口——动就是动态请求静就是静态文件Nginx分别用不同策略处理——但真正落地的时候里面的坑远比预想的多。比如location的匹配顺序、fastcgi_params和fastcgi.conf的差异、php-fpm的socket配置方式、静态资源缓存策略这些细节任何一环没搞对线上就会出稀奇古怪的问题。这篇博文不是那种照抄命令就能跑通的速成帖而是把我自己从选型、装包、配置、压测到排错的全过程拆开来讲每个关键选择都解释清楚为什么。适合三类人看第一类是刚把Nginx基础玩熟、准备上手LNMP的初学者第二类是在公司搭过环境但没系统梳理过原理、遇到问题只能靠百度的运维向开发第三类是准备面试前做一次系统复盘、想搞明白动静分离底层逻辑的同学。如果你属于其中任何一种这篇应该能帮你少走不少弯路。2. 架构设计先于安装动静分离的模型和组件边界2.1 动静分离到底拆的是什么在动手装任何东西之前先把动静分离的模型理清楚。很多人以为动静分离就是把.css、.js、.jpg这类静态文件交给Nginx处理把.php、.jsp这类动态请求转发给后端这个理解没错但太粗糙了。真正到生产环境里动静分离至少要考虑三层第一层是请求路径的分离。一个URL进来Nginx要根据后缀、路径前缀或正则规则决定走静态分支还是动态分支。这一层的关键是规则命中顺序Nginx的location匹配规则是精确匹配优先于正则优先于前缀匹配很多人就是栽在这上面写了半天规则发现永远命中不了。第二层是处理能力的分离。静态文件不走PHP-FPM直接由Nginx读磁盘返回动态请求才交给FastCGI。这一层解决的是不要让PHP进程浪费在输出图片字节流这种低价值工作。静态请求和动态请求的资源模型完全不一样——静态文件是IO密集型动态请求是CPU密集型混在一起互相拖累。第三层是缓存策略的分离。静态资源可以设置很长的浏览器缓存和代理缓存有效期动态页面通常不能这么干或者只能用短缓存两套策略通过不同的响应头来实现。这三层全部做到位才算真正完整、可以上线到生产环境的动静分离。很多教程只讲到第一层的location规则就结束了导致照做之后静态请求确实没走PHP了但缓存策略没配置每次刷新都回源Nginx那点性能优势全浪费在网络往返上。2.2 LNMP各组件的分工边界LNMP这个缩写本身也是容易让人误解的地方。它不是一个软件而是四个组件的组合Linux是操作系统Nginx是Web服务器兼反向代理MySQL是关系型数据库PHP-FPM是PHP动态语言的进程管理器。四个组件有一条清晰的请求链路浏览器 → Nginx → 静态请求直接返回文件 → 动态请求通过FastCGI协议发给PHP-FPMPHP脚本内通过MySQL扩展访问数据库这条链路里每个环节承担不同的职责。Nginx虽然叫Web Server但在LNMP组合里更准确的定位是前端控制器——所有外部流量先经过它由它决定请求的走向。PHP-FPM是PHP脚本的执行引擎它监听一个端口或Unix Socket等待Nginx把动态请求甩过来。MySQL只跟PHP-FPM通信不直接对Nginx或浏览器开放。理解这个边界有个实际的用途排错的时候你可以迅速判断问题出在哪一段。如果浏览器能拿到静态页面但动态请求报502问题一定出在Nginx到PHP-FPM这一段如果PHP脚本里访问数据库超时问题出在PHP到MySQL这一段。每段的日志各自落在不同的文件中排查范围明确比在整套环境里乱撞要高效得多。2.3 版本选型生产和自学的取舍LNMP最麻烦的地方在于每个组件都有独立的版本线而它们之间又有版本兼容性问题。我见过太多新手直接把所有软件装到最新版然后跑起来各种函数报错查了半天是PHP版本太新、某个扩展还不兼容。选型逻辑应该是这样的组件生产环境推荐自学推荐选择理由LinuxCentOS 7/8或Debian 11/12本机虚拟机或云服务器均可尽量选和公司环境一致的Nginx1.24.x或1.26.x主线版与生产保持一致即可主线版功能全稳定版修复周期更久MySQL8.0.x或Percona分支8.0.x5.7已停维护8.0是长期支持版PHP7.4或8.1/8.28.27.4兼容性极好但已EOL8.x性能明显提升这里有个很容易踩的坑PHP 8.0开始很多老项目的代码会因为某些函数废弃而报错比如each()、create_function()在PHP 8里直接移除了。如果只是学习环境直接用8.2没问题但在生产环境升级前一定要先跑一遍兼容性测试。MySQL同样如此8.0默认的认证插件是caching_sha2_password老版本的PHP mysqli扩展如果不更新驱动会报Authentication plugin caching_sha2_password cannot be loaded错误。我的做法是学习环境固定在Nginx 1.26 MySQL 8.0 PHP 8.2这套组合生产环境根据项目实际情况决定。不要在版本上追求最新稳定的意义大于新功能的意义。3. 从空系统到三件套就位安装过程中的关键决策3.1 安装方案对比包管理器、编译安装、DockerLNMP建好了每种安装方式都有自己的适用场景不要一上来就问哪个方式最好而要问我现在的场景允许我用哪种方式。包管理器安装yum/apt最快最省事软件版本随发行版仓库走通常不是最新但稳定可靠。适合本地开发、临时测试、没有特殊模块需求的环境。编译安装可以从源码定制版本按需启用或禁用某些模块比如你需要在Nginx里加一个额外的第三方模块包管理器装的就很难搞。缺点是时间长、依赖复杂、升级维护全靠自己适合对版本和模块有强诉求的生产环境。Docker部署最快最一致一条docker-compose up把全套环境拉起来彻底免去环境污染的烦恼试用新技术、做PT可能生成大量临时数据、多人协作时优势明显。缺点是容器与宿主机之间的网络模式、数据卷挂载、日志收集这些概念对新手不友好出了问题排查链路更长。我这次选编译安装Nginx 二进制包安装MySQL PHP-FPM源码编译主要原因是想把这个过程完整过一次把每一层的配置细节都暴露出来这对理解LNMP是有帮助的。如果是急着上手跑一个项目我会直接拉php:8.2-fpm和mysql:8.0两个镜像再写个简单的Nginx配置十分钟就能跑起来。学习阶段和效率阶段用不同方案没必要一条路走到黑。3.2 Nginx编译安装与必备模块我用的系统是CentOS 7.9先更新系统并安装依赖yum update -y yum install -y gcc gcc-c make automake autoconf libtool pcre pcre-devel zlib zlib-devel openssl openssl-devel这里的几个依赖缺一不可pcre是Nginx正则匹配的底层库没有它location的~正则规则全部失效zlib提供gzip压缩支持openssl是HTTPS证书处理的依赖就算暂时不配SSL也建议装全否则后面补HTTPS又得重新编译。正式编译参数如下我一般搭配prefix指定目录不用系统默认路径./configure \ --prefix/usr/local/nginx \ --sbin-path/usr/local/sbin/nginx \ --conf-path/etc/nginx/nginx.conf \ --pid-path/var/run/nginx.pid \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-pcre \ --with-file-aio \ --with-http_gzip_static_module make -j$(nproc) make install解释几个容易被忽视的参数--with-http_realip_module在Nginx放到负载均衡后面时非常有用否则拿到的客户端IP全是负载均衡器的内网IP。--with-http_v2_module开启HTTP/2支持静态资源多的站点开启后并发性能有明显提升。--with-http_gzip_static_module可以提前用gzip命令压缩静态文件Nginx直接返回.gz文件而不是实时压缩能省不少CPU。编译完做个软链把nginx命令接入PATH并测试版本和配置ln -s /usr/local/nginx/sbin/nginx /usr/sbin/nginx nginx -t nginx -Vnginx -t输出configuration file /etc/nginx/nginx.conf test is successful才说明配置语法没问题。这一步很多新手会跳过直接改配置然后service nginx start报错了也不知道自己哪儿写错了。3.3 MySQL 8.0部署二进制包的坑和初始化安全设置MySQL我用了官方二进制包这种方式比源码编译快很多又比yum源很多发行版仓库里只有老版本灵活。# 创建mysql用户和目录 groupadd mysql useradd -r -g mysql -s /bin/false mysql mkdir -p /data/mysql chown -R mysql:mysql /data/mysql # 解压二进制包并初始化 tar xf mysql-8.0.32-linux-glibc2.12-x86_64.tar.xz -C /usr/local/ ln -s /usr/local/mysql-8.0.32-linux-glibc2.12-x86_64 /usr/local/mysql /usr/local/mysql/bin/mysqld --initialize-insecure --usermysql --basedir/usr/local/mysql --datadir/data/mysql这里注意一个细节初始化时用了--initialize-insecure而不是--initialize。两者的区别是后者会生成一个随机root密码并打印在日志里前者生成一个root空密码。在生产环境我建议用前者生成的随机密码然后马上修改在个人学习环境想省事就用--initialize-insecure后面一条SQL就能设置密码。初始化完成后配置systemd服务文件[Unit] DescriptionMySQL Server Afternetwork.target [Service] Typeforking Usermysql Groupmysql ExecStart/usr/local/mysql/bin/mysqld --defaults-file/etc/my.cnf ExecReload/bin/kill -s HUP $MAINPID PrivateTmpfalse [Install] WantedBymulti-user.targetMySQL 8.0之后默认的字符集是utf8mb4、默认认证插件是caching_sha2_password这些是默认值不用改但要注意客户端连接时如果用的是老版本驱动需要ALTER USER userhost IDENTIFIED WITH mysql_native_password BY password;临时兼容。这不算安全退化只是给老客户端的过渡方案。3.4 PHP-FPM源码编译与socket模式的性能优势PHP这块我推荐源码编译。原因是PHP的扩展体系跟Nginx模块类似需要什么装什么比如后面连MySQL需要pdo_mysql扩展装WordPress还需要gd、curl、exif等。用包管理器装PHP虽然快但扩展往往要单独装还经常因为版本不匹配报错不如编译时装好一劳永逸。yum install -y libxml2-devel sqlite-devel libcurl-devel oniguruma-devel libpng-devel libjpeg-devel freetype-devel ./configure \ --prefix/usr/local/php \ --with-config-file-path/usr/local/php/etc \ --with-mysqli \ --with-pdo-mysql \ --with-curl \ --with-openssl \ --with-zlib \ --enable-fpm \ --enable-mbstring \ --enable-gd \ --with-jpeg \ --with-freetype make -j$(nproc) make installPHP-FPM的监听方式有两种TCP端口默认9000和Unix Socket。端口模式配置简单、跨机器通信方便适合PHP和Nginx不在同一台机器上的场景Socket模式走的是文件系统省掉TCP协议栈的开销同机部署时性能更高也是LNMP常见的性能优化手段之一。我生产环境一般用Unix Socket例如listen /var/run/php-fpm.sock listen.owner nginx listen.group nginx listen.mode 0660listen.mode和owner、group这三个配置都要设对否则Nginx的worker进程通常以nginx用户运行没有权限写这个socket文件会出现Permission denied的502错误。这是NginxPHP-FPM的经典报错初学时被这个问题卡了整整一个下午后来才知道只需要把socket的属主改成nginx用户、权限设置为660就好了。PHP-FPM进程池的配置在php-fpm.d/www.conf里核心参数是pm、pm.max_children、pm.start_servers、pm.min_spare_servers、pm.max_spare_servers。生产环境建议用pm dynamic根据机器内存来推算进程数。假设每个PHP进程占用约50MB内存机器有2GB可用内存那么max_children保守设置为20左右是比较稳的。设太大会把内存吃光触发OOM Killer设太小会在高峰期大量请求排队性能急剧下降。3.5 三件套联通性自检装完之后先做一个最简单的自检测试确认三个组件都启动正常、端口监听正常、日志无致命报错systemctl start nginx systemctl start mysqld systemctl start php-fpm ss -tlnp | grep -E 80|3306|9000 ps aux | grep -E nginx|mysqld|php-fpmNginx默认监听80端口MySQL默认3306PHP-FPM如果配的是TCP就监听9000配了socket则没有端口。用ss -tlnp一眼就知道三个服务有没有起来。注意MySQL监听地址默认是127.0.0.1还是0.0.0.0的区别如果配置成对外监听会暴露数据库服务一定要配合防火墙规则和账号白名单如果只跑本机应用绑定127.0.0.1就够用了。三个服务都起来了再在Nginx的默认站点目录里扔一个phpinfo()探针文件验证Nginx能不能通过FastCGI跟PHP正常联动。如果这个最基本的联通都过不了后面动静分离的配置做得再花哨也白搭。4. Nginx配置文件拆解LNMP的核心逻辑都藏在细节里4.1 nginx.conf主配置参数LNMP环境搭好之后Nginx的配置文件是核心中的核心。默认的nginx.conf内容冗余且不适合LNMP建议按我下面的结构来写逻辑清楚还容易维护user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 2048; use epoll; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss image/svgxml; include /etc/nginx/conf.d/*.conf; }几个参数值得展开讲worker_processes auto是让Nginx根据CPU核心数自动生成对等数量的worker进程。每N个worker进程对应一个CPU核心避免进程频繁切换上下文消耗性能。之前有同事看到机器CPU是32核就手写worker_processes 32其实都没问题但auto之后你加机器不用改配置省心。worker_connections 2048表示每个worker进程能同时保持最多2048个连接。这个值决定了单台Nginx的最大并发连接数max_clients worker_processes × worker_connections。8核机器就是最大16384个并发连接。需要注意的是这个数值不是越大越好因为每个连接都对应着文件描述符和内存开销过大反而可能导致系统资源耗尽。sendfile和tcp_nopush这两个配置建议同时开启。sendfile让Nginx直接从磁盘读取文件发到网络栈绕过了用户态到内核态的数据拷贝静态文件下载性能提升非常明显。tcp_nopush在响应大文件时优化TCP包的数量配合sendfile效果最好。gzip_comp_level这个参数我见过有人配置成9其实没必要。压缩等级5到6是性能与体积的平衡点压缩比再往上提升很有限但CPU消耗翻倍增长。而且现在很多前端构建工具Webpack、Vite产出的文件本身就带gz版本这种情况Nginx可以直接开启gzip_static读取预压缩文件连实时压缩都省了。4.2 站点的分离式配置conf.d与server块生产服务器的nginx.conf里不建议把server块全堆在主配置里维护起来会变成灾难。推荐的目录结构是/etc/nginx/ ├── nginx.conf # 主配置只放全局配置和http块 ├── conf.d/ │ ├── default.conf # 默认站点用于兜底 │ ├── blog.conf # 博客项目 │ ├── shop.conf # 商城项目 │ └── vue-app.conf # 纯前端项目 └── ssl/ └── ...每个独立项目一个conf文件文件里就是一个完整server块。这种方式在部署多个Web项目时优势非常明显上线新项目时只需要添加一个新配置文件然后reload不影响正在运行的其他站点定位问题时直接看对应项目的配置文件不用在一大坨配置里翻找。4.3 动静分离的配置形态一个server块内的请求分流动静分离最典型的写法是在一个server块里通过location规则分流。下面是一个实际可用的例子我用注释把每一段的用途标注清楚server { listen 80; server_name www.example.com; root /data/www/example; index index.php index.html; # 静态资源图片、CSS、JS、字体等走Nginx直接返回 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ { expires 7d; # 浏览器缓存7天 access_log off; # 静态资源访问量巨大关掉访问日志减少IO add_header Cache-Control public, no-transform; try_files $uri 404; } # 动态请求PHP文件走FastCGI location ~ \.php$ { try_files $uri 404; fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 其余请求交给index.php处理适合前后端不分离的传统PHP项目 location / { try_files $uri $uri/ /index.php?$query_string; } }上面这段配置的重点是try_files $uri 404;这一行它的作用是先尝试把请求路径作为真实文件返回如果文件不存在就直接返回404绝不把请求交给后端的PHP解析器。这一步被很多人省略后果是攻击者可以通过构造特殊URL比如在/uploads/下面放一个恶意PHP文件让Nginx把它当PHP执行形成严重的安全漏洞。PHP文件必须经过验证确定存在才能交给FastCGI处理这是Nginx安全加固的基础操作之一。另一个细节是location的匹配顺序。我上面写了三个location一个正则匹配静态资源后缀一个正则匹配php后缀一个前缀匹配兜底。Nginx处理时先检查前缀匹配的location记住最长的那个然后开始按顺序检查正则location如果命中某个正则就使用它没命中就用之前记住的前缀location。这里有个容易犯的错把静态资源匹配写成location /assets/前缀匹配把PHP匹配写成location ~ \.php$正则匹配。这时候如果请求/assets/index.php按照Nginx的匹配规则先记下前缀匹配/assets/再检查正则时命中\.php$最终会走PHP解析——这可能不是你想要的效果。理解了Nginx优先用正则location除非没有正则命中才用前缀匹配这条规则很多诡异的请求分流问题都能解释得通。4.4 fastcgi_params和fastcgi.conf的差异在配置PHP转发时会用到两个文件/etc/nginx/fastcgi_params和/etc/nginx/fastcgi.conf。两者最大的区别在于fastcgi.conf多了一行fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;而这行参数是PHP正常解析的必需条件。如果只includefastcgi_params你会发现PHP文件返回空白页或404因为PHP-FPM不知道要去加载哪个文件。有些老教程喜欢在server块里手动加上fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;这行然后include fastcgi_params。这种做法也没问题但如果include了fastcgi.conf又重复设置了这个参数反而可能因为变量覆盖顺序导致意外。建议统一使用fastcgi.conf因为PHP环境必须设置SCRIPT_FILENAME既然都要写不如直接include完整版。另外让PHP-FPM能正确读取文件还有个潜在问题如果Nginx和PHP-FPM在不同的容器或机器上$document_root指向的路径在两边必须一致比如都在/data/www/example否则Nginx认为文件存在、PHP-FPM那边却找不到文件报Primary script unknown错误。这条在Docker部署PHP-FPM时踩过好几次坑后来固定用统一的基础镜像挂同一份代码目录才一劳永逸。4.5 静态资源缓存策略与版本管理动静分离配置完之后缓存的header也值得仔细调。上面的配置里我设置了expires 7d意思是图片、CSS、JS这类文件让浏览器缓存7天。生产环境我一般会按文件类型区分缓存时间文件类型缓存策略理由htmlno-cache页面内容可能频繁更新不能长期缓存css/js7-30天通常通过构建加版本号main.9ff2a1.js来控制更新图片字体30天很少有场景需要修改图片或字体api接口响应由后端控制不做统一前端缓存关于版本号有个常见的误解很多人以为设置了7天缓存用户就要7天之后才能看到新版CSS/JS。其实现在的构建工具默认都给文件名打上内容哈希比如app.b3a5f0c2.js内容变了文件名就变了浏览器会把它当成全新资源去请求旧的缓存文件自动淘汰。所以放心大胆地给静态资源设置长缓存前提是必须配合文件名哈希否则就老老实实设置短缓存。expires指令设置的其实是响应头里的Expires和Cache-Control: max-age浏览器会按这个值决定是否发起请求。而add_header Cache-Control public, no-transform这行里的no-transform是告诉中间的所有代理节点比如CDN不要对响应内容做任何转换。这个配置在个别网络环境里防止运营商缓存劫持特别管用。5. 部署完才是开始多站点部署、HTTPS与常用排错手册5.1 同IP多域名站点部署三种写法选一种LNMP搭好之后最常见的需求就是在同一台服务器上部署多个Web项目。实现方式无非三种第一种是基于不同端口每个server块监听不同端口8080、8081通过IP端口区分站点。好处是简单粗暴一个域名都不用买缺点是不方便对外服务用户还得记住端口号也不利于SEO。第二种是基于不同域名也就是虚拟主机最常用的方式server { listen 80; server_name blog.example.com; root /data/www/blog; } server { listen 80; server_name shop.example.com; root /data/www/shop; }Nginx收到请求时根据请求头里的Host字段匹配server_name从而确定要走哪个server块。这也是反向代理场景里多个站点共用一台Nginx的主要实现方式。第三种是基于不同路径前缀location /blog/和location /shop/一般不推荐因为前端路由会跟路径冲突部署Vue这类单页应用时尤其痛苦。生产环境最合理的方式是域名HTTPS证书的组合。每个项目有自己独立的server_name各自配自己的SSL证书互不干扰。同一个IP上挂多少个HTTPS站点都可以只要都有独立的证书文件——这也就是常说的SNIServer Name Indication技术Nginx通过请求头里的域名信息来协商对应的证书。5.2 部署Vue/React静态项目的注意事项纯前端项目Vue、React构建产物部署到LNMP环境里最经典的问题是前端路由的404现象刷新一个子路由页面比如/user/profileNginx按路径去找物理文件发现/data/www/myapp/user/profile不存在就直接返回404。解决方案是加一段try_files回退规则server { listen 80; server_name myapp.example.com; root /data/www/myapp/dist; location / { try_files $uri $uri/ /index.html; } location /assets/ { expires 30d; add_header Cache-Control public, no-transform; } }try_files $uri $uri/ /index.html的意思是先看有没有这个文件再看有没有这个目录都没有就把请求重写到/index.html由前端路由接管。这段规则实现了服务端全部兜底到前端入口的效果。但这里有个隐藏问题如果某个接口路径也被后端接管了且不小心匹配到location /这个规则那接口请求就会被打回/index.html而不是转发给PHP导致接口返回200但内容是HTML页面。所以在部署纯前端项目的Nginx上API请求一般单独用location /api/ { proxy_pass ...; }转发走location /只负责兜底前端路由。前后端的路径规划从一开始就要分开不然上线之后的配置会越补越乱。这里说一个和标题关键词相关的点如果动静分离后还打算接入后端服务同时把Nginx作为反向代理放在最前面那么Nginx转发请求时默认会带上HTTP请求头包括X-Forwarded-For。后端程序拿到这个头就能拿到用户的真实IP而不是Nginx的IP——这正是网上经常搜nginx转发会带五元组信息吗这个问题的答案。这里的五元组更多指的是TCP连接层面的源IP、源端口、目标IP、目标端口和协议Nginx转发后会用自己的身份跟后端建立新连接但通过X-Forwarded-For头把原始客户端IP信息传递给后端这层逻辑必须搞清楚否则后端做IP限流时可能抓到一堆Nginx的IP。5.3 HTTS证书配置与常见写法当动静分离、多站点都配置完成后下一步就是上HTTPS。证书配置这块如果用云厂商的免费证书拿到的通常是三个文件证书文件.crt或.pem、私钥文件.key。把这两个文件放到指定位置后server块改造如下server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/ssl/example.com.crt; ssl_certificate_key /etc/nginx/ssl/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # ... 其他配置复用上面的动静分离规则 } server { listen 80; server_name www.example.com; return 301 https://$host$request_uri; }第二段server是经典的HTTP到HTTPS的永久重定向301告诉搜索引擎和浏览器这个站点只走HTTPS这样用户访问http://www.example.com时会自动跳到https://www.example.com。注意$host变量会保留原始的域名$request_uri保留原始路径多域名共用时也很安全。ssl_protocols建议只开启TLSv1.2和TLSv1.3TLSv1.0和TLSv1.1已经被IETF正式废弃继续开启会带来已知的协议漏洞风险。ssl_ciphers用HIGH:!aNULL:!MD5就够基础使用了想上更严格的安全评级可以用Mozilla的SSL配置生成器一键生成。5.4 常用排错手册从502到配置错误LNMP部署过程中最常在我整理了套按现象分类的排错顺序。502 Bad Gateway这个错误出现时按以下顺序排查先确认PHP-FPM是否在运行ps aux | grep php-fpm如果没启动就systemctl start php-fpm。确认Nginx里的fastcgi_pass地址跟PHP-FPM实际监听的地址一致。如果Nginx写的是unix:/var/run/php-fpm.sock而PHP-FPM监听的是127.0.0.1:9000必然502。如果用了socket模式检查socket文件权限确保nginx用户有读写权限。测试命令ls -l /var/run/php-fpm.sock如果是-rw-rw---- php-fpm php-fpm而不是nginx用户可访问就会502。404 Page Not Found的情况大多数是文件路径不对root指令后面的路径跟文件实际存放的路径不一致或者PHP的SCRIPT_FILENAME没设置对。用curl -I http://localhost/test.php手工测试并在Nginx错误日志里看具体报错路径。500 Internal Server Error一般是PHP脚本内部出错去/var/log/php-fpm/error.log看具体的报错信息。这种最好排查往往是代码里使用了PHP 8已废弃的函数或者数据库连接参数填错导致的。504 Gateway Timeout说明PHP-FPM处理请求超时了常见于脚本有死循环或者某个外部请求卡住。优化方向是调大fastcgi_read_timeout但根因通常得从代码层面解决。连接数据库失败如果是Connection refused说明MySQL没起来先确认3306端口是否在监听如果是Access denied for user则检查MySQL用户授权和密码。5.5 日志体系Nginx和PHP-FPM的各司其职日志是整个排错体系的基础。Nginx有两类日志access_log访问日志和error_log错误日志。访问日志记录每一次请求的客户端IP、请求时间、请求路径、状态码、响应大小、User-Agent等是分析流量和排查攻击来源的第一手资料错误日志记录Nginx自身和转发阶段的异常比如连接PHP-FPM失败、配置文件错误等。PHP-FPM也有自己的日志php的error_log记录脚本运行时产生的错误php-fpm.log记录进程池的启动、停止、worker进程异常退出等信息。Exception和Warning都会丢到这里。生产环境我一般把日志级别设为error而不是notice或debug因为notice级别下php-fpm的日志量会非常恐怖磁盘很快被写满。日志需要练出一个习惯先看日志再改配置最后做验证。有一次用户反馈某个页面502我第一时间去看Nginx错误日志发现是connect() to unix:/var/run/php-fpm.sock failed再去查socket权限原来是有个脚本以root身份重启了PHP-FPM导致socket文件的属主变成了root。整个过程靠日志定位只花了一分钟如果没看日志直接去改配置可能越改越乱。6. 动静分离之后的性能验证与体验心得6.1 用压测验证动静分离的效果LNMP搭建完成、各类站点都配置好之后最后一步是验证动静分离到底带来了多少性能提升。我用的工具是ApacheBenchab和curl的计时参数。先做静态资源压测ab -n 10000 -c 100 http://www.example.com/static/css/main.css-n是总请求数-c是并发数。这一条命令会打出吞吐率Requests per second、平均延迟、95%延迟等指标。静态文件在Nginx直出、sendfile开启、gzip生效的情况下本地测试达到几千到上万RPS都是正常的。再用同样的并发参数打PHP动态页面ab -n 1000 -k -c 50 http://www.example.com/index.php动态页面因为有PHP-FPM执行脚本和MySQL查询的开销RPS可能只有静态页面的十分之一甚至更低这完全正常。动态请求QPS能到两三百就已经不错了取决于代码复杂度和数据库性能。对动静分离前后的效果有个直观对比才好。我自己的测试数据动静分离之前所有请求都走PHP处理同一台机器扛300并发时CPU已经100%响应时间飙升到几秒动静分离之后同样300并发下静态资源全部由Nginx直出PHP-FPM只处理少量动态请求整体吞叶量提升了四五倍CPU占用反而降下来了。这就是动静分离的核心价值——不是静态文件变快了而是动态请求不再被静态请求拖累整台机器的资源利用率得到了合理分配。6.2 线上运行几个常见的隐患LNMP在线上跑了一段时间后有几个隐患会逐渐暴露出来这里提前说一下Nginx日志磁盘暴涨静态资源请求量大如果不关掉静态资源的访问日志一天就能写几个GB。动静分离时静态资源location里的access_log off;一定要保留或者把静态资源日志单独指到另一个文件定时用logrotate切割清理。PHP-FPM进程耗尽pm.max_children设置太小的话高峰期所有PHP进程都在忙新的动态请求只能在队列里排队客户端表现为页面卡住半天才打开。此时重启PHP-FPM能暂时缓解但根本办法是调大max_children或者检查代码里有没有死循环和慢SQL导致单个请求占用过久。MySQL连接数打满PHP进程太多本身就是隐患——每个PHP请求都会占用一个MySQL连接如果max_children20但MySQL的max_connections100理论上没问题但如果PHP代码里连接没有正确释放或用了短连接且频繁建立新连接连接还是会被耗尽。监控MySQL连接数如果长期超过80%就该考虑引入连接池或限流了。防火墙和SELinux的干扰CentOS上SELinux是很多诡异问题的根源。比如Nginx可以解析PHP页面的其他配置都对却还是502检查getenforce如果是Enforcing可能就拦掉了Nginx到php-fpm.sock的访问。解决方案是setsebool -P httpd_can_network_connect 1或在SELinux里给socket文件加规则而不是图省事直接setenforce 0临时关闭SELinux重启后失效生产环境长期关闭SELinux属于运维事故级别的问题。6.3 日志监控和日常保养建议LNMP上线后建议接一套简单的监控告警。不用上太重的方案就用crontab脚本定时检查端口存活#!/bin/bash # check_lnmp.sh if ! pgrep nginx /dev/null; then echo nginx down | mail -s ALERT: nginx down adminexample.com systemctl restart nginx fi if ! pgrep php-fpm /dev/null; then echo php-fpm down | mail -s ALERT: php-fpm down adminexample.com systemctl restart php-fpm fi生产级监控可以基于PrometheusAlertmanager或Zabbix来完成但上面这个简单脚本作为兜底方案在SMB场景下已经很实用了。核心思路是把日志、端口、进程三个维度都覆盖到任何一个环节异常能被及时发现而不是等用户来投诉页面打不开。日常的日志切割也建议提前配好日志文件长时间不切割单个文件可能撑到好几个GB不仅查询麻烦还会占满磁盘。Nginx和PHP-FPM各配一个logrotate规则# /etc/logrotate.d/nginx /var/log/nginx/*.log { daily missingok rotate 14 compress delaycompress notifempty create 640 nginx adm sharedscripts postrotate [ -f /var/run/nginx.pid ] kill -USR1 cat /var/run/nginx.pid endscript }这段的kill -USR1是让Nginx重新打开日志文件的信号不重启进程就能切换新的日志文件句柄。PHP-FPM的日志切割类似路径换成/var/log/php-fpm.log信号换成USR1或USR2具体查PHP官方文档对应版本。6.4 从LNMP往后走微服务和容器化场景下的LNMP变体写完这篇文章之前我一直在想一个问题现在容器化这么普及LNMP这套知识还有没有必要花时间学我的答案是仍然有必要而且必要程度不低。容器化的LNMP本质上还是LNMP只是把每个组件放进了各自的容器里。你在裸机上学的Nginx配置、PHP-FPM调优、MySQL连接管理在容器里一样适用只是服务编排和网络模式变了。现在很多若依这类框架的部署文档里就是写一个docker-compose.yml里面定义了nginx服务、mysql服务、redis服务前端build好的静态资源挂载到nginx容器里后端API通过proxy_pass转发到后端的Java服务。这种架构的爱恨情仇清晰可辨Nginx桶演动静分离反向代理两个角色静态资源在它这里直出API动态请求转发给后端应用。所以不要觉得学会了裸机LNMP就过时了容器里的配置跟裸机几乎一样只是少了编译安装那一步。如果确实想走容器化路线建议在裸机上把LNMP完整跑几遍再切容器否则排错时连日志该从哪里看都不清楚直接被容器网络模式绕晕。我个人经验是先在裸机上把一套环境调顺再去接触容器编排知识的迁移效率会高得多。最后分享一个我实操中养成的习惯每次改完Nginx配置先nginx -t测试语法再nginx -s reload平滑加载——这个顺序几乎养成了肌肉记忆。LNMP部署本身的难度并不高真正常翻车的地方全在细节上权限没设对、路径对不上、缓存没生效、日志没切好。希望这篇总结能给你省下一些摸索的时间特别是把动静分离这四个字背后真正该做的事情讲透——不是配一个location就完事而是从请求分流、缓存策略、日志监控到后续扩展的完整链路每一步都值得认真对待。
返回列表