ARTICLE DETAIL

资讯详情

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

PHP网站流量暴涨10倍?从PHP-FPM到MySQL的排查与止血指南

PHP网站流量暴涨10倍?从PHP-FPM到MySQL的排查与止血指南 先说个可能反直觉的结论流量暴涨当天真正把网站拖垮的往往不是 PHP 代码本身而是流量进来之前就埋下的那几个隐患——连接数上限、慢查询、缓存穿透、日志写入阻塞。这个我后面会详细拆。先交代背景这篇文章想聊的是当一个日活平平的 PHP 网站因为某个外部因素比如被首页推荐、接口被刷、活动页爆量在 24 小时内流量翻了 10 倍从服务器负载到 PHP-FPM 进程、从数据库连接到第三方接口超时这一整条链路上到底会发生什么以及我们该怎么一步步排查、止血、恢复。不管你是自己维护服务器的独立开发者还是在团队里背运维 KPI 的后端这篇文章都值得对照着你自己的环境看一遍。我尽量不堆理论直接用实际场景讲。你会看到“流量增长 10 倍”这个命题在小站点和大站点上的表现完全不同但底层那套排查逻辑是通用的。顺便说一句文章里提到的命令、配置、排查路径都是我在真实服务器上验证过的不是随手写的。1. 流量翻十倍的第一天最先崩溃的往往是看似无辜的组件1.1 你以为的瓶颈和实际的瓶颈通常不是同一个很多人的第一反应是“流量大了CPU 肯定先爆”。实际上在 PHP 网站里流量进来之后最先出问题的往往是php-fpm 的子进程数和MySQL 的最大连接数。原因不复杂。PHP-FPM 默认配置下pm.max_children可能是 10 或者 20每个请求占用一个子进程。当并发请求超过这个数新的请求就只能排队等空闲进程。你看到的现象是页面一直在转圈CPU 占用率却不高nginx 返回 502。这时候你去top看负载可能只有 2但网站已经打不开了。流量涨 10 倍意味着什么假设原来平均每秒 5 个请求那现在就是 50 个。如果某个页面里有外部接口调用、有多个 SQL 查询每个请求平均耗时 300 毫秒那么同一时刻需要处理的并发请求就是 50 × 0.3 15 个。如果max_children只有 10那就已经有 5 个请求在排队。排队时间一长前端就超时报错用户刷新又增加新的请求——这就是雪崩的开始。1.2 MySQL 连接数爆发比 CPU 爆掉更常见还有一个我见过很多次的场景数据库连接数被打满。PHP 默认的 MySQL 连接池行为是“脚本结束即释放”但在高并发下如果脚本执行时间变长连接持有时间也跟着变长。MySQL 的max_connections默认是 151看起来够用但当 PHP-FPM 的并发子进程数超过这个值每个子进程都持有一条数据库连接时数据库直接就拒绝新连接了。这个时候最典型的现象是PHP 报Connection refused或者Too many connections而且不只是数据库相关的接口挂掉所有依赖数据库的页面全部挂掉包括登录、Session、甚至一些看似纯静态的页面如果它们也从数据库拉配置的话。1.3 日志文件成为隐藏杀手还有一个不容易被发现但很致命的问题日志。流量涨 10 倍PHP-FPM 的slow.log、nginx 的access.log、应用的业务日志写入量都会同步涨 10 倍。如果你用的是默认配置、日志文件没有按天切割或者没有用 logrotate 做轮转那么很快会出现两种情况磁盘 IO 被打满因为多个进程同时在往同一个文件里写日志文件无限膨胀最终把磁盘空间占满我曾经遇到过一次事故/var/log/nginx/access.log在 6 个小时内从 200MB 涨到 8GB最后/分区满MySQL 直接拒绝写入。那一次流量其实只涨了 3 倍但日志量涨了 20 倍——因为很多用户在疯狂刷新页面。2. 逐层排查从 nginx 到 PHP-FPM 再到数据库的完整链路2.1 第一步先确认“流量翻倍”是真的还是假象在动手优化之前先做一件事打开 nginx 的 access log按 IP 统计一下请求量。因为很多所谓的流量暴涨实际上是某个爬虫或者竞争对手在刷你的接口。如果你看到一个 IP 占了总请求量的 40% 以上那这不是流量问题是风控问题。统计命令很简单awk {print $1} /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20如果确认是正常用户流量暴增那就继续往下排查。如果是刷流量可以直接在 nginx 层把那个 IP 封掉或者加频率限制。这个操作能在 5 分钟内解决 90% 的“伪流量”问题。2.2 第二步看 php-fpm 的状态页而不是猜很多人遇到 502 就直接重启 PHP-FPM这是治标不治本。正确做法是先看 php-fpm 的状态页。先确认你配置了pm.status_pathlocation ~ ^/php-fpm-status { fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME /var/run/php/php7.4-fpm.sock; }然后 curl 一下状态页curl http://127.0.0.1/php-fpm-status?full关键看这几个指标accepted conn总请求数确认流量确实涨了active processes当前活跃进程数这个数如果等于max_children说明进程池已经满了max children reached如果这个数字在持续增长说明已经发生过多次“进程不够用”的事件我曾经在一次排查中发现max children reached在一小时内从 0 涨到 5000而当时的max_children配置才 20——这意味着有大量请求在排队等待。但 CPU 占用率只有 30%因为大部分时间都花在等待数据库响应上。2.3 第三步打开慢查询日志找到真正的拖油瓶排除了 php-fpm 进程不够的问题之后下一步看 MySQL 慢查询。这一步是最有性价比的因为几乎每次流量暴涨最后都能在慢查询里抓到一两个“平时没问题、流量一涨就爆炸”的 SQL。先确保慢查询日志是开着的SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 2;然后在终端里实时盯着tail -f /var/log/mysql/mysql-slow.log说一个我踩过的典型案例。某个查询平时执行 0.1 秒用户根本感觉不到。流量涨 10 倍后这条 SQL 的执行时间涨到 3 秒。原因很简单表里数据量没变但并发查询多了MySQL 的线程数暴增每条查询都在跟别人抢 CPU 和 IO。慢查询日志会帮你把这个隐藏的定时炸弹暴露出来。2.4 第四步看外部依赖——Redis、Memcached、第三方 API很多时候PHP 网站的瓶颈不在 PHP 本身而在它依赖的外部组件上。流量涨 10 倍时你需要同时检查Redisinfo命令的connected_clients是否接近maxclientsused_memory是否超过 maxmemory如果超过了就开始淘汰 key可能会引发缓存雪崩。第三方 API如果你的业务里有调用微信接口、支付接口、地图 API 之类的操作流量涨 10 倍意味着这些外部调用的 QPS 也涨了 10 倍。第三方接口的限流阈值往往很低比如每秒 20 次一旦触发限流你的服务端就会收到大量超时错误。这时候我通常会做一件事在 PHP 代码里对第三方调用加熔断机制——连续失败 N 次后直接放弃调用返回降级数据而不是让每个请求都去干等超时。3. PHP 层的止血操作从配置到代码的实战调整3.1 调整 php-fpm 配置先保命再优化流量暴涨的第一时间如果确认是 php-fpm 进程不够可以临时把pm.max_children调大。但这有一个前提你的服务器内存要够。每个 PHP-FPM 进程平均占用 30-50MB 内存假设你有 4GB 可用内存max_children调到 80 已经是极限了。盲目调大反而会导致内存溢出进程被 OOM Killer 干掉然后你又看到 502。我给出一个比较稳妥的调整策略pm dynamic pm.max_children 50 pm.start_servers 10 pm.min_spare_servers 5 pm.max_spare_servers 20 pm.max_requests 1000pm.max_requests很容易被忽略。它表示每个子进程处理 1000 个请求后自动退出是为了防止 PHP 代码里的内存泄漏累积。流量大的时候这个参数尤其重要。3.2 开启并分析 PHP-FPM slow log在 php-fpm 的配置文件里加上request_slowlog_timeout 5s slowlog /var/log/php-fpm/slow.log然后实时查看慢日志tail -f /var/log/php-fpm/slow.log慢日志会直接告诉你每个请求卡在哪个函数上。我见过很多次一行慢日志暴露了问题根源——比如某个接口里调用了一个file_get_contents去请求外部 URL而没有设置超时时间默认就这样干等了几十秒。流量一涨这种请求把整个进程池给占满了。3.3 顺手检查 OpCache免费的提速利器流量涨 10 倍之后PHP 的编译开销会被放大。如果你还没开启 OpCache那相当于每个请求都在重复做“编译出来就丢掉”的蠢事。opcache.enable1 opcache.enable_cli0 opcache.memory_consumption128 opcache.interned_strings_buffer8 opcache.max_accelerated_files10000 opcache.validate_timestamps0注意最后一项validate_timestamps0会让 OpCache 不检查文件修改时间性能最好但开发环境千万别这么设不然改完代码不生效排查起来会怀疑人生。生产环境在发布代码时需要执行opcache_reset()或重启 php-fpm。3.4 代码层面临时降级不必羞耻流量暴涨时最怕的是所有功能都必须 100% 可用。在紧急时刻我通常会做这么几件事把搜索功能临时切换成简单的前端过滤或者直接关掉搜索。把一些非核心的统计接口比如用户行为埋点改为只写日志、不入库。把实时通知邮件、短信改异步队列。把首页的个性化推荐临时改成统一展示。这不是品质下降而是架构的容错设计。一个成熟的系统在极端流量下选择“损失部分功能保住核心可用性”比硬撑着导致整个网站挂掉要明智得多。4. 数据库才是真正的命门连接、查询与缓存4.1 先救连接数MySQL 的 max_connections 调整如果你的 MySQL 已经出现Too many connections最快的急救方法是临时调大连接数上限SET GLOBAL max_connections 500;但你要记住连接数不是越大越好。每个连接都在消耗内存和线程资源。普通的 MySQL 在 500 个并发连接时光线程堆栈就能吃掉 1GB 内存。所以调整连接数的同时要配合降低单连接的工作负载否则只是把崩溃的时间往后拖了 30 分钟。另外建议在 PHP 侧把数据库连接方式从长连接改成短连接或者在连接池里设置合理的超时回收时间。这能避免 PHP-FPM 子进程长时间占用数据库连接。4.2 慢查询的三种典型形态和对应的应急方案我在慢查询日志里总结出流量暴涨时代最常见的三类问题以及我的处理方法第一种没建索引的主键扫描平时数据量小全表扫描也就几百毫秒。数据量涨了之后或者并发高了之后几百毫秒变成几秒。这种最简单直接EXPLAIN看一下缺哪个索引补哪个。第二种用了LIKE %keyword%这种查询一旦数据量过万并发稍高就会打满数据库。应急方案是先下线这个功能或者改成用全文索引。但全文索引在 InnoDB 下的性能也不算好更长期的方案是上 Elasticsearch但这就是另一个话题了。第三种连表查询 排序且有多条件。典型的ORDER BY xxx LIMIT 10却扫描了 10 万行。这时候先看 explain 里的rows和Extra有没有Using filesort。如果大量请求都在做相同的排序查询可以给排序字段和 WHERE 字段建联合索引。如果你的业务允许直接把这个查询结果 Redis 缓存 30 秒流量高峰期的效果立竿见影。4.3 读写分离不是银弹但流量翻倍时它真的有用如果你的服务器资源允许流量暴涨时临时做一个只读从库把非核心的报表查询、列表查询切到从库上主库只保留写操作和强一致性的查询。这个操作能在 1 小时内让你的主库压力下降 50% 以上。不过我得说一句读写分离在流量见顶之后往往会引出一致性延迟的问题。所以它更适合当作应急手段而不是长期架构的唯一解。如果你打算长期应对高流量更优先的还是做数据处理层面的缓存和队列。4.4 Redis 缓存重用是慈悲穿透是灾难流量涨 10 倍后缓存命中率会直接决定系统的生死。我见过很多站点的缓存命中率只有 50%流量一翻倍数据库就挂了。原因往往是两个缓存过期时间设置不合理所有 key 同时过期导致缓存雪崩某个高频查询的结果没有缓存导致缓存穿透针对这两点我的建议是过期时间加随机值。不要所有 key 都设置相同的过期时间而是在基础值上加减一个随机数。比如$expire 3600 rand(-600, 600);这样能避免同一时刻大量 key 集体过期把数据库冲垮。空结果也缓存。如果某个查询数据库返回空数组也把它缓存起来设置一个较短的过期时间比如 30 秒。否则这个查询在数据不存在的时候会每次都打到数据库上等于没有缓存。5. 那些“看不见”的瓶颈文件、外部接口与队列5.1 文件存储session 写在磁盘上的代价PHP 默认的 Session 存储是文件。流量涨 10 倍意味着同一时刻大量 Session 文件被读写。如果你没有设置垃圾回收机制session 文件会越堆越多。当文件数超过几万时PHP 定位某个 session 文件的 IO 开销会明显上升。这个问题的解法有三个层次最省事的把 session 存到 Redis一行配置搞定中等方案用 session 分目录存储目录层级加深一些终极方案用 JWT 等无状态认证彻底不依赖服务端 session对于大多数项目把 session 迁到 Redis 是最合适的session.save_handler redis session.save_path tcp://127.0.0.1:63795.2 外部 API 调用超时与重试的优雅降级流量涨 10 倍时外部 API 的失败率会同步上升原因可能是对方的限流也可能是你自己的连接池耗尽。我给你的建议是给所有外部调用设置连接超时和读取超时尤其是file_get_contents千万别裸用$ctx stream_context_create([http [timeout 3]]); $content file_get_contents($url, false, $ctx);curl 则设置CURLOPT_TIMEOUT和CURLOPT_CONNECTTIMEOUT。对所有外部调用做熔断连续失败达到阈值后直接跳过该次调用返回预设的默认值。这样能防止某个外部服务挂掉时你的服务器被同步拖垮。5.3 队列把同步任务变成异步任务流量暴涨时大量非核心操作如果都同步执行会浪费大量 PHP-FPM 进程。比如用户注册后要发邮件、发短信、推送通知如果这些都在请求周期内串行完成每个请求都会多出几百毫秒的耗时。平时这些耗时无所谓流量涨 10 倍时它们就变成了压垮进程池的最后一个砝码。所以如果你有一个合理的 PHP 队列机制比如 Redis php-resque 等生态工具或者用更成熟的消息队列组件请把通知类、邮件类、日志上报类的执行统一压进队列。前端请求只负责写入队列立即返回成功。我见过最夸张的一个案例是某站点的注册接口里调用了三个外部 API 来拉取用户信息平均耗时 2 秒流量涨 5 倍后这个接口直接 503。改成异步队列后接口耗时降到 300 毫秒服务器负载下降了 60%。6. 等流量退潮后复盘清单与容量规划6.1 当天就该做的事留证据、改阈值当流量终于在凌晨降下来你松一口气之后有这么几件事需要当天就做否则下次流量波动你会继续踩同一个坑把当日 php-fpm 的max children reached、MySQL 的max_connections、慢查询日志、nginx 的 5xx 日志全部留存归档。对比流量上涨前后的 QPS 曲线确认当天实际峰值 QPS 是多少这决定你接下来要按什么标准扩容。检查所有日志切割配置确保logrotate在正常工作避免下次流量暴涨时日志把磁盘灌满。6.2 复盘时使用这个表格逐项过一遍关注项日常正常时的表现流量暴涨时的表现应急手段Nginx 连接数worker_connections 足够出现 connection refused调高 worker_connections或加 LBPHP-FPM 进程max_children 用不到一半max_children 打满502 频发临时调高 max_children检查代码耗时MySQL 连接数几十个连接达到 max_connections调大 max_connections优化慢查询慢查询耗时100ms2s占满数据库加索引查缓存读写分离缓存命中率90% 以上跌到 50% 以下修缓存穿透和雪崩日志写入正常轮转文件暴增磁盘 IO 打满配 logrotate关 debug 日志外部 API偶尔超时大量超时阻塞本地请求加超时加熔断6.3 长期容量规划别按“平均流量”规划按“峰值流量”规划最后聊一个策略层面的东西。很多团队在买服务器、配进程数时参考的是日均流量。这是一个错误。你应该按“过去 90 天内的峰值流量的两倍”来规划因为流量翻倍这种事永远比你预期来得更快。当然如果预算有限做不到按峰值两倍买机器那就必须做好弹性伸缩的准备。用 Docker 打包你的 PHP 应用镜像配合负载均衡器流量涨的时候快速扩容实例。这里给你一个最基本的思路用 Dockerfile 构建包含 PHP-FPM 和 nginx 的镜像。用 docker-compose 编排本地的 MySQL或 RDS和 Redis。在云平台上配置自动伸缩策略根据 CPU 使用率自动增加实例。如果你还没接触过容器化也不用一口吃个胖子。先从把应用代码里的本地文件缓存改成 Redis 开始从把 session 改成 Redis 存储开始这些改动对架构的影响最小却能显著提高系统的抗压能力。回到最初的问题——流量增长 10 倍时会发生什么普通站点会先出现搜索变慢然后是动态页面超时接着是 502最后干脆数据库连接拒绝。但如果提前把上面这些环节逐一捋一遍你会发现流量涨 10 倍其实没那么可怕它更像一次免费的压力测试把你系统里的所有薄弱环节一次性暴露出来。修复它们比任何花哨的架构调整都更有价值。
返回列表