如果你计划自研 A/B 实验平台,本文提供从 SDK 到数据管道到管理后台的完整工程架构指引。如果你采购商业平台,本文也可作为技术选型的评估框架。
管理后台层
实验管理 指标管理 权限管理 实验知识库
CRUD 字典 RBAC 归档 & 搜索
分析引擎层
统计检验 CUPED 报告生成 异常检测
t-test 方差削减 自动分析 SRM / AA
数据管道层
埋点采集 实时计算 离线计算 指标存储
SDK 上报 Flink Spark ClickHouse
分流服务层 + SDK 层(业务链路)
业务 SDK 分流服务 配置中心
多语言 哈希 + 路由 实验配置存储
1. 实验配置流(下行):
管理后台 配置中心 分流服务缓存 SDK 本地缓存
2. 用户分流流(在线):
用户请求 业务服务 SDK 本地缓存查 Flag 执行对应逻辑
3. 数据上报流(上行):
用户行为 业务埋点 SDK 事件总线 实时/离线计算 指标存储
4. 分析流(离线):
指标存储 分析引擎 实验报告 管理后台展示
业务 SDK 需要做的事情(越少越好):
1. 获取实验配置(启动时拉取 + 定期更新)
2. 对每个用户请求判断:user_id 属于实验的哪个组?
3. 返回分组结果,业务代码据此执行不同逻辑
4. 上报曝光事件(用户真正看到了什么)
SDK 不需要做的事情:
- 统计检验
- 指标计算
- 数据存储
# Python SDK 示例接口
class ExperimentClient:
def get_variant(
self,
experiment_id: str,
user_id: str,
attributes: dict = None
) -> VariantResult:
"""
返回用户在该实验中的分组。
Returns:
VariantResult:
- variant: str # "control" | "treatment_A" | ...
- config: dict # 实验组对应的策略参数(可选)
- in_experiment: bool
"""
pass
def track_exposure(
self,
experiment_id: str,
user_id: str,
variant: str
) -> None:
"""
上报用户真正看到了实验策略的事件。
这是数据质量的关键避免分析时"用户被分了组但没看到策略"。
"""
pass
def get_all_variants(
self,
user_id: str
) -> dict[str, VariantResult]:
"""
获取该用户当前参与的所有实验及其分组。
用于调试和问题排查。
"""
pass
启动时:
SDK 初始化 从分流服务全量拉取当前活跃的实验配置
缓存到本地内存(JSON/Protobuf)
运行时:
SDK 内存缓存直接判断分组,不产生网络请求
延迟 < 0.1ms
配置更新:
方案 A(轮询):每 N 秒拉取一次配置更新(简单,但有延迟)
方案 B(推送):分流服务通过 WebSocket / gRPC Stream 推送配置变更
方案 C(混合):轮询兜底 + 推送加速
推荐:方案 A 起步(简单可靠),方案 C 在成熟期优化
SDK 容错层级:
1. 内存缓存命中 直接返回(正常路径)
2. 缓存未命中 调用分流服务
3. 分流服务超时/不可用 使用本地磁盘缓存(上次拉取的配置)
4. 磁盘缓存不可用 返回默认值(通常是 control / 原策略)
5. 全链路不可用 SDK 抛出 soft error,业务逻辑走原策略
关键原则:
- SDK 挂了不能影响业务主流程
- 降级策略 = 原策略(宁可实验数据不准,不能业务受损)
- 所有异常都要有结构化日志 + 告警
问题:Java、Go、Python、JavaScript 四种语言的 SDK,如何保证同一 user_id 的分流结果一致?
方案:
核心分流逻辑用同一套算法规范,所有语言实现必须通过一致性测试:
1. 定义算法规范文档(不依赖特定语言实现)
2. 构建一致性测试集:
- 10 万个 user_id
- 100 个 experiment_id
- 5 个 layer_id
- 计算每种组合的 bucket 值
3. 每种语言的 SDK 实现必须通过 100% 一致性验证
4. CI 流程中自动运行一致性测试
版本管理:
- 主版本号:不兼容的 API 变更
- 次版本号:向后兼容的功能新增
- 修订号:Bug 修复
SDK 升级策略:
- 业务方升级 SDK 的窗口期通常 1-4 周
- SDK 重大升级需要兼容期(新旧 SDK 共存)
- 分流算法变更 新算法用新的 layer_id 起新层,旧层不变
分流服务是一个高可用、低延迟的在线服务:
1. 哈希计算:user_id + experiment_id + layer_id bucket_id
2. 分组映射:bucket_id group_name(查配置)
3. 配置缓存:实时同步最新的实验配置
4. 一致性保证:同一用户在整个实验周期内分组不变
核心哈希:
bucket = MurmurHash3_128(salt + ":" + user_id + ":" + layer_id) % 10000
输入:
- salt:固定随机种子(每个 layer 不同,保证层间独立)
- user_id:用户唯一标识(字符串)
- layer_id:层标识
输出:
- bucket [0, 9999](10000 个桶,每桶 0.01% 流量)
分组映射(从配置中获取):
Experiment:
layer_id = "layer_homepage"
control: bucket [0, 4999] 50% 流量
treatment: bucket [5000, 9999] 50% 流量
负载均衡
(Envoy/Nginx)
分流服务-1 分流服务-2 分流服务-3
(无状态) (无状态) (无状态)
配置中心
(Etcd/
Consul)
设计要点:
| 要点 | 实现 |
|---|---|
| 无状态设计 | 分流服务不存储任何状态,所有配置从配置中心读取 |
| 水平扩展 | 任意扩展实例,负载均衡自动分配 |
| 配置热更新 | 配置中心变更 分流服务 Watch 到变更 内存缓存刷新 |
| 本地缓存兜底 | 配置中心不可用时,使用本地磁盘缓存的配置快照 |
延迟:
P50 < 0.2ms(内存哈希计算)
P99 < 1ms
P999 < 5ms
吞吐:
单实例 > 50,000 QPS
(仅做哈希计算 + 查内存表,不涉及 IO)
可用性:
SLA > 99.99%(年宕机 < 53 分钟)
1. user_id 一旦进入实验,Hash 值永久不变
不能中途改 salt、layer_id、分桶数
2. 实验分流比例可以变(调整哪些桶属于哪个组)
但只能"增加流量",不能"减少流量"
正确:control [0,7999] [0,4999],treatment [8000,9999] [5000,9999]
错误:直接改变已分配用户的组别
3. 实验停止后,分流逻辑保留至少 7 天
确保离线数据管道中的延迟数据仍能正确归属
{
"experiment_id": "exp_homepage_redesign_2026",
"status": "running",
"layer_id": "layer_homepage",
"layer_type": "exclusive",
"buckets_total": 10000,
"salt": "a3f8b2c1_homepage",
"start_time": "2026-06-01T00:00:00Z",
"end_time": "2026-06-15T00:00:00Z",
"variants": [
{
"name": "control",
"bucket_range": [0, 4999],
"config": {
"homepage_version": "v3.2.1",
"theme": "default"
}
},
{
"name": "treatment",
"bucket_range": [5000, 9999],
"config": {
"homepage_version": "v4.0.0-beta",
"theme": "new_design"
}
}
],
"metrics": {
"primary": "ctr_core_module",
"guardrail": ["page_load_time", "crash_rate"]
}
}
管理员操作 管理后台
配置中心(Etcd / Consul)
分流服务-1 分流服务-2 分流服务-3
(Watch 配置变更,< 1s 生效)
业务 SDK 轮询拉取配置更新
(30-60s 的更新延迟)
延迟分析:
| 环节 | 典型延迟 | 说明 |
|---|---|---|
| 管理员保存 配置中心 | < 100ms | API 写入 |
| 配置中心 分流服务 | < 500ms | Watch 机制推送 |
| 分流服务 SDK | 30-60s | SDK 轮询间隔 |
| SDK 缓存刷新 | < 1ms | 内存操作 |
总端到端延迟:通常 30-60 秒,对实验场景完全可接受。
每次配置变更记录:
- 版本号
- 变更内容(diff)
- 变更时间
- 操作人
- 审批记录
回滚机制:
- 配置中心保留最近 N 个版本
- 管理员一键回滚到历史版本
- 回滚后,分流服务 < 1s 内生效,SDK < 60s 内生效
实验停止(非回滚):
- 实验结束后状态变为"已完成"
- 分流配置保留 7-14 天(允许延迟数据和日志查询)
- 过期后自动清理
所有实验相关的事件必须包含以下字段:
{
"event_id": "uuid",
"event_type": "page_view | click | purchase | ...",
"timestamp": "2026-06-10T14:30:00.000Z",
"user_id": "user_12345",
"device_id": "device_abc",
"platform": "ios | android | web",
"app_version": "6.2.0",
"experiments": [
{
"experiment_id": "exp_homepage_redesign_2026",
"variant": "treatment",
"is_exposed": true
}
],
"event_properties": {
"page_name": "homepage",
"module_id": "recommend_feed",
"duration_ms": 3200
}
}
关键规范:
| 要求 | 说明 |
|---|---|
experiments 必须是数组 |
一个用户可能同时参与多个正交层的实验 |
is_exposed |
标记用户是否真正看到了实验策略(用于计算实际触达率) |
timestamp |
统一使用 UTC 时区,禁止混用本地时间 |
| 字段不得缺失 | 上述字段任意一个缺失都应在数据管道中计数告警 |
业务服务 数据平台
埋点 SDK Event Bus 实时计算(Flink)
(Kafka)
实时实验看板
(效应量趋势)
实时 SRM 检测
离线仓库(Hive / Iceberg)
离线计算(Spark SQL)
日级指标宽表
(user_id date experiment metrics)
实验报告生成
(统计检验 + CI + 可视化)
指标字典校验
(口径一致性检查)
模式对比:
| 维度 | 实时计算 | 离线计算 |
|---|---|---|
| 用途 | 实验监控看板、SRM 检测 | 正式实验报告、统计检验 |
| 时效 | 秒/分钟级 | 小时/天级 |
| 精确度 | 近似(丢数据可接受) | 精确(数据完整性要求高) |
| 指标复杂度 | 简单聚合(SUM/COUNT/AVG) | 复杂(Join、UDF、CUPED) |
| 技术栈 | Flink + ClickHouse | Spark + Hive / Iceberg |
为什么不能只用实时计算?
数据质量检查流水线:
1. 完整性检查
实验事件的 experiments 字段是否为空?
user_id 是否为 null 或 "unknown"?
是否有重复事件(幂等性检查)?
2. 一致性检查
同一 user 在同一 experiment 中的 variant 是否一致?
实验时间段内的数据是否正确归因?
3. 及时性检查
数据从产生到进入离线仓库的延迟是否在 SLA 内?
每天的数据量是否与前 N 天在合理范围内?(防止数据管道中断)
4. 异常检测
某组的数据量突然暴增/暴跌?
某组的关键指标值异常偏离历史趋势?
分析引擎 = 数据处理 + 统计检验 + 结果输出
分析引擎
输入:实验 ID + 分析时间窗口
数据处理 统计检验
1.SRM 1.t-test
2.异常值 2.CUPED
3.过滤 3.Bootstrap
4.聚合 4.多重校正
报告生成
- 效应量 + CI
- 时间趋势
- HTE 探索
- 可视化
输出:实验报告(Markdown + 图表)
# 分析引擎核心逻辑(简化示例)
class ExperimentAnalyzer:
def analyze(self, experiment_id: str) -> ExperimentReport:
# 1. 加载数据
data = self.load_experiment_data(experiment_id)
# 2. SRM 检验(第一步!)
srm_result = self.check_srm(data)
if not srm_result.passed:
raise SRMException(f"SRM failed: {srm_result}")
# 3. 数据质量检查与清洗
data = self.clean_data(data)
# - 移除异常值(Winsorization)
# - 过滤 bot 流量
# - 处理缺失值
# 4. CUPED 方差削减
data_cuped = self.apply_cuped(
data,
covariate_metric="pre_experiment_gmv_7d"
)
# 5. 统计检验
primary_result = self.welch_t_test(
data_cuped,
metric="oec",
alpha=0.05
)
# 6. 护栏指标检查
guardrail_results = {
metric: self.welch_t_test(data_cuped, metric=metric, alpha=0.10)
for metric in self.get_guardrail_metrics(experiment_id)
}
# 7. 多重检验校正(对探索性指标)
exploratory_results = self.bh_correction(
[self.welch_t_test(data_cuped, metric=m)
for m in self.get_exploratory_metrics(experiment_id)]
)
# 8. 时间趋势分析
daily_effects = self.compute_daily_effects(data_cuped)
novelty_check = self.check_novelty_effect(daily_effects)
# 9. 生成报告
return self.generate_report(
primary_result,
guardrail_results,
daily_effects,
novelty_check
)
分析引擎输出的标准报告结构:
## 实验报告:{实验名称}
### 数据质量
- SRM: = x.xx, p = x.xxx
- 数据完整性:xx.x%
- 异常值处理:P99 Winsorization
### 主指标结果(CUPED 修正后)
[效应量表格 + 置信区间图]
### 时间趋势
[效应量 实验天数的折线图]
[Novelty Effect 检测结果]
### 护栏指标
[每个护栏指标的效应量 + 状态]
### 决策建议
- 统计显著:是/否
- 效应量 MDE:是/否
- 护栏正常:是/否
- 建议:上线 / 迭代 / 放弃
### 探索性发现(仅供假设生成)
- HTE 分组结果
- 用户反馈要点
| 页面 | 功能 |
|---|---|
| 实验列表 | 查看所有实验的状态、所有者、剩余时间 |
| 实验配置 | 创建/编辑实验:分流比例、指标配置、MDE 设定 |
| 实验详情 | 单个实验的实时看板、数据质量、分析报告 |
| 指标字典 | 浏览/搜索/管理所有标准化指标 |
| 层管理 | 查看各层的流量使用情况、冲突检测 |
| 知识库 | 搜索历史实验、查看复盘结论 |
创建实验 4 步向导:
Step 1:基本信息
- 实验名称、描述、假设
- Owner、团队
Step 2:分流配置
- 选择层(自动推荐空闲层)
- 设置分流比例
- 系统自动检查:该层是否有流量冲突?
Step 3:指标配置
- 从指标字典中选择 OEC(可选复合指标)
- 从指标字典中选择护栏指标(系统推荐默认护栏)
- 设置 MDE、、
Step 4:审查与启动
- 自动审查:
SRM 预期检查
流量冲突检测
样本量估算
护栏指标完整性
- 审批流(关键实验需要审批)
- 一键启动
第一层:基础设施监控
- 分流服务 CPU / 内存 / QPS / 延迟
- 配置中心可用性
- 数据管道 Kafka Lag
- 离线计算任务完成时间
第二层:实验质量监控
- SRM 自动检测(每 30 分钟一次)
- AA 测试的 Type-I Error Rate(周级校验)
- 数据完整性(事件到达率、缺失字段率)
第三层:业务指标监控
- 护栏指标异常检测(实验组显著恶化)
- 指标值异常波动(超出历史 3 范围)
第四层:平台运营监控
- 活跃实验数
- 实验平均时长
- 实验成功率(显著正向比例)
- 平台月活用户(使用实验平台的产品/运营人员数)
| 告警级别 | 触发条件 | 通知方式 | 响应时间 |
|---|---|---|---|
| P0 | 分流服务不可用 | 电话 + 企业微信 + Oncall | 5 分钟内 |
| P0 | 护栏指标反向显著恶化 | 电话 + 企业微信 + 实验 Owner | 15 分钟内 |
| P1 | SRM 连续两次检测异常 | 企业微信 + 实验 Owner | 30 分钟内 |
| P1 | 数据管道延迟 > 2 小时 | 企业微信 + 数据团队 | 1 小时内 |
| P2 | 护栏指标正向显著恶化 | 企业微信 + 实验 Owner | 2 小时内 |
| P3 | 数据质量指标轻度异常 | 邮件/企微群 | 当天处理 |
| 组件 | 指标 | P50 | P99 | P999 |
|---|---|---|---|---|
| 分流服务 | 延迟 | < 0.2ms | < 1ms | < 5ms |
| 分流服务 | 吞吐(单实例) | - | > 50K QPS | - |
| SDK 本地分流 | 延迟 | < 0.05ms | < 0.1ms | < 0.5ms |
| 配置变更生效 | 端到端延迟 | < 10s | < 60s | < 120s |
| 离线报告生成 | 完成时间 | < 30min | < 2h | < 4h |
| 实时看板 | 数据新鲜度 | < 1min | < 5min | < 15min |
| 组件 | 可用性目标 | 允许停机时间/年 |
|---|---|---|
| 分流服务 | 99.99% | < 53 分钟 |
| 配置中心 | 99.99% | < 53 分钟 |
| 数据管道(实时) | 99.9% | < 8.8 小时 |
| 数据管道(离线) | 99.5% | < 43.8 小时 |
| 管理后台 | 99.9% | < 8.8 小时 |
| 层 | 组件 | 推荐方案 | 替代方案 |
|---|---|---|---|
| SDK | 多语言客户端 | Java / Go / Python / JS | 自研各语言 SDK |
| 分流服务 | 在线服务 | Go(高性能)或 Rust | Java + Spring |
| 配置中心 | 配置存储与推送 | Etcd | Consul, Zookeeper, 自研 DB |
| 事件总线 | 流数据接入 | Kafka | Pulsar, AWS Kinesis |
| 实时计算 | 流处理 | Apache Flink | Spark Streaming, Kafka Streams |
| 离线计算 | 批处理 | Spark SQL | Trino, Hive |
| 指标存储 | OLAP | ClickHouse | Druid, StarRocks, Doris |
| 离线仓库 | 数据湖 | Iceberg + Hive Metastore | Delta Lake, Hudi |
| 分析引擎 | 统计计算 | Python (SciPy + Statsmodels) | R |
| 管理后台 | Web 前端 | React + TypeScript | Vue |
| 管理后台 | 后端 API | Go / Python (FastAPI) | Java (Spring Boot) |
Phase 1(MVP,0-3 个月)
- 分流:Go 服务 + MySQL 存储配置(简单但够用)
- 分析:Python 脚本手动跑
- 看板:Grafana + ClickHouse
- 目标:1 个人 1 个月内跑通第一个实验
Phase 2(平台化,3-6 个月)
- 分流:Go 服务 + Etcd 配置中心(支持热更新)
- 分析:Python 自动化分析引擎
- 管理后台:React 前端
- 数据管道:Kafka + Flink + ClickHouse
- 目标:10 个实验并发,SRM 自动检测,自动报告
Phase 3(规模化,6-12 个月)
- 分流:多机房部署,全球化
- 分析:CUPED 内置、序贯检验、贝叶斯
- 数据:Iceberg 数据湖 + 完整指标字典
- 平台:实验知识库、HTE 分析、审批流
- 目标:100+ 实验并发,全公司统一平台
你的团队是否具备以下条件?
至少 3 名全职工程师可投入平台开发?
否 采购商业平台(Statsig / Eppo / GrowthBook)
实验并发量 > 100 个/天?
否 采购或开源方案即可满足
有特殊的合规/数据安全要求(数据不能出内网)?
是 自研或 GrowthBook 自托管
否 考虑 SaaS
需要深度定制分流逻辑(如非标准实验单元、特殊哈希规则)?
是 自研
否 采购平台 + 轻量定制