快手 AB 实验平台的指标生产链路,从 Spark 切换到 Apache Doris 后,性能提升 145 倍,资源消耗下降 72%。更值得注意的是,这不是一个小规模验证:相关实践刷新了 Doris 单集群规模,达到 2000 节点、10 万核。对做实验平台、指标平台、用户行为分析的团队来说,这个案例的价值不只在“快”,而在于它说明了什么时候该把离线批处理思路,换成面向分析服务的计算与存储模型。
AB 指标生产的瓶颈不只是计算慢
AB 实验指标生产通常有几个特点:
- 数据量大:曝光、点击、播放、消费、留存等行为日志会持续增长。
- 维度多:实验、分组、版本、地域、设备、用户分层、内容类型都可能成为聚合维度。
- 指标口径复杂:均值、转化率、分位数、留存、去重用户数、分桶统计会混在一起。
- 产出频繁:实验平台需要不断刷新指标,支持看板、告警、实验结论分析。
Spark 很适合大规模批处理,但在 AB 指标生产里,链路经常会变成“反复扫描、反复 shuffle、反复写中间结果”。当指标口径和维度组合膨胀后,调度开销、资源排队、数据重算和小文件问题都会把整体时效拉长。
Doris 的优势在于它把列式存储、MPP 执行、向量化计算和高并发查询放在一个分析型数据库里。对 AB 指标这种“按实验维度持续聚合、反复查询”的场景,很多计算可以被下推到存储和执行引擎中完成,减少外部批处理链路里的数据搬运。
从 Spark 到 Doris,变化发生在链路形态上
这类迁移不是把一段 Spark SQL 原样塞进 Doris 就结束。真正的变化通常在四个层面。
存储层需要围绕查询模式建表。AB 指标表往往适合按日期、实验 ID 等字段分区或分桶,并根据常用过滤条件设计排序键。这样查询单个实验、某天增量或某批实验时,可以少扫很多数据。
计算层从“大任务批量扫全量”变成“明细写入、预聚合、按需查询”。如果指标口径稳定,可以把部分聚合结果沉淀成宽表或聚合表;如果口径变化频繁,则保留更细粒度的明细层,再用 Doris 查询能力承接交互式分析。
调度层要从 Spark 作业依赖,转向数据导入、物化结果刷新和查询服务之间的协调。比如小时级导入行为日志,分钟级刷新核心实验指标,低频补算历史数据。
稳定性层变得更重要。快手案例中提到的 2000 节点、10 万核规模,意味着问题已经不只是 SQL 优化,而是集群治理:导入压力、Compaction、查询并发、资源隔离、失败重试都要被系统化处理。
可以这样实践:用 Doris 建一个 AB 指标雏形
下面示例不是快手内部实现,而是一个可改造的最小实践:用 Doris 存放 AB 事件明细,并查询实验组核心指标。你需要把 fe_host、端口、用户名、密码替换成自己的 Doris 环境。
创建库表:
CREATE DATABASE IF NOT EXISTS ab_platform;
USE ab_platform;
CREATE TABLE IF NOT EXISTS ab_event_detail (
event_date DATE NOT NULL,
experiment_id VARCHAR(64) NOT NULL,
variant_id VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
event_name VARCHAR(64) NOT NULL,
value DOUBLE DEFAULT 0,
event_time DATETIME NOT NULL
)
DUPLICATE KEY(event_date, experiment_id, variant_id, user_id)
PARTITION BY RANGE(event_date) ()
DISTRIBUTED BY HASH(experiment_id, user_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-30",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "16"
);
写入几条测试数据:
INSERT INTO ab_event_detail VALUES
('2025-01-01', 'exp_checkout_button', 'A', 1001, 'exposure', 1, '2025-01-01 10:00:00'),
('2025-01-01', 'exp_checkout_button', 'A', 1001, 'click', 1, '2025-01-01 10:00:02'),
('2025-01-01', 'exp_checkout_button', 'B', 1002, 'exposure', 1, '2025-01-01 10:01:00'),
('2025-01-01', 'exp_checkout_button', 'B', 1003, 'exposure', 1, '2025-01-01 10:02:00'),
('2025-01-01', 'exp_checkout_button', 'B', 1003, 'click', 1, '2025-01-01 10:02:05');
查询每个实验组的曝光用户、点击用户和点击率:
SELECT
experiment_id,
variant_id,
COUNT(DISTINCT IF(event_name = 'exposure', user_id, NULL)) AS exposure_users,
COUNT(DISTINCT IF(event_name = 'click', user_id, NULL)) AS click_users,
ROUND(
COUNT(DISTINCT IF(event_name = 'click', user_id, NULL)) /
NULLIF(COUNT(DISTINCT IF(event_name = 'exposure', user_id, NULL)), 0),
4
) AS ctr
FROM ab_event_detail
WHERE event_date = '2025-01-01'
AND experiment_id = 'exp_checkout_button'
GROUP BY experiment_id, variant_id
ORDER BY variant_id;
如果你希望通过命令行跑通,可以这样执行:
mysql -h fe_host -P 9030 -u root -p < ab_metric_demo.sql
在生产里,这个模型还需要继续补强:明细表按事件日期分区,按实验和用户分桶;高频指标可以落到汇总表;历史补算和实时导入要分开资源组;核心看板查询要做超时、限流和缓存。
迁移时最容易低估的四个问题
指标口径一致性是第一道门槛。Spark 迁 Doris 后,去重、空值、时间窗口、延迟数据处理方式都要对齐。不要只比较总行数,要抽样比较实验、分组、日期、核心指标的差异。
导入吞吐和查询并发会互相影响。AB 平台通常一边写入行为数据,一边服务看板查询。大规模导入会触发后台整理,查询高峰又会抢 CPU 和 IO。需要用资源隔离、导入批大小、分区策略一起治理。
表设计决定上限。Doris 能跑大规模集群,但错误的分区和分桶会让查询扫过多 tablet,或者让数据倾斜集中到少数节点。AB 场景里,实验 ID、日期、用户 ID 往往是关键设计字段。
调度不能只追求更快。Spark 作业失败通常有清晰的批任务边界;迁到 Doris 后,失败可能出现在导入、Compaction、物化视图刷新、查询超时等多个环节。调度系统要能重试、补偿、追踪每个指标版本。
落地建议:先迁最稳定、最高频的指标
这类迁移适合从高频、口径稳定、查询压力大的 AB 指标开始,而不是一口气替换所有 Spark 作业。可以按这个清单推进:
- 选 3 到 5 个核心实验指标,建立 Spark 与 Doris 双跑对账。
- 用真实查询日志设计分区、分桶、排序键,而不是只看数据写入方式。
- 把导入、补算、看板查询放进不同资源池,避免互相拖垮。
- 为每个指标保留口径版本,迁移期间允许按版本回溯。
- 先验证 P95/P99 查询耗时、资源消耗和失败率,再扩大实验范围。
快手这个案例说明,AB 指标生产不一定要长期绑在通用批处理引擎上。当场景从“离线算一次”变成“持续生产、频繁查询、多人同时看结果”,Apache Doris 这类实时分析数据库会更贴近问题本身。不过,145 倍提速来自场景匹配、链路重构和大规模工程优化的叠加,不是简单替换 SQL 引擎就能复现。真正值得学习的是:围绕指标生产的访问模式重新设计系统,而不是只给旧链路换一个执行器。