如何在tengine/nginx層做ABtest

JavaDog發表於2019-01-15

我們在做業務專案需求的時候經常會做ABtest,在釋出時也會做灰度釋出,通常這些ABtest都是在同一應用上做的,即我們在A應用上開發新版的程式碼,並通過程式碼控制分桶和打點。但是我們也經常遇到這種情況:新版實驗與老版不在同一個應用上,那麼之前的方案就無法做切流了。

1. 問題定位:

- 我們希望 分流解決方案將一部分流量分到新應用,另一部分流量分到老應用,且該流量是可以控制的

- 我們希望新老版本有明顯的標識來區分使用者命中的是新版還是老版(即打點)

- 假如“我們嚴謹的PD們”想要看AB對比資料,我們還要比較方便的從報表分割槽新老版命中

上述三條其實基本構成了一個簡易的AB系統,類似我們常用的buckettest、BTS等,當然 BTS此類實驗平臺還有一個比較完善的控制檯來控制切流和報表彙總。

2. 問題解決:

一般此類跨應用切流都會有類似的應用依賴訪問結構:

screenshot.png

虛線是新版的訪問路徑,對於γ型別,如果要做ABtest,需要在上層vip/lvs層做,過於複雜,因此可以轉化成β的結構,或者將B中的 新老應用層級交換一下。對於β形態,我們完全可以將老應用A 當成α形態中的老應用,因此我們只需對α形態進行討論。

1)思路一:通過釋出批次控制切流節奏

這是我們做業務頁面遷移時比較常用的方法,即在應用M層修改 反向代理邏輯,使請求轉發到新應用B,並通過釋出的批數來控制切流節奏。

優點: 修改方便,只需釋出一次M,修改出錯成本低;
缺點: 無法控制使用者訪問新版老版,只能由應用M的lvs或VIPServer的負載均衡做隨機分流,如果遇到流量不均衡問題,切流會十分不均衡。業務效果無法對比,因為使用者會時而刷出新版,時而刷出老版。釋出週期長,需要長時間佔用釋出流程。

上述方案一般用於 只遷移,不做業務資料對比的技術改造升級專案。

2)思路二:在應用M層的tengine/nginx層做分流

優點: 分流策略可以根據cookie、ip、ua等靈活配置,可以比較精確的控制流量分佈;
缺點: 需要至少釋出兩次,配置較為複雜,容易搞出問題

如果是僅僅進行技術遷移,一般用方案一即可,如果遇到需要精確流量控制或者需要準確的技術和業務資料對比那方案二無疑是比較好的

那麼我們就研究一下如何在tengine裡面做切流吧:

3. 在tengine/nginx 層做AB test

1)分流器設計:

使用 if語句做分流器:

例如我們對/abc/ path下的請求,cookie中含有version=1的轉發到老應用,對version=2的轉發到新應用:

  set $version "default";
    if ($http_cookie ~* "version=1") {
        set $version v1;
    }

    if ($http_cookie ~* "version=2") {
        set $version v2;
    }

  location /abc/ {
    if ($version = v1) {
        proxy_pass http://A_APP;
    }
    if ($versuib = v2) {
        proxy_pass http://B_APP;
    }
  ......
  }
複製程式碼

使用 map做分流器:

例如我們對/abc/ path下的請求,cookie中含有version=1的轉發到老應用,對version=2的轉發到新應用:

 map $COOKIE_version $version {
    1 v1;
    2 v2;
    default default;
 }
 location /abc/ {
    if ($version = v1) {
        proxy_pass http://A_APP;
    }
    if ($versuib = v2) {
        proxy_pass http://B_APP;
    }
    ...
  }
複製程式碼

注: $COOKIE_version 是nginx的語法,指獲取cookie中key=version的值

使用split_clients 方法:

##下面在http 塊中
split_clients "$COOKIE_cna" $appversion {
        50%     v1;
        *       v2;
    }

##下面在server塊中
location /abc/ {
    if ($version = v1) {
        proxy_pass http://A_APP;
    }
    if ($versuib = v2) {
        proxy_pass http://B_APP;
    }
    ...
 }
複製程式碼

注:cna是我們常用的cookie分流的值,每一個使用者的cna是一樣的,保證能按照cookie進行分流

使用lua 編寫分流指令碼:

      init_by_lua '  
               mmh2 = require "murmurhash2"  
      '; 

      location /abc/ {
        set $version "default";

        set_by_lua '
            local cna = ngx.var.cookie_cna;
            local hash_code = mmh2(cna) % 100;
            if hash_code >= 50 then
                ngx.var.version = v1;
            else 
                ngx.var.version = v2;
            end
        ';

        if ($version = v1) {
            proxy_pass http://A_APP;
        }
        if ($versuib = v2) {
            proxy_pass http://B_APP;
        }
        ...
      }
複製程式碼

注:mmh2 = require "murmurhash2" 為引入第三方hash函式:murmurhash2;

處理第一次請求時無cookie情況:

按照慣例,第一次無cookie的情況會隨機一個數來進行分流,第二次來訪問時再根據cookie進行重新分流,雖然會導致有1/2的概率會導致使用者第一次訪問和第二次不一致,但是由於我們的業務第一次無cookie訪問的使用者大部分是新使用者,有超過60%的使用者沒有第二次訪問,因此這個比例是比較小的。

如果要做到絕對的精確分流,就要對無cookie的使用者增加一個cookie來標示其所屬的桶。兩種方法分別對應:

set_by_lua '
    local cna = ngx.var.cookie_cna;

    if cna == '' or cna == nil then
        math.randomseed(1);
nvx.var.cookie_cna= math.random(0,100);
    end
  ';
複製程式碼

@@需要進行精確分流的方法:

set $random_num 101;
set_by_lua '
    local cna = ngx.var.cookie_cna;

    if cna == '' or cna == nil then
        math.randomseed(1);
 nvx.var.random_num = math.random(0,100);
    end
  ';
if ($random_num != 101) {
    add_header Set-Cookie "random_num=$random_num;";
}

## 在後續的判斷中首先根據random_num進行分流,再根據cna進行分流
複製程式碼

2)分流比例控制:

由於上面1) 中的預設都是設定的50%比例切流,如果“我們可愛的PD”要求2:8分咋整?要麼我們改一下上面的比例重新發布一下,要麼引入實時干預的某個東西。當然重新發布對於我們懶惰的程式設計師來說是無法忍受的。還好tengine支援訪問diamond和tair:

參考tengine使用文件

http {
    diamond_server jmenv.tbsite.net:8080;    
    diamond_app group dataid $content $version;

    split_clients "$COOKIE_cna" $appversion {
        $content     v1;
        *       v2;
    }
...
}
複製程式碼

注: 上面程式碼未經線上測試,如要使用,請自行測試驗證。content變數就是從diamond裡面讀取到的設定的ratio啦,可以設定為0%,10%,50%等等。

3)分流打點與資料檢視:

只分流不打點,業務資料沒法看啊,所以我們得想辦法把新版和老版本區分開。我們可以在nginx裡面搞定或者在新老版本的應用裡面 打上對應的業務點位。在我們的實踐中,是採用的第二種方案,是為了延續之前做BT的引數,使用resion_trace 欄位中的cid打點。你也可以在新版中加個cookie: bts_v = 1, = 2, 然後在日誌報表中 撈對應的cookie來判斷。

總結

通過上述的三點,我們可以搭建起一套比較完整的切流、動態調整分流比例、檢視業務資料 的跨應用切流方案了。

有人會問,在一個應用內的AB test可以在tengine/nginx 層做嗎?我的建議是:小夥子,不要花樣作死,nginx一不小心就會改掛的哦。

如果你想使用nginx+lua的 超強併發能力,或者對nginx、lua有很深的造詣,或者你想寫一堆程式碼併成為“不可替代的程式設計師”,可以嘗試在tengine/nginx 層做很多業務邏輯,如AB test、連資料庫、拼裝頁面等等。

參考文獻:

如何在tengine/nginx層做ABtest


相關文章