Java開發者如何在tengine

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

1. 問題定位:

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

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

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

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

2. 問題解決:

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

Java開發者如何在tengine/nginx層做ABtest

虛線是新版的訪問路徑,對於γ類型,如果要做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:

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、連數據庫、拼裝頁面等等。

關注我:私信回覆“555”獲取往期Java高級架構資料、源碼、筆記、視頻Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術往期架構視頻截圖


分享到:


相關文章: