背景
想要同一時間做N個實驗? 想要同一份流量不同實驗之間不干擾? 想要每個實驗都能得到100%流量? 那么你就需要分層實驗。
1.1 什么是分層實驗 分層實驗概念:每個獨立實驗為一層,層與層之間流量是正交的。 簡單來講,就是一份流量穿越每層實驗時,都會再次隨機打散,且隨機效果離散。 所有分層實驗的奠基石–Goolge論文 《Overlapping Experiment Infrastructure More, Better, Faster Experimentation》 下面將以一個簡單例子來解釋分層實驗核心原理,如果要了解全貌,可以看一下上面論文 首先來看一下MD5的作為hash的特點,本文以最簡單得MD5算法來介紹分層實驗。(但一定要知道,實際應用場景復雜,需要我們設計更復雜的hash算法) 1.2 MD5 特點 壓縮性:任意長度的數據,算出的MD5值長度都是固定的。 容易計算:從原數據計算出MD5值很容易。 抗修改性:對原數據進行任何改動,哪怕只修改1個字節(jié),所得到的MD5值都有很大區(qū)別。(重要理論依據!) 弱抗碰撞:已知原數據和其MD5值,想找到一個具有相同MD5值的數據(即偽造數據)是非常困難的。 強抗碰撞:想找到兩個不同的數據,使它們具有相同的MD5值,是非常困難的。 正是由于上面的特性,MD5也經常作為文件是否被篡改的校驗方式。 所以, 理論上,如果我們采用MD5計算hash值,對每個cookie 加上某固定字符串(離散因子),求余的結果,就會與不加產生很大區(qū)別。加上離散因子后,當數據樣本夠大的時候,基于概率來看,所有cookie的分桶就會被再次隨機化。 下面我們將通過實際程序來驗證。 實戰(zhàn)講解
2.1 我們的程序介紹 使用java SecureRandom模擬cookie的獲取(隨機化cookie,模擬真實場景) hash算法選用上文介紹的MD5。實驗分兩種:對cookie不做任何處理;對cookie采用增加離散因子離散化 一共三層實驗(也就是3個實驗),我們會觀察第一層2號桶流量在第2層的分配,以及第2層2號桶流量在第3層的分配 如果cookie加入離散因子后,一份流量經過三個實驗,按照如下圖比例每層平均打散,則證明實驗流量正交
從上圖可以看出,即使第1層的2號桶的實驗結果比其他幾個桶效果好很多,由于流量被離散化,這些效果被均勻分配到第2層。(第3層及后面層類同),這樣雖然實驗效果被帶到了下一層,但是每個桶都得到了相同的影響,對于層內的桶與桶的對比來說,是沒有影響的。而我們分析實驗數據,恰恰只會針對同一實驗內部的基準桶和實驗桶。 =>與原來實驗方式區(qū)別? 傳統方式,我們采用將100%流量分成不同的桶,假設有A,B兩個人做實驗,為了讓他們互不影響,只能約定0-3號桶給A做實驗,4-10號桶給B做實驗的方式,這樣做實驗,每個人拿到的只是總流量的一部分。 上面基于MD5分層的例子告訴我們,分層實驗可以實現實驗與實驗之間“互不影響”,這樣我們就可以把100%流量給A做實驗,同時這100%流量也給B做實驗。(這里的A,B舉例來說,一個請求,頁面做了改版(實驗A)、處理邏輯中調用了算法,而算法也做了調整(實驗B)),如果采用不采用分層方式,強行將100%流量穿過A,B,那么最終看實驗報表時,我們無法區(qū)分,是由于改版導致轉化率提高,還是算法調整的好,導致轉化率提高。 2.2 代碼 import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.util.ArrayList;import java.util.List;/** * @author 九德 */public class MultiLayerExperiment { private static String byteArrayToHex(byte[] byteArray) { char[] hexDigits = {‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’, ‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’}; char[] resultCharArray = new char[byteArray.length * 2]; int index = 0; for (byte b : byteArray) { resultCharArray[index++] = hexDigits[b >>> 4 & 0xf]; resultCharArray[index++] = hexDigits[b & 0xf]; } return new String(resultCharArray); } private static long splitBucket(MessageDigest md5,long val,String shuffle){ String key = String.valueOf(val) +((shuffle==null)?”“:shuffle); byte[] ret = md5.digest(key.getBytes()); String s = byteArrayToHex(ret) ; long hash =Long.parseUnsignedLong(s.substring(s.length()-16,s.length()-1),16); if(hash < 0){ hash = hash * (-1); } return hash ; } private static void exp(SecureRandom sr,MessageDigest md5, final int LevelOneBucketNumm,/第一層實驗桶數/ final int LevelTwoBucketNumm,/第二層實驗桶數/ final int LevelThreeBucketNumm,/第三層實驗桶數/ final int AllFlows,/所有流量數/ String shuffleLevel1,/第一層實驗離散因子/ String shuffleLevel2,/第二層實驗離散因子/ String shuffleLevel3/第三層實驗離散因子/ ){ System.out.println("==第1層實驗 start!==");
int[] bucketlevel1 = new int[LevelOneBucketNumm];
for (int i=0; i<LevelOneBucketNumm; i++) {
bucketlevel1[i] = 0;
}
List<Integer> level1bucket2 = new ArrayList<Integer>();
for(int i=0; i<AllFlows; i++)
{
int cookie = sr.nextInt();
long hashValue = splitBucket(md5, cookie, shuffleLevel1);
int bucket =(int) (hashValue % LevelOneBucketNumm);
if(bucket == 2){
/*將2號桶的流量記錄下來*/
level1bucket2.add(cookie);
}
bucketlevel1[bucket]++;
}
for(int i=0; i<LevelOneBucketNumm; i++){
System.out.println("1層" + i + "桶:" + bucketlevel1[i]);
}
System.out.println("==第1層實驗 end!==");
System.out.println("==第1層2號桶流量到達第2層實驗 start!==");
int[] bucketlevel2 = new int[LevelTwoBucketNumm];
for (int i=0; i<LevelTwoBucketNumm; ++i) {
bucketlevel2[i] = 0;
}
List<Integer> level2bucket2 = new ArrayList<Integer>();
for(int cookie : level1bucket2) {
long hashValue = splitBucket(md5, cookie, shuffleLevel2);
int bucket =(int) (hashValue % LevelTwoBucketNumm);
if(bucket == 2){
/*將第2層2號桶的流量記錄下來*/
level2bucket2.add(cookie);
}
bucketlevel2[bucket]++;
}
for(int i=0; i<LevelTwoBucketNumm; i++){
System.out.println("2層" + i + "桶:" + bucketlevel2[i]);
}
System.out.println("==第1層2號桶流量到達第2層實驗 end!==");
System.out.println("==第2層2號桶流量到達第3層實驗 start!==");
int[] bucketlevel3 = new int[LevelThreeBucketNumm];
for (int i=0; i<LevelThreeBucketNumm; ++i) {
bucketlevel3[i] = 0;
}
for(int cookie : level2bucket2) {
long hashValue = splitBucket(md5, cookie, shuffleLevel3);
int bucket =(int) (hashValue % LevelThreeBucketNumm);
bucketlevel3[bucket]++;
}
for(int i=0; i<LevelThreeBucketNumm; i++){
System.out.println("3層" + i + "桶:" + bucketlevel3[i]);
}
System.out.println("==第2層2號桶流量到達第3層實驗 end!==");
}
public static void main(String[] args) throws NoSuchAlgorithmException {
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");/*用來生成隨機數*/
MessageDigest md5 = MessageDigest.getInstance("MD5");/*用來生成MD5值*/
/*1. 不對cookie做處理,一個cookie在每層實驗分到的桶是一致的*/
exp(sr,md5,5,5,5,1000000,null,null,null);
System.out.println("=======================");
/*2. 每層加一個離散因子,這里只是簡單的a,b,c,就可以將多層了流量打散*/
exp(sr,md5,5,5,5,1000000,"a","b","c");
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
} 2.3 結果分析(重點) 2.3.1 不對cookie處理,每層實驗的分桶號一樣 因為hash%5中的hash保持不變,無論哪層,所以流量一直處于2號桶。 ==第1層實驗 start!== 1層0桶:199698 1層1桶:199874 1層2桶:199989 1層3桶:200711 1層4桶:199728 ==第1層實驗 end!== ==第1層2號桶流量到達第2層實驗 start!== 2層0桶:0 2層1桶:0 2層2桶:199989 2層3桶:0 2層4桶:0 ===第1層2號桶流量到達第2層實驗 end!== ===第2層2號桶流量到達第3層實驗 start!== 3層0桶:0 3層1桶:0 3層2桶:199989 3層3桶:0 3層4桶:0 ===第2層2號桶流量到達第3層實驗 end!== 2.3.2. 對cookie做離散處理后,每層流量均勻分配 如下所示, 流量到達第一層時,流量被均勻分配 第2層實驗的2號桶流量到達第3層時,流量均勻分配到第2層的5個桶。 第2層實驗的2號桶流量到達第3層時,流量均勻分配到第3層的5個桶。 ==第1層實驗 start!== 1層0桶:199951 1層1桶:199536 1層2桶:200127 1層3桶:200938 1層4桶:199448 ==第1層實驗 end!== ==第1層2號桶流量到達第2層實驗 start!== 2層0桶:40122 2層1桶:40080 2層2桶:39881 2層3桶:40096 2層4桶:39948 ===第1層2號桶流量到達第2層實驗 end!== ===第2層2號桶流量到達第3層實驗 start!== 3層0桶:8043 3層1桶:7971 3層2桶:7823 3層3桶:7956 3層4桶:8088 ===第2層2號桶流量到達第3層實驗 end!== 2.4 結論 我們觀測的第2層和第3層流量均來源于第一層的2號桶。 所以得出結論,第一層的流量在第2層、第3層均得到重新的離散分配。 總結 隨著個性化和算法不斷引入我們的應用,同一時間做多個實驗需求越來越多,更多人開始使用分層實驗
|