10分鍾搞懂分層實驗原理


摘要: 想要同一時間做N個實驗?想要同一份流量不同實驗之間不干擾?想要每個實驗都能得到100%流量? 那么你就需要分層實驗。

1. 背景

  • 想要同一時間做N個實驗?

  • 想要同一份流量不同實驗之間不干擾?

  • 想要每個實驗都能得到100%流量?

那么你就需要分層實驗。

 

1.1 什么是分層實驗

 

分層實驗概念:每個獨立實驗為一層,層與層之間流量是正交的。
簡單來講,就是一份流量穿越每層實驗時,都會再次隨機打散,且隨機效果離散。

所有分層實驗的奠基石--Goolge論文

《Overlapping Experiment Infrastructure More, Better, Faster Experimentation》

下面將以一個簡單例子來解釋分層實驗核心原理,如果要了解全貌,可以看一下上面論文
首先來看一下MD5的作為hash的特點,本文以最簡單得MD5算法來介紹分層實驗。(但一定要知道,實際應用場景復雜,需要我們設計更復雜的hash算法)

1.2 MD5 特點

  • 壓縮性:任意長度的數據,算出的MD5值長度都是固定的。

  • 容易計算:從原數據計算出MD5值很容易。

  • 抗修改性:對原數據進行任何改動,哪怕只修改1個字節,所得到的MD5值都有很大區別。(重要理論依據!)

  • 弱抗碰撞:已知原數據和其MD5值,想找到一個具有相同MD5值的數據(即偽造數據)是非常困難的。

  • 強抗碰撞:想找到兩個不同的數據,使它們具有相同的MD5值,是非常困難的。


正是由於上面的特性,MD5也經常作為文件是否被篡改的校驗方式。
所以,
理論上,如果我們采用MD5計算hash值,對每個cookie 加上某固定字符串(離散因子),求余的結果,就會與不加產生很大區別。加上離散因子后,當數據樣本夠大的時候,基於概率來看,所有cookie的分桶就會被再次隨機化。
下面我們將通過實際程序來驗證。

2. 實戰講解

2.1 我們的程序介紹

    • 使用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");
          }
      }

      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層均得到重新的離散分配。

        3. 總結

        • 隨着個性化和算法不斷引入我們的應用,同一時間做多個實驗需求越來越多,更多人開始使用分層實驗。

        • 實際使用中,業務場景復雜,我們會面臨需要設計更復雜的hash算法的情況,MD5是一種相對容易,效果也不錯的方式。有興趣可以關注大質數素數hash算法等更加精密優良的算法。同時,分層實驗中,為了防止流量影響,還會有“流量隔離”等更復雜的概念。

         


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM