← 返回博客
用户案例

快手 AB 场景提速 145 倍,从 Spark 到 Apache Doris 的加速实践

曾斯维,快手数据平台部 · 2026/7/3
SelectDB 微信公众号
SelectDB 公众号
获取技术干货和产品动态

导读:快手 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千亿级行

1-AB 指标计算链路的性能与成本压力.PNG

在这一规模下,任何低效算子都会被放大,因此必须从底层执行、存储分布、计算算子和调度治理等多个层面系统优化。

为什么 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 性能提升

2-为什么 Doris 比 Spark 更适合该场景.PNG

关键优化:从减少数据流动到压缩计算热路径

完成引擎迁移后,团队没有只停留在替换执行引擎层面,而是进一步围绕 AB 指标计算模板的特点,对数据分布、执行计划、计算热点、资源调度和元数据治理进行了针对性的系统优化。最终,多项优化共同作用,释放了 Doris 在该场景下的性能潜力。

AB 指标计算链路的结构相对稳定,输入通常固定为两类数据:一类是累计分流表,用于记录用户命中了哪个实验、哪个分组;另一类是指标宽表,用于记录用户行为指标。输出也相对固定,通常按照实验、分组、桶等维度进行聚合,结果集一般只有百行到万行。

完整计算过程主要包括四步:

  1. 扫描累计分流表,按日期和实验名过滤命中信息;
  2. 扫描指标宽表,按 UID 进行预聚合,并处理指标口径;
  3. 两张表按 UID Join,并做桶粒度聚合;
  4. 将桶粒度结果上卷,写入实验结果表。

其中,第二步和第三步是优化主战场,因为这里同时涉及 Join、分组聚合、网络传输和 CPU 计算开销

3-从减少数据流动到压缩计算热路径.png

这个场景最关键的特点是:Join Key 长期固定为 UID,聚合维度也相对稳定。换句话说,这不是一个随机查询模式高度复杂的 OLAP 场景,而是一个计算模板长期稳定、执行路径高度一致的生产计算场景,因此具备持续专项优化的条件。

4-从减少数据流动到压缩计算热路径 2.png

存储优化:用 Colocate Join 消除跨节点 Shuffle

第一项核心优化是 Colocate Join。它的基本思路是:在数据写入阶段,就按照 UID 对数据进行哈希分桶,使相同 UID 的数据始终落到同一台机器、同一个分桶内。

这样做的好处是,查询时分流表和行为表可以直接在本地完成 Join,不需要跨节点 Shuffle 或搬运大批量数据。每个节点只需处理本地分桶内的数据,并完成本地聚合,最后再汇总少量桶级结果。

从生产数据看,单个 Bucket 扫描千万级数据后,本地 Join 可以先过滤掉约 95% 的无效数据,只保留几十万行匹配结果,最终输出几千行。也就是说,大部分数据在本地节点内就被消化掉了,网络中只传输 Join 和聚合后的少量结果集。

落地 Colocate Join 时,需要重点保证两点:

  1. 建表时必须按照 Join Key,也就是 UID,进行哈希分桶;
  2. 参与 Join 的两张表,分桶数必须严格一致,不能有任何偏差。

表结构调整完成后,还需要通过 EXPLAIN 检查执行计划。只有执行计划中出现 Colocated,才说明该优化真正生效。如果计划中仍然存在 Shuffle,通常意味着两张表的分桶数、分布键或 Colocate 配置没有对齐,需要继续排查。

5- Colocate Join 消除跨节点 Shuffle.png

计算优化:降低去重聚合与 UDF 热路径开销

Local Distinct Grouping Sets:减少全局 Shuffle

Colocate 优化已经消除了 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 语法显式指定本地去重,用于优化器未覆盖但已确认安全的场景。

生产收益包括:

6-Local Distinct Grouping Sets.png

需要注意的是,这项优化并非在所有场景下都一定优于原执行计划。对于原生 COUNT DISTINCT 场景,系统可在 Shuffle 过程中边计算边传输,具备一定的计算与网络并行能力;而改写为 Local 模式后,需要先完成本地去重再进入全局聚合阶段,引入约 6 秒的 Barrier 等待开销。

因此,Local Distinct Grouping Sets 更适合 Shuffle 成为主要瓶颈的场景,例如大基数 Grouping Sets + Distinct。是否启用该优化,需要结合 Profile 和实际执行计划判断

7-Local Distinct Grouping Sets2.png

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

8-用隔离、反压和优先级保障 SLA.png

稳定性治理

性能优化完成后,系统逐渐暴露出更隐蔽的一类瓶颈:元数据稳定性问题。

元数据治理:被动修复到主动治理

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 参数进行了针对性调整:

9-压缩元数据结构,缩短恢复窗口.png

调优完成后,经过 24 小时线上验证(如下),Master FE 的内存峰值由 370 GB 降至 270 GB,期间未发生任何 Full GC。

10-压缩元数据结构,缩短恢复窗口 2.png

最后,对 FE 启动恢复流程进行了并行优化。原流程在 LoadDB 阶段需要串行加载元数据,在百万级表规模下耗时约 17 分钟,总启动恢复时间约 27 分钟。通过并行化加载表元数据,该阶段耗时显著下降,使整体 FE 恢复窗口缩短至约 10 分钟11-压缩元数据结构,缩短恢复窗口 3.png

这些实践说明,随着 Doris 承载规模扩大,元数据治理不能被视为单纯的运维问题,而应提前纳入系统架构设计。容量模型、生命周期规划和恢复能力建设,都是大规模生产系统稳定运行的重要组成部分。

收益总结:从引擎迁移到计算体系重构

12-从引擎迁移到计算体系重构.jpeg

快手 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 社区交流。 13 .png