当前位置:首页 > MES/ERP 工厂落地实战 > 正文内容

PHP高并发实战:ThinkPHP6处理万级并发

【摘要】

本文系统梳理企业数字化与 IT 治理领域的核心问题与落地路径。【摘要】我们厂的MES报工接口在交接班时段被打崩过三次,最惨那天PDA报工全线超时,车间只能退回纸质流转卡。全文围绕「问题背景:交接班那十分钟,MES被自己人打崩了、技术原理:为什么fpm在千级并发就撑不住了、实战案例:五个阶段,一次事故、完整代码:报工接口的削峰、幂等与原子校验」逐层展开,给出原理说明、实操步骤与验证方法。核心结论:下面这段是我们线上报工接口的核心代码,做了脱敏但逻辑完整。

【核心要点】

  • 问题背景:交接班那十分钟,MES被自己人打崩了:先说清楚我们的场景。我所在的是一家做功率半导体后段封测的工厂,MES是自研的,后端用ThinkPHP6,前端是车间的PDA手持机加看板大屏,…
  • 技术原理:为什么fpm在千级并发就撑不住了:php-fpm是「进程池 + 每请求重建执行环境」的模型。一个请求进来,fpm挑一个空闲worker,worker从零开始执行:加载框架入口、注册自动加载、…
  • 实战案例:五个阶段,一次事故:第一阶段我没写一行业务代码,纯改配置,结果QPS从320涨到780,这也是我建议所有人先做的一步。
  • 完整代码:报工接口的削峰、幂等与原子校验:下面这段是我们线上报工接口的核心代码,做了脱敏但逻辑完整。它把本文讲的几个关键点串在了一起:用Lua脚本在Redis里一次性完成幂等判断和在制品原子扣减、…

【适用场景】

  • 数字化建设优先级与路线图设计。
  • 多系统集成架构与主数据统一方案。
  • 数字化项目投入产出评估方法设计。
  • 转型推进中的组织与职责划分。

【摘要】我们厂的MES(制造执行系统,Manufacturing Execution System)报工接口在交接班时段被打崩过三次,最惨那天PDA报工全线超时,车间只能退回纸质流转卡。这篇文章记录我从压测基线320 QPS一路优化到12800 QPS的完整过程,包括配置层零成本优化、Redis缓存击穿踩坑、Swoole常驻内存导致的跨请求数据串号事故、以及用队列把写请求削峰的具体做法,附完整代码、五阶段QPS/TP99量化对比表和实施顺序建议。

栏目:MES高并发架构优化 | 发布日期:2026-08-15 | 环境:ThinkPHP 6.1 / PHP 8.1 / Swoole 5.0 / Redis 6.2 / MySQL 8.0 / CentOS 7.9(8核16G x 3)

🔧 配套工具:本节的核算/判读可用站内工具直接跑,推荐 半导体MES工单管理v2 详解、半导体生产排程优化v2、半导体批次追溯增强版v2(zip 包,含可运行 Python 脚本与示例数据)。
这个方向的工具共 53 款,完整清单与选型建议见 MES与生产管理工具包。全部 351 款见 工具资源包下载页。

一、问题背景:交接班那十分钟,MES被自己人打崩了

先说清楚我们的场景。我所在的是一家做功率半导体后段封测的工厂,MES是自研的,后端用ThinkPHP6,前端是车间的PDA手持机加看板大屏,另外还有一套设备数采程序往MES推数据。平时白天的接口压力并不大,日常QPS只有一百多,跑得很安稳。真正出问题的是交接班时段。我们是三班制,早班八点、中班十六点、夜班零点交接,交接前后十分钟,上一班要把所有未报的产出批量补报完,下一班要拉当班的工单和工艺路线,同时设备数采程序按整点触发批量上传前一小时的机台参数。三股流量叠在同一分钟里。

第一次崩是去年三月的中班交接。表现是PDA点「提交报工」转圈三十秒然后超时,车间主任电话直接打到我手机上。我登上服务器看,nginx错误日志刷满了「connect() to unix:/run/php-fpm.sock failed (11: Resource temporarily unavailable)」,php-fpm的进程全部处于Running状态,MySQL的Threads_connected顶到最大连接数,慢查询日志里全是工艺路线的多表关联查询。更麻烦的后果是数据错了:工人看到转圈没反应就反复点提交,有个工单的完工数量被记成了实际的三倍,后面盘账花了两天才把账对回来。那天最后是车间退回纸质流转卡撑到下班的。

事后复盘,我犯的第一个错是把「平时够用」当成了「架构没问题」。MES这类系统的流量特征跟电商完全不同,它不是平滑的日活曲线,而是被生产节拍和班次强行对齐的尖峰,波峰波谷能差六十倍以上。第二个错是没有基线数据,出事后我甚至说不出「崩之前系统到底能扛多少并发」,只能靠猜。所以那次之后我做的第一件事不是改代码,而是把压测环境搭起来,把基线量出来。我给自己定的目标是:在3000并发下TP99控制在500毫秒以内、错误率低于千分之一,并且要能扛住万级并发的峰值不崩,因为随着两条新线投产,报工终端会从180台涨到接近500台,未来的峰值必然翻倍。

这篇文章就是那之后半年的完整记录。我按时间顺序拆成五个阶段,每个阶段只动一件事,压完测再动下一件,这样才能知道收益到底来自哪。中间有一次因为Swoole常驻内存导致跨工位数据串号的事故,我也会原原本本写出来,因为那个坑我相信很多从fpm迁到Swoole的人都会踩。

二、技术原理:为什么fpm在千级并发就撑不住了

2.1 进程模型的本质差别

php-fpm是「进程池 + 每请求重建执行环境」的模型。一个请求进来,fpm挑一个空闲worker,worker从零开始执行:加载框架入口、注册自动加载、构造容器、读配置文件、注册中间件、解析路由表、初始化数据库连接,最后才跑到我的业务代码。请求结束,整个PHP变量空间销毁,下一个请求重新来一遍。我在基线阶段用xhprof量过,ThinkPHP6在我们这套配置下光引导阶段就要38到45毫秒,业务逻辑本身只有12毫秒。也就是说四分之三的CPU时间花在了重复做同样的准备工作上。

更要命的是并发上限。fpm的pm.max_children是硬上限,我们8核16G配的是64。这意味着同一时刻最多只有64个请求在处理,第65个开始排队。而报工接口里有一次数据库事务写入,事务耗时大约80毫秒,这80毫秒里worker什么都不干,纯粹在等IO返回,但它占着进程不放。算一下就明白了:64个worker除以0.08秒,理论吞吐上限只有800 QPS,而且这是最理想情况。一旦MySQL因为连接暴涨而变慢,事务耗时涨到300毫秒,吞吐立刻掉到213 QPS,队列越积越长,最后就是雪崩。

Swoole的模型不一样。它以常驻进程方式启动,框架只在进程启动时引导一次,之后所有请求复用同一份内存中的容器、路由表和配置,引导那38毫秒从每请求一次变成整个生命周期一次。更关键的是协程:当业务代码执行数据库查询时,协程会主动让出CPU,让同一个worker去处理别的请求,IO返回后再恢复现场继续跑。一个worker可以同时挂着几百个处于IO等待的协程,这就把「进程数决定并发数」变成了「CPU算力决定并发数」,天花板完全不同。

2.2 缓存与队列在这套架构里各自解决什么

很多人把缓存和队列混着谈,其实它们解决的是两类完全不同的问题。缓存解决的是读放大:工艺路线、BOM(物料清单,Bill of Materials)、设备主数据、班次日历这些东西一天可能改一次,但每次报工都要读,读写比大概是5000比1。这类数据放Redis,把一次三表关联的12毫秒查询变成0.4毫秒的内存读取,还顺带把MySQL的负载卸掉。队列解决的是写尖峰:报工写入是刚性的、不能丢的,但它不需要同步完成。工人按下提交,他真正关心的是「系统收到了没有」,而不是「数据库事务提交了没有」。这两件事之间的时间差,就是我们能做削峰的空间。

把写入改成异步之后,接口耗时从80毫秒的事务变成2毫秒的入队,单机吞吐直接提了一个量级。峰值来的时候队列会积压,但积压是可控的:我们监控队列长度,只要消费速率大于平均生产速率,积压就会在峰值过后自然消化。我们实测交接班那十分钟最多积压过两万三千条消息,四个消费进程用了一分四十秒消化完,车间完全无感。

异步化带来的代价是一致性和幂等。接口返回「已受理」但落库还没完成,这段时间里如果有人查这张工单的完工数,会看到旧值。我们的处理办法是分场景:报工提交这种写多读少的走异步,而在制品数量校验这种必须实时的,用Redis里的计数器加Lua脚本原子扣减,在入队之前就把库存判断做完。这样既保住了不超报的业务约束,又不用等数据库。幂等则靠「工单+工序+设备+时间窗」拼出来的键,用SET NX兜住重复提交,这一条直接解决了当初PDA重复点击导致数量翻倍的老问题。

图1:每一阶段只改一件事,压测数据来自同一台压测机、同一份真实报工样本,横轴是优化阶段,柱状为QPS,折线为TP99。阶段3的跳变来自框架不再重复引导。

三、实战案例:五个阶段,一次事故

3.1 阶段一:零成本的配置层优化,从320到780

第一阶段我没写一行业务代码,纯改配置,结果QPS从320涨到780,这也是我建议所有人先做的一步。具体动了五处:开启opcache并把opcache.validate_timestamps设为0(生产环境文件不会变,省掉每次stat文件的开销);执行php think optimize:route和optimize:config生成路由与配置缓存;关闭app_debug和trace;把日志级别从debug提到warning;还有一个容易被忽略的,把日志写入方式从单文件改成按小时切分。最后这一条的效果超出我预期,因为debug模式下每个请求写十几行日志,几十个worker抢同一个文件的写锁,磁盘IO反倒成了瓶颈。

3.2 阶段二:Redis缓存与那次缓存雪崩

第二阶段做热点数据缓存,把工艺路线、BOM展开结果、设备与工位映射、班次日历这四类丢进Redis,QPS到2150。但上线第三天早上八点整,系统又卡了一次,这次持续了大约四十秒。查下来是我自己埋的雷:我给所有缓存都设了统一的3600秒TTL,而这批缓存是上线那一刻同时写进去的,于是它们也在同一秒同时失效。那一秒钟涌进来的几百个请求全部穿透到MySQL,同时执行同一个重查询,MySQL瞬间被打满。这就是典型的缓存雪崩,书上都写过,但真轮到自己身上才知道那种手心出汗的感觉。

修的办法有三层。第一层,TTL加随机抖动,我们用的是基础3600秒加上0到600秒的随机值,把失效时刻打散。第二层,热点键的重建加互斥,第一个发现缓存失效的请求拿到Redis锁去查库回填,其他请求短暂等待后重读缓存,而不是一起冲向数据库。第三层是逻辑过期:我们给缓存值里存一个业务过期时间戳,物理TTL设得更长,读到「逻辑上过期但物理还在」的数据时,先把旧值返回给用户,同时丢一个异步任务去刷新。对工艺路线这种几分钟内不变的数据,返回一个几秒钟前的旧值完全可以接受,但避免了任何一个请求被阻塞。

3.3 阶段三:Swoole常驻内存,以及跨工位数据串号事故

第三阶段上Swoole,QPS一跃到6400,TP99从720毫秒降到310毫秒,这是整个优化过程收益最大的一步。但也是这一步让我在生产上出了一次真正的事故,严重程度比性能问题高得多,因为它是数据正确性问题。

上线第二天中午,质检的同事在群里贴了张截图问我:为什么B区打磨工位的报工记录里,操作员显示的是A区一个封装工位的人?我当时后背就凉了。排查了两个多小时,根因是我们有个UserContext服务类,早年为了省事写成了单例,里面有个$currentUser属性,在中间件里赋值,后面业务代码到处从它取当前用户。在fpm下这么写完全没问题,因为每个请求的内存空间是独立的,请求结束就销毁了。但在Swoole常驻模型下,这个单例的生命周期跟worker进程一样长,两个协程并发跑的时候,协程A刚把用户设成张三,协程B紧接着把它改成李四,协程A从IO等待里恢复过来再去读,读到的就是李四。

这个坑的可怕之处在于它不是必然发生的,并发低的时候一个月都不出一次,并发一高就开始随机串号,测试环境根本复现不出来。我们最后的解决办法有三步:第一步紧急止血,先回滚到fpm跑了两天,同时用SQL把当天所有报工记录跟PDA终端登录日志做交叉核对,捞出17条串号记录人工修正。第二步是治本,把所有请求态数据从单例里挪进Swoole的协程上下文Context,以协程ID为隔离粒度,协程结束自动回收。第三步是防复发,我在CI里加了一条静态检查规则,扫描所有单例类的属性,凡是名字里带user、request、token、session、tenant这类请求态特征的一律拦下来,同时在团队编码规范里写死一条:常驻内存模式下单例只允许存无状态的配置和连接资源。

3.4 阶段四与阶段五:队列削峰和连接池调优

第四阶段把报工、参数上传这些写接口改成入队异步落库,QPS到10200,TP99降到165毫秒。这里踩的坑是消费者内存泄漏:消费进程跑了大概十几个小时后内存从80兆涨到1.4G,最后被系统OOM杀掉。原因是队列消费进程也是常驻的,代码里有个地方用静态数组做了「本地缓存」,每处理一条消息就往里加一个键,永远不清理。我们的处理方式是两条:一是把那个静态数组换成有容量上限的LRU结构,二是给消费进程设置max-jobs参数,每处理一万条消息主动退出让supervisor拉起来,用重启兜住所有我们没发现的泄漏。这个做法不优雅,但在生产上非常实用。

第五阶段是连接池和worker参数调优,QPS到12800。这里最重要的一条经验是:连接池上限必须和数据库的最大连接数联合计算。我们一开始给每个节点的MySQL池设了100,三个节点就是300,再加上定时任务和数采程序的连接,直接把MySQL的max_connections(当时设的500)顶穿了,报出Too many connections。后来改成每节点60,三节点180,给其他系统留出足够余量,反而更稳。worker数量我们最终定在CPU核数的两倍,也就是16,再往上加协程调度本身的开销就开始吃掉收益了,这个值建议每个人在自己机器上压出来。

图2:fpm在1000并发后TP99急剧劣化,因为worker数量固定、每个请求都要重新引导框架;Swoole方案在12000并发时TP99仍然178ms,曲线斜率明显更平。

四、完整代码:报工接口的削峰、幂等与原子校验

下面这段是我们线上报工接口的核心代码,做了脱敏但逻辑完整。它把本文讲的几个关键点串在了一起:用Lua脚本在Redis里一次性完成幂等判断和在制品原子扣减、接口只入队不落库、消费端负责事务写入与失败补偿。这段代码在我们线上单机扛过12800 QPS,你可以直接对照着改自己的接口。

【常见坑】

  • 【摘要】我们厂的MES报工接口在交接班时段被打崩过三次,最惨那天PDA报工全线超时,车间只能退回纸质流转卡。这篇文章记录我从压测基线320 QPS一路优化到12800 QPS的完整过程,包括配置层零成本优化、Redis缓存击穿踩坑、Swoole常驻内存导致的跨请求数据串号事故、…
  • 第一次崩是去年三月的中班交接。表现是PDA点「提交报工」转圈三十秒然后超时,车间主任电话直接打到我手机上。我登上服务器看,nginx错误日志刷满了「connect() to unix:/run/php-fpm.sock failed (11: Resource temporarily unavail…
  • 这篇文章就是那之后半年的完整记录。我按时间顺序拆成五个阶段,每个阶段只动一件事,压完测再动下一件,这样才能知道收益到底来自哪。中间有一次因为Swoole常驻内存导致跨工位数据串号的事故,我也会原原本本写出来,因为那个坑我相信很多从fpm迁到Swoole的人都会踩。
  • 第一阶段我没写一行业务代码,纯改配置,结果QPS从320涨到780,这也是我建议所有人先做的一步。具体动了五处:开启opcache并把opcache.validate_timestamps设为0(生产环境文件不会变,省掉每次stat文件的开销);
  • 这个坑的可怕之处在于它不是必然发生的,并发低的时候一个月都不出一次,并发一高就开始随机串号,测试环境根本复现不出来。我们最后的解决办法有三步:第一步紧急止血,先回滚到fpm跑了两天,同时用SQL把当天所有报工记录跟PDA终端登录日志做交叉核对,捞出17条串号记录人工修正。
  • 第四阶段把报工、参数上传这些写接口改成入队异步落库,QPS到10200,TP99降到165毫秒。这里踩的坑是消费者内存泄漏:消费进程跑了大概十几个小时后内存从80兆涨到1.4G,最后被系统OOM杀掉。

常见问题(FAQ)

Q:数字化转型应该从哪一步开始?

A:从业务诊断开始,量化当前最大损失并定义改善目标,再据此确定系统建设顺序。跳过诊断直接做系统规划,容易得出与实际痛点无关的功能清单。

Q:怎么说服管理层投入数字化?

A:用损失量化的语言而不是技术语言。把「需要上 MES」转化为「当前因信息不透明导致的返工、呆滞与延期每月造成多少损失」。决策者更容易对损失数字而非功能列表作出回应。

Q:数字化项目如何避免变成无底洞?

A:三件事:一是在立项时定义可量化的验收指标与时间点;二是分阶段交付,每阶段都要有独立可用价值;三是建立内部承接能力,减少对单一外部实施方的依赖。

Q:有限的预算应该先投在哪个方向?

A:建议顺序是:先补基础数据治理(这是所有分析的前提),再解决当前损失最大的业务环节,最后才考虑前瞻性的技术验证。反过来做(先上新技术再补数据)通常导致项目无法落地,因为数据基础不支撑应用。

Q:怎么向管理层说明数字化的必要性?

A:用损失和成本的量化语言而非技术语言。把「需要上系统」转化为「当前因信息不透明、响应滞后导致的返工与呆滞每月造成多少损失」,并把预期改善量化。决策者更容易对具体损失与收益数字作出回应。

【总结】

企业数字化与 IT 治理的难点往往不在单点技术,而在于把原理、数据与现场验证串成闭环。下面这段是我们线上报工接口的核心代码,做了脱敏但逻辑完整。它把本文讲的几个关键点串在了一起:用Lua脚本在Redis里一次性完成幂等判断和在制品原子扣减、接口只入队不落库、消费端负责事务写入与失败补偿。这段代码在我们线上单机扛过12800 QPS,你可以直接对照着改自己的接口。规划的起点应是业务损失最大的环节,而不是功能最全的系统。用「当前最大损失发生在哪」来确定建设优先级,比按部门需求排序更有效。建议先用小范围试点验证有效性,再逐步扩大适用范围,并保留完整的数据与判断记录以便复盘。

相关阅读

// app/api/controller/Report.php —— PDA报工接口(削峰入队 + 幂等 + 库存原子校验)namespace app\api\controller;use think\facade\Cache;use think\facade\Queue;class Report 相关阅读SECS-GEM协议:半导体设备的"普通话"从入门到实战工程师副业的边界:竞业限制与合规红线到底在哪里本站为工程师整理了免费资源下载包(30套半导体AI提示词 + 5套SKILL + 10款Python小工具),需要更多进阶工具可看VIP工具下载。

📚 同栏目延伸阅读:FDC故障检测与分类:FAB设备异常的"天眼系统"、SECS-GEM协议:半导体设备的"普通话"从入门到实战、MES系统数据库读写分离:主从复制+分库分表实战踩坑、消息队列在FAB调度:RabbitMQ实现工序排程

📦 本文相关资源:文中方法可直接用站内工具落地,推荐 半导体MES工单管理v2、半导体生产排程优化v2、半导体批次追溯增强版v2、工单准时交付率与延期原因帕累托分析器(zip 包,含可运行 Python 脚本与示例数据)。更多同类工具见 工具资源包下载页(共 351 款)。

相关文章

半导体产业全景:从沙子到芯片的完整产业链

半导体产业全景:从沙子到芯片的完整产业链

【摘要】 本文系统梳理光刻与图形化领域的核心问题与落地路径。大家好,我是老张,在半导体行业摸爬滚打了十五年。全文围绕「芯片到底是什么?、三大商业模式:IDM、Fabless、Foundry、芯片设计...

MES制造执行系统:半导体FAB的信息中枢到底管什么

MES制造执行系统:半导体FAB的信息中枢到底管什么

【摘要】 本文系统梳理MES 制造执行系统领域的核心问题与落地路径。Manufacturing Execution System — 当FAB遇上数字化转型,信息流如何驱动价值流?。全文围绕「问题背...

APC先进过程控制:工艺参数自动调控的秘密武器

APC先进过程控制:工艺参数自动调控的秘密武器

【摘要】 本文系统梳理APC 先进过程控制领域的核心问题与落地路径。我在FAB里做工艺工程师的时候,最头疼的事情就是调参数。全文围绕「问题背景:手动调整的局限性、技术原理:R2R控制与核心算法、实战...

EAP设备自动化:SECS/GEM协议从零到实战

EAP设备自动化:SECS/GEM协议从零到实战

【摘要】 本文系统梳理SECS/GEM 设备通信与 EAP领域的核心问题与落地路径。我在FAB第一次接触EAP的时候,被一堆缩写搞晕了——MES、EAP、EDA、SECS、GEM……傻傻分不清。全文...

存储器技术详解:DRAM/NAND/HBM一篇看懂

存储器技术详解:DRAM/NAND/HBM一篇看懂

从存储单元结构到市场格局:全面拆解三大存储技术的原理与应用 【摘要】 本文系统梳理工业 AI 与机器学习落地领域的核心问题与落地路径。从存储单元结构到市场格局:全面拆解三大存储技术的原理与应用。全文...

FDC故障检测与分类:FAB设备异常的"天眼系统"

FDC故障检测与分类:FAB设备异常的"天眼系统"

【摘要】 本文系统梳理设备管理与预测性维护领域的核心问题与落地路径。我在FAB做整合工程师的时候,有一次刻蚀机出了异常,射频功率悄悄漂移了5%,持续了整整2个小时才发现——因为没有实时监控,…。全文...