When we are conducting grayscale releases, we often need to forward some traffic to newly launched services and conduct small-scale verification. As the functions continue to be improved, we will gradually increase the forwarded traffic, which requires the traffic splitting in proportion. So how to achieve traffic splitting?
It is easy to think of implementing it by generating random numbers, and by judging whether the generated random numbers fall within a specified interval, thereby deciding whether to forward traffic. Although this method is simple to implement, it has two disadvantages:
- A new random number must be generated every time, which is due to performance losses, especially in scenarios with high concurrency;
- The generation of random numbers is often not uniform enough. For example, there are two services A and B, with a traffic ratio of 3:7. If you use the random number method, if you are not lucky, you may request 100 times and all of them fall on the B service.
So is there a way to have small performance overhead and accurately dividing traffic? Of course there are. The implementation idea is as follows:
- Determine the ratio and obtain a base base based on the ratio. For example, the ratio is 3:7, then the base is 10;
- Generate an array source with a length of radix base and fill in data 0, 1, 2, 3, 4, 5...;
- Chaos the order of elements in the array source;
- Create a global counter queryCount, adding 1 every time there is a request (ensure atomicity);
- Calculate the remaining value rate of the counter queryCount and base, and get the corresponding position value source[rate] in the array;
- Determine which range source[rate] falls in.
It may feel a bit awkward to understand the text, so here is the complete code:
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 } (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 := [int(atomic.AddUint32(&, 1))%] if rate < { return true } else { return false } }
Next, let's check whether this code can really accurately divide the traffic:
func main() { trafficCtl := NewTrafficControl(10, 6) cnt := 100 serviceAQueryCnt := 0 serviceBQueryCnt := 0 for cnt > 0 { if () { serviceAQueryCnt++ } else { serviceBQueryCnt++ } cnt-- } ("service A query count: %v, service B query count %v", serviceAQueryCnt, serviceBQueryCnt) }
The execution results are as follows:
service A query count: 60, service B query count 40
In fact, the idea is very simple: by taking the remaining number of requests and the base, we ensure that the traffic can always be divided proportionally within a certain range; by disrupting the array, we ensure that the traffic distribution is as uniform as possible. Of course, there are other ways to implement traffic slicing. If you have a more sophisticated implementation, please leave a message in the comment area.
This is the end of this article about Golang's example of achieving proportional traffic splitting. For more relevant content on Golang's proportional traffic splitting, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!