摘要: 想要同一時間作N個實驗?想要同一份流量不一樣實驗之間不干擾?想要每一個實驗都能獲得100%流量? 那麼你就須要分層實驗。java
想要同一時間作N個實驗?git
想要同一份流量不一樣實驗之間不干擾?算法
想要每一個實驗都能獲得100%流量?cookie
那麼你就須要分層實驗。
app
分層實驗概念:每一個獨立實驗爲一層,層與層之間流量是正交的。
簡單來說,就是一份流量穿越每層實驗時,都會再次隨機打散,且隨機效果離散。
全部分層實驗的奠定石--Goolge論文dom
《Overlapping Experiment Infrastructure More, Better, Faster Experimentation》
下面將以一個簡單例子來解釋分層實驗覈心原理,若是要了解全貌,能夠看一下上面論文
首先來看一下MD5的做爲hash的特色,本文以最簡單得MD5算法來介紹分層實驗。(但必定要知道,實際應用場景複雜,須要咱們設計更復雜的hash算法)spa
壓縮性:任意長度的數據,算出的MD5值長度都是固定的。設計
容易計算:從原數據計算出MD5值很容易。code
抗修改性:對原數據進行任何改動,哪怕只修改1個字節,所獲得的MD5值都有很大區別。(重要理論依據!)blog
弱抗碰撞:已知原數據和其MD5值,想找到一個具備相同MD5值的數據(即僞造數據)是很是困難的。
強抗碰撞:想找到兩個不一樣的數據,使它們具備相同的MD5值,是很是困難的。
正是因爲上面的特性,MD5也常常做爲文件是否被篡改的校驗方式。
因此,
理論上,若是咱們採用MD5計算hash值,對每一個cookie 加上某固定字符串(離散因子),求餘的結果,就會與不加產生很大區別。加上離散因子後,當數據樣本夠大的時候,基於機率來看,全部cookie的分桶就會被再次隨機化。
下面咱們將經過實際程序來驗證。
使用java SecureRandom模擬cookie的獲取(隨機化cookie,模擬真實場景)
hash算法選用上文介紹的MD5。實驗分兩種:對cookie不作任何處理;對cookie採用增長離散因子離散化
一共三層實驗(也就是3個實驗),咱們會觀察第一層2號桶流量在第2層的分配,以及第2層2號桶流量在第3層的分配
若是cookie加入離散因子後,一份流量通過三個實驗,按照以下圖比例每層平均打散,則證實實驗流量正交
從上圖能夠看出,即便第1層的2號桶的實驗結果比其餘幾個桶效果好不少,因爲流量被離散化,這些效果被均勻分配到第2層。(第3層及後面層類同),這樣雖然實驗效果被帶到了下一層,可是每一個桶都獲得了相同的影響,對於層內的桶與桶的對比來講,是沒有影響的。而咱們分析實驗數據,偏偏只會針對同一實驗內部的基準桶和實驗桶。
=>與原來實驗方式區別?
傳統方式,咱們採用將100%流量分紅不一樣的桶,假設有A,B兩我的作實驗,爲了讓他們互不影響,只能約定0-3號桶給A作實驗,4-10號桶給B作實驗的方式,這樣作實驗,每一個人拿到的只是總流量的一部分。
上面基於MD5分層的例子告訴咱們,分層實驗能夠實現實驗與實驗之間「互不影響」,這樣咱們就能夠把100%流量給A作實驗,同時這100%流量也給B作實驗。(這裏的A,B舉例來講,一個請求,頁面作了改版(實驗A)、處理邏輯中調用了算法,而算法也作了調整(實驗B)),若是採用不採用分層方式,強行將100%流量穿過A,B,那麼最終看實驗報表時,咱們沒法區分,是因爲改版致使轉化率提升,仍是算法調整的好,致使轉化率提升。
package com.yiche.library; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; /** * @author shihongxing * @since 2018-09-04 17:25 */ 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"); } }
由於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層實驗的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層和第3層流量均來源於第一層的2號桶。
因此得出結論,第一層的流量在第2層、第3層均獲得從新的離散分配。
隨着個性化和算法不斷引入咱們的應用,同一時間作多個實驗需求愈來愈多,更多人開始使用分層實驗。
實際使用中,業務場景複雜,咱們會面臨須要設計更復雜的hash算法的狀況,MD5是一種相對容易,效果也不錯的方式。有興趣能夠關注大質數素數hash算法等更加精密優良的算法。同時,分層實驗中,爲了防止流量影響,還會有「流量隔離」等更復雜的概念。