3. 实时实验系统架构:实时数据流、事件驱动架构、实验平台核心组件
这一章,我们来聊聊实时实验系统的骨架。
很多同学做 AB 测试,还停留在「上线 → 等一天 → 看报表」的阶段。但说实话,在用户行为瞬息万变的场景下——比如推荐系统、广告竞价、动态定价——你等一天再出结果,黄花菜都凉了。实时实验系统,就是让你在几分钟甚至几秒内,看到实验效果。
我个人习惯把实时实验系统拆成三个层面:数据怎么流进来、系统怎么响应事件、核心组件怎么协作。下面我们一个一个说。
3.1 实时数据流:从埋点到 Kafka
实时实验的第一步,是数据能「秒级」到达计算引擎。这背后离不开消息队列。
我在项目中遇到过最典型的场景:客户端上报用户点击事件,如果直接写数据库,QPS 一高,DB 立马被打挂。所以,我们通常用 Kafka 或 Pulsar 做缓冲层。
核心链路:
用户行为 → SDK 埋点 → 网关 → Kafka Topic → 实时计算引擎(Flink/Spark Streaming)→ 实验分流决策 → 结果回写
这里有个关键点:事件必须携带实验上下文。每条消息里,除了用户 ID、行为类型、时间戳,还得带上当前命中的实验 ID 和分桶 ID。否则下游没法判断这条数据属于哪个实验组。
我的经验:Kafka 的 Topic 设计,建议按业务域拆分。比如「曝光事件」「点击事件」「转化事件」各一个 Topic。别把所有事件塞到一个 Topic 里,否则消费端容易乱,排查问题也费劲。
3.2 事件驱动架构:不轮询,只响应
实时系统最忌讳「轮询」。你想想看,每隔几秒查一次数据库,不仅浪费资源,延迟还高。事件驱动架构,说白了就是「有事才干活」。
在实验系统里,事件驱动体现在两个地方:
- 分流决策事件化:用户请求进来,不是去查一个静态配置,而是触发一个「分流事件」,由事件处理器实时计算该用户进哪个实验组。
- 指标计算事件化:每来一条用户行为数据,就触发一次指标更新。比如用户点击了按钮,立即更新该实验组的点击率分子分母。
我曾经踩过一个坑:用 Redis 的 pub/sub 做事件分发。看起来简单,但一旦消费者挂了,消息直接丢失。后来全部换成 Kafka 或 Pulsar,至少保证消息不丢。
避坑指南:事件驱动架构下,一定要考虑「事件回溯」能力。比如你修复了一个 bug,需要重新计算过去 1 小时的数据。如果事件没有持久化,你就只能干瞪眼。所以,Kafka 的 retention 设置长一点,我一般设 7 天。
3.3 实验平台核心组件
一个完整的实时实验平台,至少包含以下 5 个核心组件。我画了一张架构图,方便你理解它们之间的关系。
下面我把每个组件的职责拆开讲。
3.3.1 实验配置中心
这是实验平台的「大脑」。它管理着所有实验的元数据:实验名称、状态(运行中/暂停/结束)、分桶比例、白名单、分层信息等。
我建议配置中心用 etcd 或 ZooKeeper 这类强一致性存储。为什么?因为实验配置变更必须原子化——你不想出现「一半机器用了新配置,另一半还在用旧配置」的情况吧?
小技巧:配置中心要支持「热加载」。每次修改实验参数,不需要重启服务。我一般用 Watch 机制监听配置变化,变化后自动刷新本地缓存。
3.3.2 分桶引擎
分桶引擎负责把用户分配到不同的实验组。它通常是一个无状态的服务,输入用户 ID,输出实验组 ID。
常见的分桶算法有:
| 算法 | 特点 | 适用场景 |
|---|---|---|
| Hash 分桶 | 一致性高,同一用户始终进入同一桶 | 长期实验、用户级实验 |
| Mod 分桶 | 简单快速,但扩容时容易打乱 | 短期实验、请求级实验 |
| 分层分桶 | 多个实验互不干扰,流量复用 | 多实验并行、复杂场景 |
我在项目中遇到过一个问题:Hash 分桶时,如果用户 ID 是字符串,直接用 hashCode() 会导致分布不均匀。后来我改用 MurmurHash,分布就平滑多了。
3.3.3 实时指标计算器
这是实时实验的「心脏」。每来一条事件,它都要更新对应的指标。
举个例子,计算实时点击率:
// 伪代码:实时点击率计算
class RealtimeCTRCalculator {
private AtomicLong impressions = new AtomicLong(0);
private AtomicLong clicks = new AtomicLong(0);
public void onEvent(Event event) {
if (event.type == IMPRESSION) {
impressions.incrementAndGet();
} else if (event.type == CLICK) {
clicks.incrementAndGet();
}
// 实时更新 Redis 中的指标
redisClient.hset("exp:123:group:A", "ctr", clicks.get() / (double) impressions.get());
}
}
注意,这里用了 AtomicLong 保证线程安全。如果你用 Flink,可以用 ValueState 或 ReducingState 做窗口聚合。
我曾经踩过的坑:指标计算器如果直接写数据库,高并发下性能极差。后来我改成「先写 Redis,再异步刷到 ClickHouse」。Redis 扛高并发,ClickHouse 做历史查询,各司其职。
3.3.4 可视化与告警模块
数据算出来了,得让人看得懂。可视化模块通常展示:
- 实时趋势图:实验组 vs 对照组,指标随时间变化
- 累计统计:累计样本量、累计均值、置信区间
- 告警规则:比如「实验组指标下降超过 5%」时自动告警
我个人习惯用 Grafana 做可视化,配上 Prometheus 做告警。当然,你也可以自建 Dashboard,但 Grafana 的生态更成熟,省心。
3.4 架构设计要点总结
最后,我把实时实验系统架构的几个关键原则列出来,你设计系统时可以对照着检查:
- 解耦:每层通过消息队列通信,不要直接调用
- 无状态:分桶引擎、指标计算器尽量无状态,方便水平扩展
- 可回溯:事件数据持久化,支持历史重算
- 低延迟:从事件产生到指标可见,延迟控制在秒级
- 一致性:实验配置变更必须原子化,避免脏读