游乐游手机版
首页/AI热点日报/热点详情

快手AB场景提速145倍:Spark到Doris加速实践

类型:热点整理2026-07-09
快手AB指标生产场景从Spark迁移至Doris后,计算速度提升145倍,资源消耗下降72%,创下2000节点、10万核的单集群规模纪录。通过ColocateJoin、LocalDistinctGroupingSets、C++NativeUDF及调度隔离、元数据治理等优化,实现性能突破与稳定运行。

一组数据值得关注:快手AB指标生产场景从Spark切换到Doris后,计算速度提升了145倍,资源消耗下降了72%。与此同时,还刷新了Doris单集群最大规模的记录——2000个节点、10万核。

本文整理自快手数据平台部数据引擎技术中心曾斯维在Apache Doris × 快手北京Meetup上的分享。核心聚焦于为什么选择Apache Doris来构建AB实验平台,以及在这个落地过程中,团队针对存储、计算、调度、稳定性等方面做了哪些“硬核”优化。对于同样需要搭建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指标计算模板长期稳定,团队才有机会围绕执行链路做更深入的专项优化,最终实现了145倍的性能提升。

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

完成引擎迁移后,团队并没有止步于简单的引擎替换。他们进一步围绕AB指标计算模板的特点,对数据分布、执行计划、计算热点、资源调度和元数据治理进行了系统性的专项优化。最终,这些优化共同作用,彻底释放了Doris在该场景下的性能潜力。

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

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

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

其中,第二步和第三步是优化的主战场。因为这里同时涉及Join、分组聚合、网络传输和CPU计算这几种高消耗操作。

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

3.1 存储优化:用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配置没有对齐,得继续排查。

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

3.2.1 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语法显式指定本地去重,用来覆盖优化器没能识别但经过确认是安全的场景。

生产环境的收益很明显:

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

因此,Local Distinct Grouping Sets更适合于Shuffle成为主要瓶颈的场景,比如大量级的Grouping Sets + Distinct。是否启用这项优化,还需要结合Profile和实际执行计划来综合判断。

3.2.2 UDF Native化:压缩CPU热路径成本

在AB实验链路中,一个核心计算逻辑是分流判定UDF,它需要为每一条用户行为日志判断其UID所属的实验组、对照组或策略组。

单次UDF调用的计算开销其实很小(纳秒级),但因为数据规模极其庞大(可以达到百亿级日志行),这个逻辑就成了典型的CPU热路径。

通过Profile分析发现,Ja va 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倍。

3.3 调度优化:用隔离、反压和优先级保障SLA

当Doris承载的规模扩展到数十万级日任务后,光是靠SQL层或计算层的优化已经不足以保证整体稳定性了,还需要在调度层做系统级的治理。

第一层是物理隔离。 把整个AB集群拆分为12个独立计算组,不同优先级的业务分别跑在不同的计算组里,各组资源配额独立控制,互不干扰。这样一来,即使低优先级的任务出现流量突增,也不会挤占高优先级的计算资源,有效避免了跨业务干扰。

第二层是组内控制。 在单个计算组内部,叠加了三类机制:

  • 并发上限控制: 限制同时运行的任务数量,超出的部分进入队列等待,避免瞬时流量冲击集群。
  • 上游反压机制: 根据实时负载动态控制任务提交速率,让写入/提交的节奏和集群的处理能力相匹配。
  • 优先级队列调度: 划分P1–P4四级队列,高优队列优先执行,即使低优队列积压,也不会影响高优任务的调度。

物理隔离负责守住跨组的资源边界,组内控制负责保障单组的稳定性。两者协同,确保了高优链路在高峰期依然能满足SLA。

稳定性治理

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

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

Doris的FE作为元数据管理节点,库、表、分区、事务以及Tablet等信息都保存在JVM内存里。为了保证宕机后能恢复,系统通过Edit Log持续追加写操作日志,并周期性生成Checkpoint Image落盘,重启时通过回放Image加上少量Edit Log来完成恢复。在生产运行中,主要暴露了两类风险:

1)单表维度风险: 当单表的分区规模增长到万级以后,FE在生成Checkpoint时需要遍历完整的分区列表,而内部结构使用了Ja va Int索引,触发上限约束,导致进程异常退出,影响了生产链路的稳定性。

2)集群维度风险: 随着表规模持续增长,当Tablet达到千万级后,FE元数据对象在堆内持续累积,Master FE的内存峰值逼近了400GB的上限,并频繁触发Full GC,系统整体稳定性受到了严重威胁。

针对这些问题,团队从被动修复转向了主动治理,引入了元数据容量建模与监控体系。

通过统计分析和拟合,得出了一个经验结论:单个Tablet在FE内存中平均占用大约11KB。基于这个模型,可以把Tablet的增长量直接映射为内存增长趋势,从而提前预判FE的内存压力,避免出现不可控的扩张。

4.2 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的内存峰值从370GB降到了270GB,期间没有发生任何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单集群扩展性的疑问。可以明确的是,这个规模对于绝大多数用户来说,已经完全足够应对需求了。

来源:https://www.bestblogs.dev/article/96119afc?utm_source=rss&utm_medium=feed&utm_campaign=resources&entry=rss_article_item

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。