2 核 4G 能扛多少用户?一次老 PHP 项目的性能评估与架构优化实录 本文是《接手外包项目后的两件事修复分裂的 Git 历史 搭建 Gitee 推送自动部署》的姊妹篇。上一篇讲怎么把代码送上服务器这一篇讲送上去之后——这台服务器到底能扛多少人以及在不加钱的前提下怎么让它多扛几倍。背景接手一个外包开发的征信查询系统原生 PHP跑在腾讯云 2 核 4G 70G 的机器上。老板问了两个问题这配置同时最多支持多少用户现在的架构是不是落后了要不要重构这两个问题都不能拍脑袋回答。本文记录完整的评估过程用哪些命令采集数据、怎么从数据推算容量、发现了什么问题、以及优化的优先级怎么排。环境CentOS 7.9 PHP 7.4.33 MySQL 5.7.44 宝塔面板89 个 PHP 文件无框架。一、先采集不要猜评估容量的第一原则所有数字都要有出处。下面这套命令能在 5 分钟内把一台 PHP 服务器的底摸清。1. PHP-FPM 进程池配置grep-E^(pm|pm\.|listen|request_terminate)/www/server/php/74/etc/php-fpm.conf关键输出pm dynamic pm.max_children 80 # 最大工作进程数 ← 并发上限 pm.start_servers 5 request_terminate_timeout 100 # 进程强杀超时 ← 埋雷点pm.max_children就是同时能处理的请求数上限。这个数字乘以单进程内存必须小于可用内存。2. 单进程真实内存占用配置里的memory_limit是上限不是实际值。要测实际的ps-orss-Cphp-fpm|awk{s$1; n} END {printf 进程数%d 平均%.1fMB 合计%.1fMB\n, n, s/n/1024, s/1024}进程数16 平均17.8MB 合计285.2MB这是推算内存天花板的唯一可靠依据。网上「PHP 进程约 30MB」的经验值对你的项目未必成立。3. OPcache 是否启用php-m|grep-i-Eopcache|zend如果[Zend Modules]下面是空的说明没启用。这一条我在本次评估中发现了后面详述——它直接决定 23 倍的性能差距。4. 内存与 swapfree-mtotal used free shared buff/cache available Mem: 3630 988 280 1 2361 2349 Swap: 0 0 0重点看两个数available真正可用不是free和Swap。Swap 为 0 意味着内存打满就是 OOM Killer 直接杀进程——通常先杀内存占用最大的 MySQL整站瞬间瘫痪。这个前提会显著改变容量结论。5. 真实流量别问业务方「大概多少用户」直接看日志# 总请求数wc-l/www/wwwlogs/example.com.log# 峰值单分钟最高请求数awk{print $4}/www/wwwlogs/example.com.log|cut-c2-18|sort|uniq-c|sort-rn|head-5# 每小时分布awk{print substr($4,14,2)}/www/wwwlogs/example.com.log|sort|uniq-c|sort-rn|head-3本例结果日请求 73,885 次峰值小时 5,468 次≈1.5 req/suptime显示负载 0.07。先知道现状用了多少再谈能撑多少。很多「性能优化」是在解决不存在的问题。6. 数据库配置与备份ps-orss,comm-Cmysqld|awk{printf mysqld %.0fMB\n, $1/1024}crontab-l|grep-v^#find/www/backup-name*.sql*-mtime-30|head-5MySQL 占 393MB备份任务每天 01:30 执行保留 3 份最新文件是当天的——这项是健康的。顺带一个教训crontab里有备份任务 ≠ 备份真的产出了。一定要find一下实际文件。我在上一篇里踩过「45 字节的空 tar 包」的坑。二、容量推算方法拿到数据后从三个维度分别算天花板取最小值。维度一内存可用内存 总内存 − MySQL − 系统/面板 3630 − 393 − 300 ≈ 2300 MB 内存支持的进程数 2300 / 17.8 ≈ 129 个但这是理想值。峰值内存要留余量生成报告的页面会拼几百 KB 的 HTML单进程内存可能涨到 40MB 以上。按 40MB 算安全进程数 2300 / 40 ≈ 57 个而配置里写的是max_children 80。在无 swap 的机器上这是超配的——真跑满 80 个重请求就是 3.2GB直接 OOM。维度二CPU2 核是真正的硬天花板。吞吐上限 核心数 / 单请求 CPU 耗时未开 OPcache 时每次请求都要重新解析编译所有 PHP 文件。本项目一个页面涉及common.phpclasses/下十几个类估算单请求 CPU 约 100ms2 核 / 0.1s 20 req/s上限 留 25% 余量 → 15 req/s可持续维度三并发模型换算成「同时在线用户」这一步是最多人算错的地方。请求数不等于用户数中间隔着「思考时间」同时在线用户 吞吐(req/s) × 用户平均操作间隔(s)普通用户浏览时大约每 15 秒点击一次15 req/s × 15s 225 人同时在线本例结论场景当前配置开 OPcache 后同时在线用户浏览为主150250 舒适400 明显变慢400600同时发起业务查询4060 并发同左受外部接口限制日请求承载约 130 万次约 350 万次当前实际用量 7.4 万次/天负载 0.07用了不到容量的 10%。三、发现的四个问题问题 1OPcache 没启用影响最大的低级失误PHP 是解释型语言每次请求都要走读取源码 → 词法分析 → 语法分析 → 编译成 opcode → 执行而前四步的结果每次都完全一样。OPcache 把编译结果缓存在共享内存里后续请求直接跳到「执行」。一个请求涉及 20 个 PHP 文件就是省掉 20 次重复的读取和编译。在本项目上大约省掉 6070% 的 CPU 时间。配置php.ini的[opcache]段zend_extensionopcache.so opcache.enable1 opcache.memory_consumption128 opcache.max_accelerated_files10000 opcache.validate_timestamps1 opcache.revalidate_freq2revalidate_freq2表示每 2 秒检查源文件修改时间变了就重新编译。这对配了自动部署的项目很重要——代码推上去 2 秒内自动生效不需要重启 PHP。有种激进配置是validate_timestamps0完全不检查性能再高一点但每次发版必须重启 PHP和自动部署的工作流冲突不建议用。代价128MB 共享内存。而且开了之后 PHP-FPM 进程本身内存占用会下降——多个进程共享同一份 opcode不用各存一份。风险几乎没有。PHP 5.5 之后是官方标配不开才是异常状态。问题 2max_children与内存不匹配max_children 80无 swap可用内存 2.3GB。前面算过重请求场景下会 OOM。改成 40。这不会降低实际容量本例峰值才 16 个进程但保证极端情况下不会被 OOM Killer 杀掉 MySQL。问题 3外部接口串行调用 —— 真正的架构瓶颈这是本次评估最有价值的发现。核心业务代码publicfunctionqueryAll($name,$idCard,$phone){$results[];$apiList[identity_check[...],// 身份二要素认证judicial_litigation[...],// 个人司法涉诉panorama_radar[...],// 全景雷达special_list[...],// 特殊名单验证probe[...],// 探针police_bad_list[...],// 公安不良人员名单court_execution[...],// 法院被执行人rongan_credit[...],// 信用分phone_online[...],// 手机在网时长];foreach($apiListas$key$api){// ← 串行$method$api[method];$result$this-$method($name,$idCard,$phone);// 每个 curl 超时 20s$results[$key][...];}return$results;}一次业务查询要依次调用 8 个第三方接口。串行意味着总耗时是所有接口之和情况单接口耗时总耗时期间占用顺利1s8 秒1 个 PHP-FPM 进程全程阻塞一般23s1624 秒同上上游慢20s 超时理论 160 秒被 FPM 在 100 秒时强杀这些接口之间没有任何依赖关系完全可以并发。问题 4超时配置自相矛盾静默失败max_execution_time 300 # PHP 脚本超时 request_terminate_timeout 100 # FPM 强杀超时FPM 会在 100 秒时KILL进程。此时PHP 的try/catch执行不到进程是被信号杀死的不是抛异常业务记录状态停在「查询中」错误日志里什么都没有用户付了钱看不到结果这类「静默失败」比报错难查十倍。排查时要特别注意这两个值的关系。四、curl_multi 并发改造针对问题 3 的解法。改造前后对比指标串行并发单次查询耗时1624 秒23 秒进程占用时间1624 秒23 秒并发查询能力40 进程≈ 2.5 次/秒≈ 13 次/秒每小时查询量≈ 9,000≈ 48,000同样的 2 核 4G业务吞吐提升 58 倍用户等待从 20 秒降到 3 秒。核心思路是把curl_exec换成curl_multi_exec/** * 并发执行多个 curl 请求 * param array $requests [key [url..., post..., headers...]] * return array [key [body..., error..., http_code...]] */privatefunctionmultiRequest(array$requests,$timeout8){$mhcurl_multi_init();$handles[];foreach($requestsas$key$req){$chcurl_init();curl_setopt($ch,CURLOPT_URL,$req[url]);curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);curl_setopt($ch,CURLOPT_TIMEOUT,$timeout);curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,3);if(!empty($req[post])){curl_setopt($ch,CURLOPT_POST,true);curl_setopt($ch,CURLOPT_POSTFIELDS,$req[post]);}if(!empty($req[headers])){curl_setopt($ch,CURLOPT_HTTPHEADER,$req[headers]);}curl_multi_add_handle($mh,$ch);$handles[$key]$ch;}// 并发执行直到全部完成$runningnull;do{curl_multi_exec($mh,$running);if($running0){curl_multi_select($mh,1.0);// 阻塞等待避免空转烧 CPU}}while($running0);// 收集结果$results[];foreach($handlesas$key$ch){$results[$key][bodycurl_multi_getcontent($ch),errorcurl_error($ch),http_codecurl_getinfo($ch,CURLINFO_HTTP_CODE),];curl_multi_remove_handle($mh,$ch);curl_close($ch);}curl_multi_close($mh);return$results;}改造时的三个注意点curl_multi_select不能省。不加它do-while会疯狂空转把 CPU 打满。加CURLOPT_CONNECTTIMEOUT。连接超时和总超时是两回事只设后者的话连不上的接口会白等到总超时。保持返回结构不变。把multiRequest的原始返回再分发给各接口原有的解析逻辑业务代码零改动风险可控。五、优化优先级优先级改动成本收益1外部接口改curl_multi并发改 1 个文件业务吞吐58 倍2启用 OPcache改配置整站吞吐23 倍3pm.max_children80 → 40改配置防 OOM4单接口超时 20s → 8s改 1 行上游故障不拖垮全站5理顺request_terminate_timeout改配置消除静默失败1 2 叠加同样配置的整体承载能力约提升到原来的 46 倍成本是改一个文件加几行配置。六、不该做的事评估的另一半价值是识别出不值得做的优化。以下几项在当前量级都是负收益❌ 换框架Laravel / ThinkPHP89 个文件、单表读写为主、没有复杂领域模型。框架带来的是每请求额外的路由解析、容器初始化、中间件开销。会变慢不会变快。❌ Docker 化没有构建步骤无 composer、无 npm、单机、一个半开发者。Docker 解决的是环境一致性和水平扩展这两个需求都不存在。徒增运维复杂度。❌ 引入队列做异步化把查询改成「提交后返回处理中后台 worker 调接口完成推送通知」确实能让 PHP-FPM 完全不等外部接口。但需要引入 Redis 常驻 worker、改前端轮询、处理重试和状态机——工作量比上面五项加起来大一个数量级。当前日均 7.4 万请求、负载 0.07远没到门槛。等查询量涨到 20 倍再说。❌ 读写分离 / CDN / 加配置在负载 0.07 的机器上讨论这些没有意义。触发升配的信号应该是峰值load average持续超过 2.0或free -m的available长期低于 500MB或用户反馈变慢。七、值得警惕的其他发现评估过程中还发现两件和性能无关、但优先级更高的事1. 系统组件全部 EOL组件版本停止支持至今CentOS7.92024-06-302 年无安全更新PHP7.4.332022-11-283 年 8 个月无安全补丁MySQL5.7.442023-10-312 年 9 个月无安全补丁考虑到这是个存储姓名、手机号、身份证号的系统跑在两年没打过安全补丁的机器上性质和普通展示站不同。升级的难点不在系统在于老代码对 PHP 8 的兼容性$var{0}字符串下标、each()、隐式类型转换等都会报错。务实做法新开一台机器搭好新环境代码跑通测试后再切域名别在生产机上原地升级。2. 备份只存在本机数据库每天备份、保留 3 份、文件确实在产出——这些都健康。但全部存在同一台服务器上。整机故障、误删、被入侵加密备份跟着一起没。改成推送到对象存储COS/OSS十几分钟的事收益比任何性能优化都大。总结评估方法论抛开这个具体项目一套可复用的评估流程先采集不要猜。ps测真实内存、日志算真实流量、php -m验扩展。所有数字都要有出处。三个维度取最小值内存天花板、CPU 天花板、并发模型。注意隐含前提。无 swap 会让「内存打满」从「变慢」变成「进程被杀」结论完全不同。区分「请求数」和「用户数」中间隔着思考时间。找阻塞点而不是找慢代码。本例真正的瓶颈不是任何一行 PHP 慢而是 8 个外部接口串行——一个纯粹的架构问题用curl_multi就能解决。明确列出不该做的事。识别出「不值得优化」和找出「值得优化」同样重要。最后一点最容易被忽略先看现在用了多少再谈能撑多少。一台负载 0.07 的机器讨论要不要上 K8s 是没有意义的。文中所有 IP、域名、密钥、路径均已替换为占位符。