我們在進行灰度發布時,往往需要轉發一部分流量到新上線的服務上,進行小規模的驗證,隨著功能的不斷完善,我們也會逐漸增加轉發的流量,這就需要按比例去切分流量,那么如何實現流量切分呢?
我們很容易想到通過生成隨機數方式進行實現,通過判斷生成隨機數是否落在指定區間內,從而決定是否進行流量的轉發,這種方式雖然實現很簡單,但是它有兩點弊端:
- 每次都要生成新的隨機數,這是有性能損耗的,尤其是并發量高的場景下更為明顯;
- 隨機數的生成往往不夠均勻,比如有A、B兩個服務,流量比例3:7,如果使用隨機數方式,如果運氣不好的話有可能請求100次全落在B服務上。
那有沒有性能開銷又小,又能精準切分流量的方式呢?當然是有的。實現思路如下:
確定比例,并根據比例得到一個基數base,例如比例是3:7,那么基數就是10;
生成長度為基數base的數組source,并填充數據0、1、2、3、4、5…; 打亂數組source中元素順序;
創建全局計數器queryCount,每次有請求時加1(確保原子性);
計算計數器queryCount與base取余后的值rate,并得到數組中對應位置的值source[rate];
判斷source[rate]落在哪個區間。
看文字可能覺得理解起來有些別扭,這里貼上完整代碼:
import ( "fmt" "math/rand" "sync/atomic")
type TrafficControl struct { source []int queryCount uint32 base int ratio int
}
func NewTrafficControl(base int, ratio int) *TrafficControl { source := make([]int, base) for i := 0; i < base; i++ { source[i] = i }rand.Shuffle(base, func(i, j int) { source[i], source[j] = source[j], source[i] })return &TrafficControl{ source: source, base: base, ratio: ratio, }
}func (t *TrafficControl) Allow() bool { rate := t.source[int(atomic.AddUint32(&t.queryCount, 1))%t.base] if rate < t.ratio { return true } else { return false }
}
接下來我們檢測下這段代碼是否真的能精準切分流量:
func main() { trafficCtl := NewTrafficControl(10, 6) cnt := 100 serviceAQueryCnt := 0 serviceBQueryCnt := 0 for cnt > 0 { if trafficCtl.Allow() { serviceAQueryCnt++ } else { serviceBQueryCnt++ } cnt-- }fmt.Printf("service A query count: %v, service B query count %v", serviceAQueryCnt, serviceBQueryCnt)
}
執行結果如下:
service A query count: 60, service B query count 40
其實思路很簡單:通過請求數與基數取余,確保在一定范圍內總能按比例 實現流量切分;通過打亂數組確保流量分布盡可能均勻