批量推理优化

解读

在国内互联网业务里,“批量推理”通常指一次 HTTP 请求内要对成百上千条数据做规则判断、评分、风控或推荐计算。PHP 作为无常驻内存的同步脚本语言,最怕“一条一条”地在循环里跑 SQL、调 Redis、拼数组,结果 CPU 空转、FPM 进程被占满,超时 502。面试官问“批量推理优化”,核心是想看候选人有没有“降次数、升密度、保结果”的工程化思维:能否把 O(n) 次 IO 压成 O(1),能否用内存级批处理把耗时从 3 s 降到 300 ms,能否在代码层面兼顾可读性与可维护性。回答时要先给场景(如订单风控、优惠券匹配、内容审核),再给出量化指标(QPS、耗时、内存),最后落到具体做法,避免只背八股文。

知识点

  1. 批量 IO:SQL 的 IN + 索引覆盖、Redis pipeline、Memcached getMulti
  2. 批量计算:预加载字典 → 内存哈希 → 位运算/生成器/向量化函数
  3. PHP 运行时:opcache.preload、生成器 yield、SplFixedArray、pack/unpack 二进制
  4. 并发:curl_multi_*、Swoole\Runtime::enableCoroutine、ReactPHP 批量请求下游
  5. 内存与 GC:unset 及时释放、内存阈值报警、php.ini memory_limit 动态调大
  6. 结果一致性:批量加锁(Redis SET NX)、事务消息、对账任务补偿
  7. 监控:埋点 TRACE、Slow log、Prometheus + Grafana 看 P99
  8. 代码组织:策略模式 + 工厂 + 闭包,单元测试用 PHPUnit 数据提供者模拟 10 k 数据

答案

【场景】电商大促“下单前优惠券匹配”接口,一次请求带 200 个 SKU+用户标签,系统要在 200 ms 内返回每个 SKU 可用券列表,峰值 QPS 3 k。
【目标】把 200 次 Redis 查询和 200 次 MySQL 查询压到个位数,CPU 占用降 60%,P99 latency < 180 ms。

  1. 数据预聚合

    • 凌晨脚本把“券模板规则”打成哈希表,按 SKU-ID 做 key,序列化后写入 Redis Hash;过期时间 6 h,失效时触发异步刷新。
    • 用户维度券包单独存 Redis String(bit 位或 JSON),减少网络往返。
  2. 批量 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。
  3. 内存级批处理

    • 预加载规则到 SplFixedArray,用生成器 yield 逐条校验,内存占用恒定 8 MB。
    • 位运算判断“互斥标签”:
      if (($ruleTag & $userTag) === $ruleTag) { ... }
    • 结果写入 PHP 8 的 array 并一次性 json_encode,减少多次拼接。
  4. 热点隔离

    • 本地 OPcache 预加载 CouponMatcher.php,常驻 30 个函数。
    • 把 200 条数据拆 4 组,用 curl_multi_* 调内部评分服务,并发 4 路,超时 80 ms,失败自动降级用缓存分。
  5. 限流与降级

    • 令牌桶限流:Lua 脚本保证原子性,超限直接返回“系统繁忙”。
    • 结果缓存:以 userId+md5(skuIds) 为 key,有效期 5 s,防止用户疯狂 F5。
  6. 上线效果

    • 压测 5 k QPS,P99 从 1.2 s 降到 160 ms,FPM 活跃进程从 240 降到 90,CPU idle 回升 35%。
    • 监控看板:Slow log 清零,Redis 平均耗时 < 2 ms,MySQL 无慢查询。

拓展思考

  1. 如果规则动态变更且不能容忍 6 h 延迟,可引入版本号机制:Redis 存“规则版本”,PHP 每次拿版本对比,增量拉取差异,实现秒级热更新。
  2. 当 SKU 数量涨到 2 k,内存成为瓶颈,可把规则压缩为位图或 protobuf,再用 PHP 的 pack/unpack 做位级解析,内存降 80%。
  3. 对实时性要求更高的场景(如金融风控),可让 PHP 只做编排,把重计算下沉到 Go/Rust 写的 gRPC 服务,PHP 用 Swoole 协程并发调 16 路,整体 latency 可再降一半。
  4. 批量推理结果需回写 MySQL 时,用“INSERT ... ON DUPLICATE KEY UPDATE” 批量拼接,一次写 500 条,避免 200 条单行写入带来的索引抖动。
  5. 最后务必做好灰度:按用户尾号 1% → 10% → 100% 放量,同时对比新老链路 P99、错误率、GC 次数,确保优化不引入新抖动。