大家好,我是Java烘焙師。本文結(jié)合筆者的經(jīng)驗和思考,對灰度方案做個總結(jié),重點介紹AB實驗。
灰度在開發(fā)流程中非常普遍。先做小流量驗證,確認無誤后再推全,灰度過程中一旦發(fā)現(xiàn)系統(tǒng)異常、或業(yè)務(wù)指標異常,應(yīng)立刻回滾。
灰度場景
- 代碼灰度:是最典型的灰度,灰度內(nèi)做新邏輯,灰度外做舊邏輯
- 既可以提供v2版本新接口給調(diào)用方服務(wù),由調(diào)用方來做灰度切換
- 也可以內(nèi)部切灰度,做到調(diào)用方無感
- 發(fā)版灰度:上線過程中,新版本服務(wù)實例不斷增加,需考慮兼容新舊協(xié)議
- 配置灰度:修改配置時,按服務(wù)實例灰度推送配置變更
灰度模式
- 數(shù)字id尾號灰度:取id最后2位(百分比)、最后3位(千分比)、最后4位(萬分比)等
- 實現(xiàn)方式:id取模,例如
id % 100 < 灰度百分比,則命中灰度 - 特點:簡單,適用于絕大部分技術(shù)優(yōu)化場景
- 實現(xiàn)方式:id取模,例如
- 隨機灰度:取一部分隨機流量做灰度
- 實現(xiàn)方式:
ThreadLocalRandom.current().nextInt(100) < 灰度百分比 - 之所以使用ThreadLocalRandom、而不是Random,是為了避免多線程競爭用于生成隨機數(shù)的seed
- 實現(xiàn)方式:
- A/B實驗
- 實現(xiàn)方式:分層實驗、實驗數(shù)據(jù)收集、離線統(tǒng)計
- 特點:適用于小流量驗證新業(yè)務(wù)功能的效果,整體方案相對復(fù)雜,需要技術(shù)基建
id選取
- 業(yè)務(wù)id:如用戶id、商品id等
- 設(shè)備id:未注冊/未登錄用戶,此時沒有用戶id,只能取設(shè)備的唯一標識
下面重點介紹一下A/B實驗。
A/B實驗
目的
- 小流量驗證新業(yè)務(wù)功能,正向顯著則推至全量,否則繼續(xù)迭代優(yōu)化、或下線,避免功能過于臃腫
- 用數(shù)據(jù)作為依據(jù),避免想當(dāng)然、拍腦袋決策
分層實驗
主要目的是為了同時做多個實驗,而不是給每個實驗均分一部分流量。因為當(dāng)同時進行的實驗變多時,組合數(shù)量成倍增加,每個實驗分到的流量就很少了。
有這幾層結(jié)構(gòu):實驗層、實驗、分組
- 實驗層之間正交,可同時進行多個實驗層的實驗
- 同一實驗層的實驗之間互斥,比如命中了實驗1-1,就不會命中實驗1-2。實驗持有0到多個分桶,根據(jù)業(yè)務(wù)id可計算出桶號,進而知道命中哪個實驗
- 同一實驗內(nèi)有多個分組,包括1個對照組,和1到多個實驗組,只會命中其中一個分組。分組持有0到多個分桶,根據(jù)業(yè)務(wù)id可計算出桶號,進而知道命中哪個分組
實驗層、實驗舉例:
- 展示實驗層:根據(jù)頁面進行劃分,如首頁、搜索頁、推薦頁、詳情頁等。每個頁面作為一個實驗層,每個實驗層里可同時做多個展示實驗
- 算法實驗層:根據(jù)場景進行劃分,如相似推薦、搭配購?fù)扑]、個性化推薦、搜索排序、廣告排序等。每個場景作為一個實驗層,每個實驗層里可同時做多個算法實驗

哈希算法打散
要同時支持多個分層實驗,核心在于通過哈希算法將每一層的流量打散,用于實現(xiàn)“均勻分流”和“層間正交”,使得流量在各個實驗的效果正負抵消,才能得到真實的對比結(jié)果。
以下是計算實驗層桶號的代碼示例,實驗桶號同理:
import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;
public class ABTestRouter {
/**
* 根據(jù)用戶ID和實驗層ID(實驗層ID充當(dāng)鹽的角色),計算桶號 (0-99)
*/
public static int getBucket(String userId, String layerId) {
// 1. 拼接 Key: "layerId:userId"
String key = layerId + ":" + userId;
// 2. 使用 MurmurHash3 (32-bit)
// Guava 的 murmur3_32_fixed 是線程安全的
int hash = Hashing.murmur3_32_fixed()
.hashString(key, StandardCharsets.UTF_8)
.asInt();
// 3. 取模并確保結(jié)果為正數(shù)
// Math.abs(Integer.MIN_VALUE) 會返回負數(shù),所以推薦使用位運算去除符號位
return (hash & Integer.MAX_VALUE) % 100;
}
public static void main(String[] args) {
String uid = "user_123456";
// 不同層的流量是正交的(打散重新分配)
System.out.println("展示層桶號: " + getBucket(uid, "layer_ui"));
System.out.println("算法層桶號: " + getBucket(uid, "layer_algo"));
}
}
之所以用murmurhash,而非md5,是因為md5是加密算法,計算開銷更大,在AB實驗中僅需均勻打散即可,無需擔(dān)心根據(jù)哈希結(jié)果反推原文。
之所以把實驗層id作為鹽,是因為微小的輸入差異都會導(dǎo)致哈希結(jié)果相差巨大,實現(xiàn)打散的效果。
實驗數(shù)據(jù)收集
實驗數(shù)據(jù)收集流程如下:
- 在AB實驗管理系統(tǒng)中配置實驗信息:如實驗鹽值、桶號與實驗組的映射關(guān)系等,可動態(tài)修改
- 代碼邏輯開發(fā):
- 引入實驗sdk,sdk在啟動、或配置變更時拉取實驗信息,本地計算業(yè)務(wù)id的桶號,進而得到命中的分組
- 對照組做當(dāng)前邏輯,實驗組1做邏輯1,實驗組2做邏輯2
- 在正式開始AB實驗之前,先做AA分桶實驗,模擬實驗組、對照組的結(jié)果,判斷是否均勻,避免分桶不均勻帶來錯誤的實驗結(jié)果
- 實驗開始,后端埋點:sdk發(fā)出后端埋點消息
- 消息格式舉例:
業(yè)務(wù)id, 實驗層id, 實驗id, 分組id, 桶號, 觸發(fā)時間
- 消息格式舉例:
- 實驗過程:實驗持續(xù)時間至少一周,覆蓋工作日、周末/假期,避免受時間周期帶來的波動影響
- 離線統(tǒng)計實驗效果:
- 后端埋點數(shù)據(jù)導(dǎo)入曝光事件hive表
- 業(yè)務(wù)DB數(shù)據(jù)導(dǎo)入行為事件hive表,如注冊、登錄、瀏覽、點擊、收藏、加購、下單、支付等,取決于實驗關(guān)注的業(yè)務(wù)指標
- 把曝光事件、行為事件join起來,對比實驗組、對照組的業(yè)務(wù)指標差異

以下是sql示例,代表從實驗曝光后24小時內(nèi)各個分組的轉(zhuǎn)化率對比。
SELECT
e.group_id,
COUNT(DISTINCT e.user_id) as exposed_users,
COUNT(DISTINCT a.user_id) as converted_users,
COUNT(DISTINCT a.user_id) / COUNT(DISTINCT e.user_id) as conversion_rate
FROM exposure_events e
LEFT JOIN action_events a ON e.user_id = a.user_id
AND a.event_time BETWEEN e.event_time AND (e.event_time + INTERVAL 24 HOUR)
WHERE e.experiment_id = 'ui_test_001'
GROUP BY e.group_id;
實驗報表分析
評估實驗結(jié)果是否正向、是否顯著。了解統(tǒng)計學(xué)里的核心概念,能看懂實驗報表即可。
p值
用來衡量實驗結(jié)果是否顯著,p值的含義是:假設(shè)實驗組與對照組沒有區(qū)別,此時觀察到實驗有差異的概率。一般要求 p < 0.05,也就是說實驗結(jié)果顯著的概率大于95%(1 - 0.05 = 95%)
置信區(qū)間
在顯著的前提下,用來衡量實驗結(jié)果是否正向,代表業(yè)務(wù)指標的可能范圍分布。
比如:實驗結(jié)果里業(yè)務(wù)指標提升了1%,95%置信區(qū)間在[0.8%, 1.2%],則代表有95%的把握可以把業(yè)務(wù)指標提升至少0.8%、至多1.2%,效果正向。如果置信區(qū)間的下界是負數(shù),就有可能是負向效果了,需要警惕。
以上就是灰度方案的總結(jié)了,歡迎討論交流。