实时特征更新管道

解读

在国内互联网高并发业务(电商大促、金融风控、短视频推荐)场景下,特征往往需要在秒级甚至亚秒级完成“产生→计算→存储→Serving”闭环。PHP 作为 Web 层主力语言,常被要求:

  1. 接收上游实时事件(MQ、Kafka、RocketMQ);
  2. 调用 C++/Go/Python 的特征计算服务,拿到结果;
  3. 把特征写回高速存储(Redis、Tair、Aerospike);
  4. 保证 Exactly-Once 或 At-Least-Once,且对业务接口 P99 延迟 < 50 ms。

面试官想考察的是:PHP 能否扛住实时流,如何与周边组件配合,以及你在“脚本语言”先天短板下怎样兜底稳定性、性能和一致性。

知识点

  1. PHP 常驻进程方案:Swoole、OpenSwoole、Swow、WorkerMan,协程、Channel、Timer、Table 内存表。
  2. 异步 Kafka 客户端:php-rdkafka(基于 librdkafka)配置参数 queue.buffering.max.msenable.idempotence
  3. 连接池与熔断:Swoole\ConnectionPool、Hyperf/Guzzle-SocketPool、限流算法(令牌桶、漏桶)。
  4. 特征存储选型:Redis Hash + Lua 脚本原子写、Redis Module (RedisBloom/RedisTimeSeries)、TairRoaring。
  5. 一致性策略:幂等键(eventId + 业务主键)、版本号乐观锁、Lua 脚本保证“读-改-写”原子。
  6. 监控与降级:Prometheus + Grafana 埋点(消费延迟 lag、写入失败率)、Sentry 实时异常、阿里 SLS 日志链路。
  7. 内存与 GC:Swoole Table 大小固定、环形缓冲区避免 C 段溢出;OPcache 保存字节码,减少重编译。
  8. 灾备:跨机房双写、RocketMQ 延迟消息重试、Redis 哨兵 + Cluster 主从漂移。

答案

整体架构采用“PHP 常驻进程 + 异步消息 + 内存缓存 + 兜底批补”三层模型,具体实现分五步:

  1. 启动 Swoole 多进程 Consumer

    • 进程数 = Kafka 分区数,保证一个分区只被一个进程消费,天然避免重复。
    • 配置 max_request = 0 防止业务代码内存泄漏后自动重启导致消费位点跳变。
    • onWorkerStart 阶段初始化: ‑ 每个进程独享一个 php-rdkafka 高阶消费者(HighLevel Consumer),开启 enable.auto.offset.store=false,手动提交。 ‑ 创建 Redis 连接池(大小 8),提前建立 TCP 连接,避免高峰建连耗时。
  2. 事件解析与特征计算

    • 收到消息后先解析为统一 Event DTO,做字段校验与版本号检查;若 eventId 已存在(Redis SETNX),直接 ACK 返回,实现幂等。
    • 对耗时 > 5 ms 的复杂特征(如图模型 Embedding),通过 Swoole\Coroutine\Http\Client 调用内部 Go 微服务,设置超时 20 ms;失败立即记录失败表,走离线修补链路,不阻塞主流程。
  3. 原子写特征缓存

    • 使用 Redis Lua 脚本一次性完成“HGET 旧值→计算增量→HSET 新值→EXPIRE”三步,保证并发安全;脚本 SHA1 提前 LOAD,减少网络往返。
    • 对高并发计数特征(如实时 UV),采用 TairRoaring 的 TR.SETBIT 指令,内存节省 10 倍,且支持合并、去重。
  4. 位点与异常管理

    • 消费成功后,先 Redis 流水落地(Hash 结构,key=eventId,field=workerId),再提交 Kafka offset;进程重启时根据 Redis 未提交事件表做补 ACK,防止重复消费。
    • 进程内设置 SIGTERM 优雅退出:先 rd_kafka->close() 停止拉取,再循环 EventLoop 直到待处理消息数为 0,最后 exit(0),实现零丢数。
  5. 监控与回退

    • 每 10 s 上报一次 Prometheus:消费速率、平均延迟、Redis 写入失败次数;当 lag > 30 s 或失败率 > 1% 时触发钉钉告警,并自动降级:把实时特征计算任务转交给 Flink 流任务,PHP 只负责转发事件,保障核心接口可用。

通过以上设计,PHP 端在 4 核 8 G 容器内可稳定消费 3 万 QPS 消息,特征更新到 Redis 的 P99 延迟 18 ms,全年线上无数据丢失事故。

拓展思考

  1. 如果业务需要“多版本特征回滚”,你会如何在 PHP 层设计版本快照?是否考虑 Redis Stream 或 HBase TTL?
  2. 当特征计算逻辑需要热更新(比如模型换版),PHP 常驻进程如何做到毫秒级灰度切换而不重启 Consumer?
  3. 面对 Redis 单线程瓶颈,特征写入 QPS 再高时,你是否愿意把 PHP 仅作为代理,转而用 Kafka → Flink → Redis 的纯流计算链路?此时 PHP 的角色和价值如何重新定位?