前言必须先给php.ini 性能优化划一条边界它优化的是 PHP 自身的固定开销也就是每个请求都要重复付的那部分成本——比如把源码重新解析编译一遍、把相对路径重新解析成绝对路径、为每个请求重新建一次数据库连接。它完全管不了算法复杂度、SQL 有没有走索引、有没有 N1 查询。真实系统里慢的主因多半在后者而很多人一上来就把 ini 从头改到尾改完发现曲线一点没动。另一个更危险的误解是把网上的性能优化大全抄进 php.ini 就完事。这类参数大多与业务规模强相关memory_limit抄大了一个失控请求就能把整机内存吃光validate_timestamps抄成 0 而部署脚本没跟着改结果是代码改了几天都不上线save_comments抄成 0依赖注解反射的框架直接报错。同一份配置在不同项目上完全可能一个有效、一个致命。本文按OPcache → realpath 缓存 → JIT 与 preload → 请求层与进程层参数四块讲每块先说清原理再说参数最后给一段可以自己跑的测量脚本。所有数字类结论都只讲机制不做速度承诺配置默认值可能随版本演进请以你所用版本对应的官方手册为准。一、OPcache省掉每次请求重新编译PHP 执行脚本的流程是读文件 → 词法分析 → 语法分析 → 编译成 opcode → 由虚拟机执行。前四步的结果只跟源码有关跟本次请求无关但默认情况下每个请求都要重做一遍。OPcache 做的事就是把编译得到的 opcode 存进共享内存后续请求直接取来执行跳过前面的阶段。[opcache]opcache.enable1; CLI 默认关闭。如果 CLI 要跑大量短脚本打开它能省掉重复编译opcache.enable_cli0; 共享内存池大小单位 MB放不下就要重启缓存、重新编译opcache.memory_consumption128; 驻留在共享内存里的字符串类名、方法名、字面量缓冲区单位 MBopcache.interned_strings_buffer16; 可缓存的脚本数量上限应大于项目里 PHP 文件总数opcache.max_accelerated_files20000; 是否检查文件修改时间。生产环境通常设为 0opcache.validate_timestamps0; 仅在 validate_timestamps1 时有意义单位秒opcache.revalidate_freq2; 保留注释。依赖 docblock 注解反射的框架必须为 1opcache.save_comments1四个必须理解的取舍memory_consumption与max_accelerated_files不够用会怎样。前者写满会让缓存整体重启oom_restarts计数增长后者不够会造成哈希表重启hash_restarts增长。两者都意味着缓存反复被清空、反复重新编译收益被抵消。判断方法在最后一节的测量脚本里。validate_timestamps0是最大的双刃剑。设成 0 之后 PHP 不再 stat 每个脚本文件省掉大量文件系统调用代价是改了代码也不会自动生效必须reload或restartphp-fpm。部署流程里如果没有这一步会出现代码明明提交了线上却没变的灵异现象。save_comments不要轻易设 0。PHP 8 的 Attribute 以语法结构存在不受影响但大量成熟框架与 ORM 仍用 docblock 注解关掉之后反射读不到注释会以实体映射缺失这类面目全非的错误表现出来。它省下的内存通常远不及排查成本。CLI 与 FPM 的 OPcache 是两套。它们是不同进程不共享内存。调 ini 时别只看 CLI 的输出要确认改的是 FPM 那一份。二、realpath 缓存省掉重复的路径解析include、require、file_exists()这些操作里如果给的是相对路径或者带..的路径PHP 必须把它解析成绝对路径才能查文件。这个解析要逐级查找目录、发stat系统调用。一个用 Composer 的项目自动加载器每请求可能要探测几十上百个候选路径重复开销可观。realpath 缓存把路径 → 真实绝对路径的映射存在内存里避免重复解析; 缓存区大小默认 4M手册里写作 4096krealpath_cache_size4M; 缓存条目有效期单位秒默认 120realpath_cache_ttl120要点项目文件数多、自动加载路径深时把realpath_cache_size调大是有意义的调太小会一直命不中等于白设。realpath_cache_ttl是缓存有效期。对于部署过程中文件会被替换而不是原地修改的场景这个值决定了多久之后路径映射会被刷新。设得太长符号链接切换类部署可能出现短暂的路径错乱。可以用realpath_cache_size()看当前占用的字节数用realpath_cache_get()看缓存了哪些条目用来判断设的值够不够。这条优化的前提是路径确实被重复解析。如果你的代码里本来就用__DIR__拼绝对路径收益自然就小——这也说明先测量再调参比抄配置更靠谱。三、JIT 与 preloadJITJust-In-Time 编译在 PHP 8.0 引入由 OPcache 提供。它把热点 opcode 进一步编译成机器码减少虚拟机解释执行的开销。opcache.jittracing; 默认值为 0即缓冲区为 0 时 JIT 实际不生效opcache.jit_buffer_size64M两个前提opcache.enable必须为 1jit_buffer_size必须大于 0否则 JIT 只是配了个模式但不工作。JIT 的收益高度依赖代码形态——CPU 密集型的计算受益相对明显而典型 Web 请求大量时间花在等待数据库和网络 I/O 上opcode 解释本身占比有限。所以不要指望打开 JIT 就整体变快正确做法是先用性能分析工具确认瓶颈在 PHP 计算上再考虑它。注意开启 JIT 后进程占用的内存会增加pm.max_children的估算要跟着调整。preload预加载在 PHP 7.4 引入。它允许在 php-fpm 启动时就把指定文件里的类/函数编译并常驻内存此后所有请求都直接用不再需要运行时加载。opcache.preload/var/www/html/preload.php; 以 root 运行 fpm 主进程时必须指定执行预加载的用户opcache.preload_userwww-data两个坑预加载的文件一旦改动必须重启 php-fpm不是 reload 配置就行。因此预加载内容要选稳定的、不常改的框架核心类别把业务代码都塞进去。预加载文件里出错会导致 fpm 直接启动失败。它是在启动阶段执行的普通 PHP 文件路径写错、类不存在都会让整个服务起不来。上线前先在测试环境验证。四、请求层参数、进程参数与实测方法这一层参数不影响跑得多快影响的是跑不跑得完和会不会互相挤死。; 单请求可用内存。按业务峰值观察后设定不要无脑填大memory_limit256M; 单请求最长执行时间秒。CLI 下默认为 0即不限制max_execution_time30; 单次请求最多接受多少个输入变量超出部分被丢弃max_input_vars1000; 请求体上限必须大于等于 upload_max_filesizepost_max_size16M; 单个上传文件上限upload_max_filesize12M; 生产环境不显示错误只记录display_errorsOfflog_errorsOnerror_log/var/log/php/error.log几个必须知道的语义max_execution_time只统计 PHP 自身执行的时间。它不包含数据库查询、网络请求、sleep()等脚本之外的等待时间。所以我设了 30 秒脚本却跑了五分钟不是 bug是设计如此。真正要限制整体时长得用 fpm 侧的request_terminate_timeout它在到点后直接杀掉 worker 进程。max_input_vars超限是静默截断。表单字段超过这个数量多出来的部分不会进$_POST。表现是提交了一大堆数据后面几项莫名丢了排查时很难想到是 ini 参数。批量编辑类功能要特别注意。请求体超过post_max_size时$_POST和$_FILES会一起变空并且不会抛出你能捕获的异常。表现是什么都没提交上来。所以post_max_size一定要大于upload_max_filesize否则上传大文件时连表单里的其它字段都收不到。memory_limit不是越大越好。设得太小会在正常业务上触发 Allowed memory size exhausted设得太大一个死循环或一次超大结果集就能把整台机器的内存拖垮。合理做法是先观测正常请求的峰值占用再留出余量。进程层参数属于 php-fpmWindows 官方包不提供 php-fpm不在php.ini里[www]pm dynamicpm.max_children 16pm.start_servers 4pm.min_spare_servers 2pm.max_spare_servers 6; 子进程处理多少个请求后主动重启用于兜住缓慢的内存增长pm.max_requests 500; 超过这个时长就杀掉 worker防止单个请求把进程池占满request_terminate_timeout 60s; 慢请求日志把超过阈值的请求的 PHP 调用栈写下来slowlog /var/log/php-fpm/slow.logrequest_slowlog_timeout 5s其中request_slowlog_timeout配合slowlog是性价比极高的一项它不猜、不采样直接把这次慢请求卡在哪个函数调用上写进文件。定位偶尔变慢的问题时比盯着top有用得多。pm.max_children的估算原则是单进程常驻内存 × 子进程数不能超过可用物理内存否则一旦并发上来就会触发交换全站一起变慢。单进程常驻内存要用实际运行中的进程来量比如在服务器上按进程名查看 RSS 列取一个稳定值做基准再结合可用内存反推能开多少个子进程。用一段脚本做实测下面这段代码不依赖任何扩展hrtime()需要 PHP 7.3 及以上更低版本用microtime(true)取秒?php// 适用于 PHP 7.3hrtimePHP 8.0 可直接运行declare(strict_types1);function measure(callable $fn, int $times 1000): float{$start hrtime(true); // 纳秒for ($i 0; $i $times; $i) {$fn();}return (hrtime(true) - $start) / 1e6 / $times; // 平均毫秒}// 示例观察数组拼接与字符串拼接的差异替换成你自己的可疑代码$n 10000;$ms measure(static function () use ($n): void {$parts [];for ($i 0; $i $n; $i) {$parts[] (string) $i;}$s implode(,, $parts);}, 50);printf(单次平均耗时: %.3f ms\n, $ms);// 观察 OPcache 的实际工作状况$status opcache_get_status(false); // false 表示不返回脚本明细避免输出过大if ($status false) {echo OPcache 未启用或未加载\n;} else {printf(缓存脚本数 : %d\n, $status[opcache_statistics][num_cached_scripts]);printf(命中率 : %.2f%%\n, $status[opcache_statistics][hit_rate]);printf(哈希表重启 : %d 次max_accelerated_files 可能偏小\n,$status[opcache_statistics][hash_restarts]);printf(内存耗尽重启: %d 次memory_consumption 可能偏小\n,$status[opcache_statistics][oom_restarts]);printf(共享内存已用: %d 字节剩余: %d 字节\n,$status[memory_usage][used], $status[memory_usage][free]);printf(缓存已满 : %s\n, $status[cache_full] ? 是 : 否);}这段脚本的用法是在实际承载流量的那个 SAPI也就是网页环境里访问一次看hash_restarts、oom_restarts、cache_full三个指标。两个重启计数长期为 0、cache_full为否说明 OPcache 的容量配置是够的如果它们持续增长说明缓存一直被清空重建此时调大对应参数才是有效的优化否则改多少都白改。常见坑点❌ 把opcache.validate_timestamps设成 0 提升性能改了代码却发现线上没变✅ 设为 0 后 PHP 不再检查文件修改时间必须重启或 reload php-fpm才会加载新代码。部署脚本里要显式带上这一步。❌ 只调大opcache.memory_consumption却不管max_accelerated_files✅ 两者管的是不同的上限前者是内存池后者是能缓存的脚本条数。项目文件多时后者不够会造成哈希表重启缓存反复被清。两个都要按项目规模设。❌ 为了让框架跑快一点把opcache.save_comments设为 0✅ 依赖 docblock 注解很多 ORM、路由、序列化库都在用的代码会直接失效报错信息还很难指向根因。除非确认项目完全不依赖注解反射否则保持为 1。❌ 在代码里ini_set(memory_limit, ...)却发现没效果✅memory_limit属于可在运行时修改的指令ini_set是有效的但post_max_size、upload_max_filesize属于 per-directory 级别ini_set调用不报错但不生效必须改 ini 或 fpm 配置。❌ 脚本明明设了max_execution_time 30一个慢查询却让它跑了十分钟✅ 该指令只统计 PHP 自身的执行时间数据库查询、网络等待、sleep()都不计入。要限制整体时长得用 fpm 的request_terminate_timeout。❌ 表单字段一多就丢数据查了半天是前端问题✅ 先怀疑max_input_vars默认 1000。超出部分会被静默丢弃不抛异常。批量提交场景要按需调大。❌ 上传大文件失败同时连表单里的其它字段也收不到于是去查表单代码✅ 请求体超过post_max_size时$_POST与$_FILES会同时变空。确保post_max_size大于upload_max_filesize并让前端也做体积校验。❌ 把memory_limit直接设成-1不限制来解决内存报错✅ 这只是把错误从请求失败推迟成整机内存耗尽。应该定位是什么操作产生了超大数组或超大结果集从数据量上解决memory_limit只作为兜底存在。总结优化方向主要指令机制主要代价跳过重复编译opcache.enable、memory_consumption、max_accelerated_filesopcode 常驻共享内存占用内存参数不足会反复重启缓存减少文件系统调用opcache.validate_timestamps不再 stat 源文件改代码后必须重启 fpm减少路径解析realpath_cache_size、realpath_cache_ttl缓存路径到绝对路径的映射占用内存与文件替换类部署有交互减少解释开销opcache.jit、jit_buffer_size热点 opcode 编译为机器码内存增加收益依赖代码形态提前加载opcache.preload、preload_user类在 fpm 启动时常驻变更需重启写错会导致服务起不来请求与进程限制memory_limit、max_input_vars、post_max_size、pm.max_children资源上限与进程池管理设太小会功能性失败php.ini 调优的正确姿势是先看指标再动参数用 OPcache 的状态函数确认缓存是否被反复清空用慢请求日志确认时间花在哪个函数上用实际进程的常驻内存反推进程池大小。凡是拿不出指标支撑的参数改动都只是心理安慰——而当瓶颈本来就在 SQL 或外部接口上时再怎么调 ini 也不会有变化。