ARTICLE DETAIL

资讯详情

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

LNMP环境搭建实战:Nginx与PHP-FPM动静分离配置全解析

LNMP环境搭建实战:Nginx与PHP-FPM动静分离配置全解析 LNMP这套组合我用了很多年从最早手动编译Nginx、再装PHP和MySQL到后来用一键脚本、docker-compose每次回看都发现最值钱的不是那条安装命令而是“为什么要这么搭”。“Nginx 系列实战”写到第三篇这回我完整梳理一下LNMP环境从零搭建、以及动静分离配置的学习总结。这篇内容主要面向已经在用Nginx部署静态站点、但还没把PHP和数据库串起来的朋友也适合刚接触LNMP、想知道每个配置项到底在干什么的初学者。我会把安装过程、配置思路、踩过的坑都摊开讲尽量让每个选择都有依据而不是背一条配置模板就完事。1. LNMP架构与前期规划1.1 一次搞懂Nginx、PHP-FPM、MySQL是怎么协作的很多人第一次搭LNMP都会被“Nginx处理静态、PHP处理动态”这句话带偏以为Nginx和PHP是平级关系实际上它们根本不在一个层级上。一个完整请求的路径是这样的浏览器发起请求先到NginxNginx看请求的是静态资源比如图片、CSS、JS就直接从磁盘返回如果请求的是PHP脚本Nginx自己处理不了就把它转交给PHP-FPM进程PHP-FPM执行PHP代码时如果需要读写数据库再通过MySQL协议去连接MySQL服务器。所以Nginx在这里的角色是“入口网关静态服务器”PHP-FPM是“动态解析器”MySQL才是最终的数据仓库。动静分离的本质就是把“Nginx直接返回静态文件”和“Nginx转交PHP解析”这两条路径理顺让静态请求不经过PHP动态请求不碰静态逻辑各干各的活。我在实际部署时见过最典型的反面案例就是有人把PHP-FPM的listen端口和Nginx的proxy_pass端口搞混或者把fastcgi_pass配置成了PHP-FPM的9000端口但服务根本没监听结果502。理解了这条链路后面所有配置问题都能顺着链路去排查。1.2 版本选型与目录规划先明确版本这是安装前最该花时间想清楚的事情。不同项目对PHP版本要求差异特别大老项目可能只兼容PHP 7.4新框架比如Laravel 10、ThinkPHP 8则要求PHP 8.1以上MySQL有5.7和8.0之分8.0在身份认证插件上有变化默认的caching_sha2_password会坑掉一部分老版本PHP扩展Nginx相对宽容1.20和1.24稳定版都行建议选主线版的最新稳定版本旧版本在HTTP/3支持上会差一些。我的选型建议是新项目一律Nginx 1.24PHP 8.2MySQL 8.0老项目根据框架文档来不要盲目升PHP因为PHP 8.0以后很多函数行为变了以前能用mysql_*的老代码基本全废。目录规划上我用这套约定维护多个项目时特别清晰/data/www/存放网站根目录/data/logs/nginx/存放Nginx日志/data/logs/php/存放PHP-FPM日志/data/mysql/存放MySQL数据文件/usr/local/nginx/Nginx安装目录日志单独放是常年被踩出来的教训默认的/var/log目录在系统盘一旦日志暴涨比如被扫描、被CC就会把系统盘塞满而放数据盘则能扛得住。后面排查问题也方便不需要临时去翻系统目录。2. 三件套安装与环境配置2.1 Nginx源码编译安装与关键参数用系统包管理器的Nginx虽然省事但版本往往落后而且缺少常用模块。我推荐源码编译参数可控日后要加模块也清楚自己装了什么。以CentOS 7.9/8.x为例先装依赖yum install -y gcc gcc-c make autoconf libtool pcre-devel zlib-devel openssl-develUbuntu的话对应的是build-essential libpcre3-dev zlib1g-dev libssl-dev。这些依赖分别是编译器、正则库、压缩库和SSL库缺哪个后续configure都会报错。然后下载Nginx源码并编译。我的常用configure参数是./configure \ --prefix/usr/local/nginx \ --usernginx \ --groupnginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_gzip_static_module \ --with-http_stub_status_module \ --with-http_realip_module \ --with-file-aio几个参数的解释http_ssl_module是支持HTTPS的必须模块没有它配置listen 443 ssl会直接报错http_v2_module用于HTTP/2能显著提升多个并发请求的加载速度stub_status_module提供/nginx_status页面查连接数很有用realip_module配合CDN或反代时能拿到真实IP没有它日志里全是代理IP。编译安装make -j$(nproc) make install这里-j$(nproc)是让编译用满CPU核心数第一次编译时很多人不用这个参数一个单核机器等半天。装完后创建nginx用户顺便把systemd服务建好不然重启系统后Nginx不会自动拉起来useradd -r -s /sbin/nologin nginxsystemd服务文件放在/usr/lib/systemd/system/nginx.service内容大致是ExecStart指向/usr/local/nginx/sbin/nginx加入Restarton-failure这样Nginx进程异常退出后会自动重启。我建议这里多花两分钟后面生产环境少一个坑。2.2 PHP-FPM安装与socket通讯配置PHP一定装PHP-FPM版本它才是和Nginx对接的进程管理器。我按PHP 8.2来。CentOS用remi源Ubuntu有ondrej/php PPA装的时候顺手把常用扩展一起装了yum install -y php82-php-fpm php82-php-cli php82-php-mysqlnd php82-php-gd php82-php-mbstring php82-php-xml php82-php-curl php82-php-zip php82-php-redis php82-php-opcache装完后需要重点改/etc/php-fpm.d/www.conf或对应版本路径这里最关键的就是监听方式。我的建议是单机部署用Unix Socket多机分离再改成TCP。listen /run/php-fpm/www.sock listen.owner nginx listen.group nginx listen.mode 0660为什么不直接listen 127.0.0.1:9000因为Unix Socket走的是内核进程间通信不走TCP/IP协议栈少一层封装在高并发小请求的场景下性能差距能到5%到10%。代价是Nginx必须和PHP-FPM在同一台机器上而且socket文件有权限问题所以需要:0660并指定属主为nginx。如果哪个服务读不到socket文件报错通常就是“connect() failed (13: Permission denied)”。进程管理参数是按机器内存估的。2C4G的机器我都这么配pm dynamic pm.max_children 50 pm.start_servers 5 pm.min_spare_servers 5 pm.max_spare_servers 35max_children不是越大约好每个PHP-FPM子进程平均占用内存大概30MB到50MB50个就是1.5GB到2.5GB和4G内存匹配如果机器是1G内存就要把max_children降到15左右否则很容易OOM。这也是我踩过最惨的坑当年在1G小机器上把max_children拉到100结果内存耗尽连SSH都登录不上。2.3 MySQL安装与基础权限配置MySQL 8.0在CentOS上直接用yum装会拿到系统仓库版本但如果你是CentOS 7默认repo那个版本非常老建议装官方repo或者直接用二进制包。我图省事用官方repo安装yum install -y https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm yum install -y mysql-community-server systemctl start mysqldMySQL 8.0安装后会在/var/log/mysqld.log里生成临时root密码首次登录必须用它连接然后强制改密码。这里有个操作细节临时密码里经常有特殊字符在shell里要加引号很多新手直接复制在命令行里发现各种报错。生产环境建议一起改掉的东西ALTER USER rootlocalhost IDENTIFIED BY 你的强密码; DELETE FROM mysql.user WHERE User; CREATE DATABASE app_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER app_userlocalhost IDENTIFIED BY 你的强密码; GRANT ALL PRIVILEGES ON app_db.* TO app_userlocalhost; FLUSH PRIVILEGES;这里我用utf8mb4而不用utf8是因为utf8在MySQL里根本不是真正的“全量Unicode”emoji和生僻字都能让它报错或者乱码。项目一上线再改字符集非常痛苦所以建库这一步就选对。权限分配的核心思路是最小权限数据库账号只给对应库的权限不要让应用账号用root直连连接地址能写localhost就不写%。我在公司审计时见过好些项目用root跑业务一旦SQL注入攻击者拿到的是数据库管理员的权限那就不是丢几张表的问题了。3. 站点配置与动静分离落地3.1 一个干净的站点配置文件长什么样三件套装好后进入真正配置Nginx的阶段。我习惯在/usr/local/nginx/conf/conf.d/下按项目建配置文件然后在主配置里include这样项目和项目之间互不干扰haproxy、nginx这种管理的风格是一致的。以app.test.com为例最终配置如下server { listen 80; server_name app.test.com; root /data/www/app/public; index index.php index.html; access_log /data/logs/nginx/app.access.log main; error_log /data/logs/nginx/app.error.log warn; # 静态文件直接交给Nginx返回 location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ { expires 30d; add_header Cache-Control public, max-age2592000; access_log off; } # 上传目录单独处理避免脚本执行 location /uploads/ { alias /data/www/app/public/uploads/; expires 7d; access_log off; } # PHP动态请求转发给PHP-FPM location ~ \.php$ { try_files $uri 404; fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTPS off; } location / { try_files $uri $uri/ /index.php?$query_string; } }这是一份能支撑ThinkPHP、Laravel这类前后端一体项目的标准配置。它做了三件事静态资源加长缓存、上传目录隔离、PHP请求走FastCGI正好对应动静分离的三个层面。3.2 location匹配规则动静分离的地基location的匹配优先级经常被低估我见过有人在这个上面卡了一晚上配置看着没问题但访问/uploads/a.jpg就是不生效原因就是被前面的正则匹配抢先了。Nginx的location匹配优先级从高到低是规则类型示例优先级精确匹配location /index.php最高前缀匹配停止location ^~ /static/高正则匹配location ~ \.php$中按配置顺序普通前缀匹配location /uploads/低取最长匹配在动静态混合的站点中最容易出的问题有两个。一是把location /uploads/写在带~的正则规则后面而正则规则又是\.*\.(php|jpg|png)$之类把所有文件都圈进去的“大杂烩”结果uploads目录的alias配置永远不生效。解决办法很简单如果某个目录明确要Nginx直出静态文件就用^~前缀匹配比如location ^~ /uploads/正则规则再也不会抢它。二是location ~ \.php$里的~表示区分大小写如果项目里某些PHP文件名有大写比如Index.PHP就会漏匹配掉。看项目实际情况需要兼容就改成~*但性能会稍微降一点日常问题不大。3.3 动静分离如何真正分离出效果上面的配置文件已经带出了动静分离的基本形态但真要“分离出效果”还要在三个地方下功夫。第一是缓存策略。静态资源的expires 30d只是“告诉浏览器可以缓存30天”但Nginx到磁盘之间的读取并没有省掉。要减轻Nginx自身I/O负担可以开open_file_cache缓存文件句柄和元信息open_file_cache max1000 inactive20s; open_file_cache_valid 60s; open_file_cache_min_uses 2; open_file_cache_errors on;第二是动态请求的FastCGI缓存。PHP代码是动态的但很多页面或接口在几秒内其实根本没变这时候可以让Nginx缓存PHP-FPM返回的结果。在http层定义一个缓存路径fastcgi_cache_path /data/nginx/fastcgi_cache levels1:2 keys_zonephp_cache:10m inactive1h max_size1g;然后在server或location里启用location ~ \.php$ { fastcgi_cache php_cache; fastcgi_cache_valid 200 301 302 30s; add_header X-FastCGI-Cache $upstream_cache_status; }加一个X-FastCGI-Cache响应头可以用浏览器开发者工具直接看到命中结果是HIT还是MISS。不过我提醒一句凡是涉及登录态、个性化数据的页面不能直接开FastCGI缓存否则用户A看到用户B的数据这是生产事故。要缓存就只缓存公开的、不依赖session的接口和页面。第三是压缩。静态和动态都该开gzip配置放在http块里全局生效gzip on; gzip_vary on; gzip_min_length 1024; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript application/xml image/svgxml;gzip_comp_level不是越高越好6级以上CPU开销陡增、收益很低5级是性能和压缩比的平衡点。图片不要gzipjpg/png本来就是压缩过的再压缩浪费时间还降不了多少体积。3.4 多站点隔离与PATH_INFO支持实际生产很少只跑一个项目/data/www/建一个目录一个站点是常态。多站点部署的关键是每个server块独立、每个项目一个root、日志分开。如果两个server块的root指向同一个目录那它们在配置层面其实就没隔离一个项目的动静分离规则会影响另一个。所以我在原则上是目录隔离优于server块隔离server块隔离优于location隔离。另一个隐藏的坑是PHP框架的PATH_INFO支持。ThinkPHP、CodeIgniter这类框架喜欢用/index.php/Home/Index这种URL格式如果Nginx配置里只写了fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;不带PATH_INFO取不到干净的路径参数路由就会全部404。这种情况下要显式加上fastcgi_param PATH_INFO $path_info;但$path_info在Nginx里不是内置变量一般用fastcgi_split_path_info来提取例如针对ThinkPHPlocation ~ ^(.\.php)(.*)$ { fastcgi_split_path_info ^(.\.php)(.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_pass unix:/run/php-fpm/www.sock; }这类问题排查起来特别隐蔽页面打开是200但内容全是404或者空白日志里又不报错其实就是PATH_INFO没配好。4. 踩坑记录与排查思路4.1 访问PHP文件变成下载或乱码刚搭好LNMP时最常见的问题就是浏览器访问index.php下载了一个文件或者干脆白屏。这个现象的本质是Nginx没有把PHP文件交给PHP-FPM解析。排除思路就三步第一确认PHP-FPM在跑ps aux | grep php-fpm能查到进程第二确认配置文件里location ~ \.php$块存在且fastcgi_pass的socket路径和PHP-FPM的listen完全一致这里最容易犯的错是socket路径不同Nginx连不上第三确认fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;这一行存在没有它Nginx不知道要执行哪个文件。曾经有个朋友卡了大半天配置看起来都对最后还是我发现他改的是/etc/nginx/nginx.conf但Nginx实际加载的是/usr/local/nginx/conf/nginx.conf——两个Nginx配置目录不一样。这种情况在源码编译安装的机器上很常见先nginx -t看配置语法再nginx -T看完整生效配置能省下大量排查时间。4.2 502 Bad Gateway与504 Gateway Timeout502基本上等于“Nginx连不上后端PHP-FPM”集中在三个原因。一是PHP-FPM没启动或者崩了看进程和/data/logs/php/里的日志二是socket权限不对Nginx的worker进程跑在nginx用户下而socket文件属主不是nginx或者权限不是0660就报Permission denied三是连接数满了pm.max_children设太小并发一高进程全部忙碌新请求进不来报502。502之后紧跟着就是504它的含义是“PHP-FPM收到了请求但处理超时”。PHP里默认max_execution_time是30秒Nginx的fastcgi_read_timeout默认60秒。如果有个接口要跑80秒Nginx先在它那层超时客户端看到504。这种时候必须割裂看是PHP层慢还是Nginx转发层慢# 查看PHP-FPM的slow log有没有记录慢请求 grep -i slow /var/log/php-fpm.log设置request_slowlog_timeout 10s和slowlog /var/log/php-fpm-slow.log后超过10秒的PHP请求都会把调用栈打到日志里比Face Value地调超时时间高效得多。4.3 权限监听与日志路径的隐性坑LNMP从根上踩项目权限最常见的是把/data/www/的属主设成rootNginx根本读不了文件然后页面要么403要么全空白。生产环境我推荐这个权限方案chown -R nginx:nginx /data/www/app; find /data/www/app -type d -exec chmod 755 {} \; find /data/www/app -type f -exec chmod 644 {} \;Nginx用户对目录有读和执行权限即可写权限只给特定项目目录比如/uploads/并且单独再授权给PHP-FPM用户。这里要看清一个因果Nginx和PHP-FPM的工作用户不是同一个时PHP脚本要写上传目录却因为目录属主是nginx而没权限写并且Nginx日志里还不会提示因为写文件的是PHP进程不是Nginx进程。排查这类问题最好的办法是看PHP-FPM错误日志。日志路径这个坑我前面提过一次但值得单独再说。务必把access_log和error_log都显式写到配置里不要依赖默认路径。默认路径通常位于系统盘日志暴涨会把磁盘写满独立路径方便做日志切割也方便交给日志采集工具。日志轮转用logrotate就行配置写好后可以手动触发一次logrotate -f /etc/logrotate.d/nginx验证切分效果不要等磁盘满了再处理。4.4 动静分离后缓存不生效一类的缓存疑难动静分离配置好后有时会发现部分静态文件就是不缓存明明expires都加了。浏览器开发者工具里看到Cache-Control: no-cache大概率是Nginx兜底配置了add_header Cache-Control no-cache;把它写在server块里后所有location都继承了静态location的add_header Cache-Control public...又被同名的父级指令覆盖最终不生效。Nginx的add_header有个继承规则一旦当前level有add_header父级的add_header全部失效所以静态location里写了add_header Cache-Control父级那个no-cache也就失效了。反过来如果静态location里没写任何add_header父级的no-cache反而会继承下来这是个很好的排查点。另外如果开了FastCGI缓存但“动态页面还是每次重新生成”看X-FastCGI-Cache响应头是不是一直是MISS。MISS的原因通常是缓存key里带了用户标识或者请求方法不是GETHEAD又或者缓存路径权限不对。日志里查fastcgi_cache的结果最直观tail -f /data/logs/nginx/app.error.log | grep cache我在项目里还遇到过“关了FastCGI缓存但缓存还在”的假象其实是浏览器端的Expires还在生效清理浏览器缓存后一切恢复正常这也是排查缓存类问题时要记住的分清服务端缓存、代理缓存和浏览器缓存这三层互相叠加很容易乱。5. LNMP性能调优与扩展方向5.1 调高并发先动这三个参数LNMP搭完只是开始想在高并发下扛住压力需要动三个容易忽略的参数。Nginx事件模型events { worker_connections 4096; use epoll; }use epoll在Linux下是内核级的事件通知机制效率远超select/poll尤其长连接多的时候差距很明显。worker_connections是每个worker进程的最大连接数乘以worker进程数就是理论最大并发连接数。我经常看到有人把它从1024调到10240却忘了文件描述符上限也要一起调结果报“too many open files”ulimit -n 65535PHP-FPM的pm.start_servers和pm.max_children要结合pm.status_path来看。开启pm.status_path /status后把Nginx配一个location /status访问/status?full就能看到每个子进程的空闲、忙碌情况。根据这个调整进程数才不会被“看起来够用”骗到。我曾经发现一台4核8G机器满载运行时还有几十个空闲FPM进程白白占着内存后来把min_spare_servers调小内存占用直接降了700MB。MySQL的max_connections默认151在PHP连接池没开通时很容易被打满。调大之前先想清楚每个MySQL连接本身就是一段内存缓冲连接数翻倍内存不一定够这时候真正该做的是引入连接池或者在PHP层用完就关。这里分享一个观察SQL日志的经验slow_query_log一旦开启能看到大量慢查询集中在同一张表上光把max_connections调大是掩盖问题不是解决问题。5.2 反向代理与HTTPS这两件事也顺手做LNMP本身是一个站点但Nginx真正强大的地方是反向代理。动静分离解决的是“静态文件不麻烦PHP”反向代理解决的是“整个后端服务不直接暴露”。如果业务后面是Java应用或者Go服务配置这样的location就能把请求转发过去location /api/ { 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; }X-Real-IP和X-Forwarded-For如果不设置后端拿到的全是Nginx本机IP业务里做日志审计、风控、地域判断就全废了。这个配置很多项目反代已经写了但漏了那一行的比比皆是也导致了不少安全问题。HTTPS在这个系列里属于顺手的扩展。证书放好路径然后在80的server里加跳转listen 80; server_name app.test.com; return 301 https://$host$request_uri;然后443的server块里把之前80的server内容复制过去唯一一个要改的是fastcgi_param HTTPS on;保证PHP里$_SERVER[HTTPS]正确否则项目里判断“当前是不是HTTPS”的逻辑会出错生成的回跳链接全是http被浏览器拦成不安全链接。5.3 从LNMP到容器化的升级路径LNMP这套组合完全可以迁移进容器而且迁移顺序很有讲究。我第一次做容器化时傻乎乎地一顿操作把三个服务全部打成镜像结果数据、日志、配置文件全部乱七八糟后来总结了一套稳妥的顺序先容器化MySQL因为状态最独立数据卷挂载做好就完事再容器化PHP-FPM把代码目录挂进去最后容器化Nginx通过同一个代码目录与PHP容器共享文件通过nginx:alpine镜像配合docker-compose把三个容器串起来供应一个站点。容器化最大的好处不是“更炫”而是环境一致性。你本机调好的Nginx配置、PHP扩展、MySQL字符集在服务器上一键启动就是同一套不存在“我机器上明明能跑”的尴尬。但我也要说清楚容器化不等于自动化LNMP底层的那些配置逻辑一点都不会变反而因为多了一层容器网络需要额外考虑容器间的网络通信。好在我们这篇已经理解了socket和TCP的不同迁移时就知道用同一个compose网络让PHP-FPM和Nginx通过容器名互相访问而不是继续写127.0.0.1。6. 写在最后的一些经验和习惯我在反复搭建LNMP的过程中最深的体会是一套环境最终的稳定性不取决于第一次安装多顺利而取决于后续的维护习惯有多好。比如每次改动配置前先备份一下当前可用的配置改完用nginx -t验证再reload这两步能避免90%以上的线上故障。动静分离这个题目说到底是让每一层只干它擅长的事。Nginx擅长高效读静态文件就让它读PHP擅长业务逻辑就让它专心解析MySQL擅长检索和事务就别在PHP里写一堆低效SQL去拖垮它。配置文件的每个location、每条fastcgi指令背后都应该是这个原则而不是“别人这么写我也这么写”。最后再分享一个小技巧每次搭完环境后把Nginx的nginx -T输出PHP-FPM的php -m、MySQL的SHOW VARIABLES关键项都存一份快照放到项目文档里。以后出现莫名奇妙的兼容性问题时对比快照定位配置差异比对着报错盲目搜答案快得多。这套方法我用了很多年依然是排查LNMP疑难杂症最高效的起点。
返回列表