快手 AB 场景提速 145 倍,从 Spark 到 Apache Doris 的加速实践
导读快手 AB 指标生产场景从 Spark 切换到 Doris 提速 145 倍、资源消耗下降 72%并刷新了 Doris 单机群最大规模2000 节点、10 万核。本文介绍快手数据平台团队为什么选择 Apache Doris 构建 AB 实验平台针对 AB 指标生产场景进行了哪些落地过程中的存储、计算、调度、稳定性等方面优化。快手的干货分享对需要构建 AB 实验平台的团队有很大的参考价值。本文整理自快手数据平台部数据引擎技术中心 曾斯维 在 Apache Doris × 快手北京 Meetup 上的分享。AB 指标计算链路的性能与成本压力快手 AB 实验平台是公司级指标计算底座服务于全公司业务线。任何策略上线前都需要先在平台中计算实验报告确认实验结果具备正向收益后才能继续推进上线。因此这条链路直接支撑公司级业务决策。在 Spark 时代AB 指标计算主要面临两类问题计算慢。以核心指标模板为例单链路计算耗时约21 分钟。业务同学下午想看的实验结论往往第二天才能拿到决策被迫延后。成本高。在 100% 流量推全场景下计算成本压力已经较大而实验数量仍在持续增长计算成本会随规模线性上升。基于性能和成本考虑团队将生产链路从 Spark 迁移到 Doris。目前快手 Doris AB 集群规模包括5 个 FE 节点、2000 个 BE 节点、10 万 CU、数百 TB 内存池并划分为 12 个逻辑计算组。近 14 天审计中集群承载了数百万个任务日均任务量达到几十万级扫描数据量达到40TB、千亿级行。在这一规模下任何低效算子都会被放大因此必须从底层执行、存储分布、计算算子和调度治理等多个层面系统优化。为什么 Doris 比 Spark 更适合该场景Doris 相比 Spark 的优势主要来自三方面执行模型差异。第一Pipeline 执行引擎减少线程等待。Spark 在两个 Stage 之间进行 Shuffle 时数据通常需要落盘下游必须等待上游全部完成后才能继续执行线程存在阻塞。Doris 采用 Pipeline 模型在等待数据时可以让出线程去执行其他任务减少 CPU 空转。第二向量化执行提升批处理效率。Spark SQL 内部使用行式 Internal Row而 Doris 是全链路列式 Block每 4096 行作为一批进行处理。列式布局更适合 CPU SIMD 批量处理函数调用也可以从每行一次摊薄到每批一次。第三C 运行时降低 JVM 相关开销。Spark 运行在 JVM 上存在 GC Stop-The-World 和 JIT 开销。Doris 使用 C 编译后的机器码执行没有 JVM GC启动后即可进入较高执行效率。不过这些只是通用能力。对于快手 AB 指标计算而言更大的收益来自业务计算模型与 Doris 执行机制之间的匹配。正因为 AB 指标计算模板长期稳定团队才有机会围绕执行链路进行更深入的专项优化最终达到 145x 性能提升。关键优化从减少数据流动到压缩计算热路径完成引擎迁移后团队没有只停留在替换执行引擎层面而是进一步围绕 AB 指标计算模板的特点对数据分布、执行计划、计算热点、资源调度和元数据治理进行了针对性的系统优化。最终多项优化共同作用释放了 Doris 在该场景下的性能潜力。AB 指标计算链路的结构相对稳定输入通常固定为两类数据一类是累计分流表用于记录用户命中了哪个实验、哪个分组另一类是指标宽表用于记录用户行为指标。输出也相对固定通常按照实验、分组、桶等维度进行聚合结果集一般只有百行到万行。完整计算过程主要包括四步扫描累计分流表按日期和实验名过滤命中信息扫描指标宽表按 UID 进行预聚合并处理指标口径两张表按 UID Join并做桶粒度聚合将桶粒度结果上卷写入实验结果表。其中第二步和第三步是优化主战场因为这里同时涉及 Join、分组聚合、网络传输和 CPU 计算开销。这个场景最关键的特点是Join Key 长期固定为 UID聚合维度也相对稳定。换句话说这不是一个随机查询模式高度复杂的 OLAP 场景而是一个计算模板长期稳定、执行路径高度一致的生产计算场景因此具备持续专项优化的条件。存储优化用 Colocate Join 消除跨节点 Shuffle第一项核心优化是Colocate Join。它的基本思路是在数据写入阶段就按照UID对数据进行哈希分桶使相同 UID 的数据始终落到同一台机器、同一个分桶内。这样做的好处是查询时分流表和行为表可以直接在本地完成 Join不需要跨节点 Shuffle 或搬运大批量数据。每个节点只需处理本地分桶内的数据并完成本地聚合最后再汇总少量桶级结果。从生产数据看单个 Bucket 扫描千万级数据后本地 Join 可以先过滤掉约 95% 的无效数据只保留几十万行匹配结果最终输出几千行。也就是说大部分数据在本地节点内就被消化掉了网络中只传输 Join 和聚合后的少量结果集。落地 Colocate Join 时需要重点保证两点建表时必须按照 Join Key也就是 UID进行哈希分桶参与 Join 的两张表分桶数必须严格一致不能有任何偏差。表结构调整完成后还需要通过EXPLAIN检查执行计划。只有执行计划中出现Colocated才说明该优化真正生效。如果计划中仍然存在Shuffle通常意味着两张表的分桶数、分布键或 Colocate 配置没有对齐需要继续排查。计算优化降低去重聚合与 UDF 热路径开销Local Distinct Grouping Sets减少全局 ShuffleColocate 优化已经消除了 Join 带来的跨节点 Shuffle但在计算层中去重算子仍然可能引入新的全局 Shuffle 开销。在 AB 指标计算中SQL 广泛使用Grouping Sets因为单条查询需要同时输出多个维度组合的聚合结果。在 Doris 的原生执行框架中普通 Distinct 的两阶段优化无法覆盖Grouping Sets Distinct的组合场景因此仍然会触发全局 Shuffle从而带来较高的网络开销和内存峰值压力。针对这一问题团队设计并实现了Local Distinct Grouping Sets 改写机制。其核心思想是在 Colocate 分桶已保证同一 UID 数据局部聚集的前提下将去重计算前移至各计算节点本地执行先完成局部 Distinct再对局部结果进行全局聚合汇总从而在语义等价的前提下降低 Shuffle 成本。该优化提供两种使用方式透明改写模式优化器自动识别Grouping Sets Distinct模式并重写为本地计算路径业务 SQL 无需改动显式调用模式业务侧可通过Distinct Local语法显式指定本地去重用于优化器未覆盖但已确认安全的场景。生产收益包括需要注意的是这项优化并非在所有场景下都一定优于原执行计划。对于原生COUNT DISTINCT场景系统可在 Shuffle 过程中边计算边传输具备一定的计算与网络并行能力而改写为 Local 模式后需要先完成本地去重再进入全局聚合阶段引入约 6 秒的 Barrier 等待开销。因此Local Distinct Grouping Sets 更适合 Shuffle 成为主要瓶颈的场景例如大基数 Grouping Sets Distinct。是否启用该优化需要结合 Profile 和实际执行计划判断。UDF Native 化压缩 CPU 热路径成本在 AB 实验链路中核心计算逻辑之一是分流判定 UDF即对每条用户行为日志判断其 UID 所属实验组、对照组或策略组。单次 UDF 调用的计算开销较小纳秒级但由于数据规模极大可达百亿级日志行该逻辑成为典型的 CPU 热路径。通过 Profile 分析发现Java UDF 占据约 80% CPU 开销主要瓶颈来自 JVM 调用与对象创建开销。为此团队将其改写为C Native UDF消除 JNI 调用成本并进一步深入到 STL 层进行热点优化包括内存分配、哈希访问和对象构造等。主要优化点如下P0字符串拼接优化。原实现每行创建 String 对象约占 30% CPU。优化后改为 ThreadLocal 复用固定 Buffer减少逐行内存分配与 GC 压力。P1实验配置访问优化。原实现每行通过 Unordered Map 查询实验配置存在哈希计算与指针跳转开销。优化后在初始化阶段展开为数组结构执行阶段通过下标 O(1) 访问。P2用户对象构造优化。原实现每行构造智能指针与对象实例引入堆分配与引用计数开销。优化后改为直接从 Block 列数据读取原始值避免对象化封装。总体来看该优化遵循两条核心原则一是尽可能消除热路径上的堆分配二是将循环内重复计算与查找提前至初始化阶段完成。最终三项优化叠加后AB 实验模板整体执行性能提升约 3 倍。调度优化用隔离、反压和优先级保障 SLA当 Doris 承载规模扩展到数十万级日任务后仅依赖 SQL 层或计算层优化已经不足以保证整体稳定性还需要引入调度层进行系统级治理。第一层是物理隔离。将整个 AB 集群拆分为 12 个独立计算组不同优先级业务分别运行在不同计算组内各组资源配额独立控制互不影响。这样即使低优任务出现流量突增也不会挤占高优计算资源从而避免跨业务干扰。第二层是组内控制。在单个计算组内部叠加三类机制并发上限控制限制同时运行的任务数量超出部分进入队列等待避免瞬时流量冲击集群上游反压机制根据实时负载动态控制任务提交速率使写入/提交节奏与集群处理能力保持匹配优先级队列调度划分 P1–P4 四级队列高优队列优先执行即使低优队列积压也不会影响高优任务调度。物理隔离负责守住跨组资源边界组内控制负责保障单组稳定性两者共同保证高优链路在高峰期仍能满足 SLA。稳定性治理性能优化完成后系统逐渐暴露出更隐蔽的一类瓶颈元数据稳定性问题。元数据治理被动修复到主动治理Doris FE 作为元数据管理节点库、表、分区、事务及 Tablet 等信息均保存在 JVM 内存中。为保证宕机可恢复性系统通过 Edit Log 持续追加写操作日志并周期性生成 Checkpoint Image 落盘重启时通过回放 Image 少量 Edit Log 完成恢复。在生产运行过程中主要暴露出两类风险1单表维度风险当单表分区规模增长至万级后FE 在生成 Checkpoint 时需要遍历完整分区列表由于内部结构使用 Java Int 索引触发上限约束导致进程异常退出影响生产链路稳定性。2集群维度风险随着表规模持续增长当 Tablet 达到千万级后FE 元数据对象在堆内持续累积Master FE 内存峰值接近 400GB 上限并频繁触发 Full GC系统整体稳定性受到威胁。针对上述问题团队从被动修复转向主动治理引入元数据容量建模与监控体系。通过统计分析与拟合得到经验结论单个 Tablet 在 FE 内存中平均占用约 11KB。基于该模型可以将 Tablet 增长量直接映射为内存增长趋势从而提前预测 FE 内存压力避免不可控扩张。FE 优化压缩元数据结构缩短恢复窗口在容量治理之外团队进一步对元数据结构本身进行了压缩优化。原有 Table Inverted Index 结构面向本地存储设计包含 local data path、local meta path 等冗余字段。在 Cloud 模式下这些字段并不参与实际计算形成额外内存开销。针对这一问题引入Cloud Table Inverted Index裁剪 Cloud 场景无用字段并将部分集合结构替换为紧凑数组结构从而降低对象开销。在百万级 Tablet压测中该优化使元数据内存占用从 4.16GB 降至 1.49GB整体下降约 64%。在千万级规模下该优化在 Checkpoint 峰值阶段可节省数十 GB 内存。FE 元数据、Catalog、EditLog Checkpoint 等长生命周期对象主要驻留在 Old 区在大堆场景下Mixed GC 的 Region 选择、年轻代比例以及 IHOP 触发节奏均需要重新适配否则 Old 区可能持续增长最终触发 Full GC。针对 FE 运行在超大堆约 400GB的场景对 G1 GC 参数进行了针对性调整调优完成后经过 24 小时线上验证如下Master FE 的内存峰值由 370 GB 降至 270 GB期间未发生任何 Full GC。最后对 FE 启动恢复流程进行了并行优化。原流程在 LoadDB 阶段需要串行加载元数据在百万级表规模下耗时约 17 分钟总启动恢复时间约 27 分钟。通过并行化加载表元数据该阶段耗时显著下降使整体 FE 恢复窗口缩短至约 10 分钟。这些实践说明随着 Doris 承载规模扩大元数据治理不能被视为单纯的运维问题而应提前纳入系统架构设计。容量模型、生命周期规划和恢复能力建设都是大规模生产系统稳定运行的重要组成部分。收益总结从引擎迁移到计算体系重构快手 AB 指标生产场景从 Spark 升级到 Doris 性能最大提升 145 倍、资源消耗下降 72%一方面得益于 Doris 在 Pipeline 异步执行、向量化计算引擎、C 高性能运行时等架构优势另一方面是快手基于 Doris 针对 AB 场景的深度优化Colocate JOIN、Local distinct count、C Native UDF、workload group 物理分组 逻辑队列资源隔离。快手 AB 指标生产场景的大规模落地证明 Doris 非常适合这个场景并且为社区提供了基于 Doris 构建 AB Test 平台的最佳实践。同时快手也刷新了全球最大单个 Doris 集群规模的记录达到 2000 节点、10 万核用生产实践回答了很多用关于 Doris 单机群扩展性的疑问相信这个规模对于 99% 的用户来说已经完全足够。关于 AB 指标生产和更多应用场景、Doris 落地和优化等主题欢迎大家加入 Doris 社区交流。