Golang之 go实现交替加权轮询-interleaved weighted round robin

Posted by 董江 on Thursday, August 31, 2023

golang 实现交替加权轮询-interleaved weighted round robin

背景

背景实现: O(1) 时间复杂度O(n) 空间复杂度 的 加权负载平衡实现

交错式加权轮询负载均衡算法 interleaved weighted round robin (iwrr) 是 时间复杂度O(1),空间复杂度O(n),相比于VNSWRRO(sigma(W))更省空间且可控

算法如下: https://en.wikipedia.org/wiki/Weighted_round_robin#Interleaved_WRR

iwrr

除了性能提升外,也解决小请求、小queue中的请求长期饥饿的问题;

实现说明

与Linux O(1)调度器思路相同:

  1. 实现中维护两个队列,分别是当前队列current queue下一次队列next queue。队列中的每个元素entry都维护weight表示在当前队列中任选中次数权重.

  2. 最初时weightentry权重相同,每次被选中后都会减少step(使用GCD公约数来调整个周期)。

  3. weight0时表示当前周期内不可调度,将其放在下一次队列中。当当前队列current queueempty时, 表示当前周期所有元素entry都被按权重选择结束,此时对换当前队列下一次队列开始新的周期。

  4. 由于始终重复用链表节点,因此该负载均衡算法在初始化后实现可以保证是zero-copy的。

实现

package iwrr

import (
	"math/rand"
	"sync"

	"dubbo.apache.org/dubbo-go/v3/cluster/loadbalance"
	"dubbo.apache.org/dubbo-go/v3/protocol"
)

type iwrrEntry struct {
	weight  int64
	invoker protocol.Invoker

	next *iwrrEntry
}

type iwrrQueue struct {
	head *iwrrEntry
	tail *iwrrEntry
}

func NewIwrrQueue() *iwrrQueue {
	return &iwrrQueue{}
}

func (item *iwrrQueue) push(entry *iwrrEntry) {
	entry.next = nil
	tail := item.tail
	item.tail = entry
	if tail == nil {
		item.head = entry
	} else {
		tail.next = entry
	}
}

func (item *iwrrQueue) pop() *iwrrEntry {
	head := item.head
	next := head.next
	head.next = nil
	item.head = next
	if next == nil {
		item.tail = nil
	}
	return head
}

func (item *iwrrQueue) empty() bool {
	return item.head == nil
}

// InterleavedweightedRoundRobin struct
type interleavedweightedRoundRobin struct {
	current *iwrrQueue
	next    *iwrrQueue
	step    int64
	mu      sync.Mutex
}

func NewInterleavedweightedRoundRobin(invokers []protocol.Invoker, invocation protocol.Invocation) *interleavedweightedRoundRobin {
	iwrrp := new(interleavedweightedRoundRobin)
	iwrrp.current = NewIwrrQueue()
	iwrrp.next = NewIwrrQueue()

	size := uint64(len(invokers))
	offset := rand.Uint64() % size
	step := int64(0)
	for idx := uint64(0); idx < size; idx++ {
		invoker := invokers[(idx+offset)%size]
		weight := loadbalance.GetWeight(invoker, invocation)
		step = gcdInt(step, weight)
		iwrrp.current.push(&iwrrEntry{
			invoker: invoker,
			weight:  weight,
		})
	}
	iwrrp.step = step

	return iwrrp
}

func (iwrr *interleavedweightedRoundRobin) Pick(invocation protocol.Invocation) protocol.Invoker {
	iwrr.mu.Lock()
	defer iwrr.mu.Unlock()

	if iwrr.current.empty() {
		iwrr.current, iwrr.next = iwrr.next, iwrr.current
	}

	entry := iwrr.current.pop()
	entry.weight -= iwrr.step

	if entry.weight > 0 {
		iwrr.current.push(entry)
	} else {
		weight := loadbalance.GetWeight(entry.invoker, invocation)
		if weight < 0 {
			weight = 0
		}
		entry.weight = weight
		iwrr.next.push(entry)
	}

	return entry.invoker
}

func gcdInt(a, b int64) int64 {
	for b != 0 {
		a, b = b, a%b
	}
	return a
}

dubbo-go各loadbalance性能对比

goos: darwin
goarch: amd64
pkg: dubbo.apache.org/dubbo-go/v3/cluster/loadbalance
cpu: VirtualApple @ 2.50GHz
BenchmarkRoudrobinLoadbalace-8                       	    6739	    157254 ns/op	   88101 B/op	    2555 allocs/op
BenchmarkLeastativeLoadbalace-8                      	    7248	    153721 ns/op	   81877 B/op	    2043 allocs/op
BenchmarkConsistenthashingLoadbalace-8               	   57810	     20709 ns/op	    3425 B/op	     267 allocs/op
BenchmarkP2CLoadbalace-8                             	 2716834	     445.1 ns/op	     456 B/op	      10 allocs/op
BenchmarkInterleavedWeightedRoundRobinLoadbalace-8   	   20829	     57722 ns/op	   45296 B/op	    1027 allocs/op
BenchmarkRandomLoadbalace-8                          	   23924	     49694 ns/op	   38976 B/op	     767 allocs/op
PASS
ok  	dubbo.apache.org/dubbo-go/v3/cluster/loadbalance	10.348s

对比原生的wrr, iwrr的性能是wrr的3x+. 内存使用只有wrr不到一半

其他

「如果这篇文章对你有用,请随意打赏」

Kubeservice博客

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付