批量推理优化
解读
在国内互联网业务里,“批量推理”通常指一次 HTTP 请求内要对成百上千条数据做规则判断、评分、风控或推荐计算。PHP 作为无常驻内存的同步脚本语言,最怕“一条一条”地在循环里跑 SQL、调 Redis、拼数组,结果 CPU 空转、FPM 进程被占满,超时 502。面试官问“批量推理优化”,核心是想看候选人有没有“降次数、升密度、保结果”的工程化思维:能否把 O(n) 次 IO 压成 O(1),能否用内存级批处理把耗时从 3 s 降到 300 ms,能否在代码层面兼顾可读性与可维护性。回答时要先给场景(如订单风控、优惠券匹配、内容审核),再给出量化指标(QPS、耗时、内存),最后落到具体做法,避免只背八股文。
知识点
- 批量 IO:SQL 的 IN + 索引覆盖、Redis pipeline、Memcached getMulti
- 批量计算:预加载字典 → 内存哈希 → 位运算/生成器/向量化函数
- PHP 运行时:opcache.preload、生成器 yield、SplFixedArray、pack/unpack 二进制
- 并发:curl_multi_*、Swoole\Runtime::enableCoroutine、ReactPHP 批量请求下游
- 内存与 GC:unset 及时释放、内存阈值报警、php.ini memory_limit 动态调大
- 结果一致性:批量加锁(Redis SET NX)、事务消息、对账任务补偿
- 监控:埋点 TRACE、Slow log、Prometheus + Grafana 看 P99
- 代码组织:策略模式 + 工厂 + 闭包,单元测试用 PHPUnit 数据提供者模拟 10 k 数据
答案
【场景】电商大促“下单前优惠券匹配”接口,一次请求带 200 个 SKU+用户标签,系统要在 200 ms 内返回每个 SKU 可用券列表,峰值 QPS 3 k。
【目标】把 200 次 Redis 查询和 200 次 MySQL 查询压到个位数,CPU 占用降 60%,P99 latency < 180 ms。
-
数据预聚合
- 凌晨脚本把“券模板规则”打成哈希表,按 SKU-ID 做 key,序列化后写入 Redis Hash;过期时间 6 h,失效时触发异步刷新。
- 用户维度券包单独存 Redis String(bit 位或 JSON),减少网络往返。
-
批量 IO
- 一次 pipeline 拉 200 个 SKU 规则:
$redis->pipeline(function($pipe) use ($skuIds, &$ruleMap) { foreach ($skuIds as $id) $pipe->hGetAll("sku:coupon:rule:$id"); }); - 用户券包用
mget($userKeys)一次拿回来,避免 200 次 RTT。
- 一次 pipeline 拉 200 个 SKU 规则:
-
内存级批处理
- 预加载规则到 SplFixedArray,用生成器 yield 逐条校验,内存占用恒定 8 MB。
- 位运算判断“互斥标签”:
if (($ruleTag & $userTag) === $ruleTag) { ... } - 结果写入 PHP 8 的
array并一次性json_encode,减少多次拼接。
-
热点隔离
- 本地 OPcache 预加载
CouponMatcher.php,常驻 30 个函数。 - 把 200 条数据拆 4 组,用
curl_multi_*调内部评分服务,并发 4 路,超时 80 ms,失败自动降级用缓存分。
- 本地 OPcache 预加载
-
限流与降级
- 令牌桶限流:Lua 脚本保证原子性,超限直接返回“系统繁忙”。
- 结果缓存:以
userId+md5(skuIds)为 key,有效期 5 s,防止用户疯狂 F5。
-
上线效果
- 压测 5 k QPS,P99 从 1.2 s 降到 160 ms,FPM 活跃进程从 240 降到 90,CPU idle 回升 35%。
- 监控看板:Slow log 清零,Redis 平均耗时 < 2 ms,MySQL 无慢查询。
拓展思考
- 如果规则动态变更且不能容忍 6 h 延迟,可引入版本号机制:Redis 存“规则版本”,PHP 每次拿版本对比,增量拉取差异,实现秒级热更新。
- 当 SKU 数量涨到 2 k,内存成为瓶颈,可把规则压缩为位图或 protobuf,再用 PHP 的
pack/unpack做位级解析,内存降 80%。 - 对实时性要求更高的场景(如金融风控),可让 PHP 只做编排,把重计算下沉到 Go/Rust 写的 gRPC 服务,PHP 用 Swoole 协程并发调 16 路,整体 latency 可再降一半。
- 批量推理结果需回写 MySQL 时,用“INSERT ... ON DUPLICATE KEY UPDATE” 批量拼接,一次写 500 条,避免 200 条单行写入带来的索引抖动。
- 最后务必做好灰度:按用户尾号 1% → 10% → 100% 放量,同时对比新老链路 P99、错误率、GC 次数,确保优化不引入新抖动。