Kubernetes 负载均衡中 Golang 周期执行器的实现

前文我们学习了 Kubernetes 如何实现负载均衡,这次我们来看一下负载均衡中维护 proxy 周期执行器 BoundedFrequencyRunner 的实现细节,通过学习实现细节,我们可以看 Kubernetes 是如何写好一个监听循环的,这也是 go 编程中最优优势的协程和 channel 操作模型。

我们先通过一张图来快速了解一下 BoundedFrequencyRunner 的执行流程

BoundedFrequencyRunner

用于周期性的执行同步方法,并且提供了执行失败进行重试,内部封装了运行的限流器

go 复制代码
type BoundedFrequencyRunner struct {
	minInterval time.Duration // 运行之间的最小时间间隔,避免多次执行
	maxInterval time.Duration // 运行之间的最大时间间隔

	run chan struct{} // 触发运行的信号

	mu      sync.Mutex  // 保护对fn的运行和所有变更的互斥锁
	fn      func()      // 要运行的函数,有外部传入,Proxier传入的是syncLoop
	lastRun time.Time   // 上次运行的时间,可以用于计算下一次可能执行的时间
	timer   timer       // 延迟运行的计时器
	limiter rateLimiter // 运行的速度限流器

	// 重试这部分在代码里面并没有使用
	retry     chan struct{} // 重试信号量
	retryMu   sync.Mutex    // 保护 retryTime 的互斥锁
	retryTime time.Time     // 重试的时间
}

我们来看一下他的构造方法

go 复制代码
func NewBoundedFrequencyRunner(name string, fn func(), minInterval, maxInterval time.Duration, burstRuns int) *BoundedFrequencyRunner {
	// 这里的目的是保证timer已经准备好接收下一个信号
	// 如果实现没有调用Reset直接开始监听,有可能会多执行一次
	timer := &realTimer{timer: time.NewTimer(0)} 
	<-timer.C()                                  
	return construct(name, fn, minInterval, maxInterval, burstRuns, timer)
}

func construct(name string, fn func(), minInterval, maxInterval time.Duration, burstRuns int, timer timer) *BoundedFrequencyRunner {
	bfr := &BoundedFrequencyRunner{
		name:        name,
		fn:          fn,
		minInterval: minInterval,
		maxInterval: maxInterval,
		run:         make(chan struct{}, 1),
		retry:       make(chan struct{}, 1),
		timer:       timer,
	}
	
	if minInterval == 0 {
		// 没有设置最小间隔时间则不需要限流器
		bfr.limiter = nullLimiter{}
	} else {
		// 通过最小时间间隔来计算QPS,初始化限流器
		qps := float32(time.Second) / float32(minInterval)
		bfr.limiter = flowcontrol.NewTokenBucketRateLimiterWithClock(qps, burstRuns, timer)
	}
	return bfr
}

Loop 循环执行方法

流程图如下:

整个循环通过监听信号来启动运行方法

go 复制代码
func (bfr *BoundedFrequencyRunner) Loop(stop <-chan struct{}) {
	// 第一次启动的时候重置定时执行器
	bfr.timer.Reset(bfr.maxInterval)
	for {
		select {
		case <-stop: // 停止信号,则结束循环,这里其实也可以用context来做
			bfr.stop()
			return
		case <-bfr.timer.C(): // 到达定时器出发的时间
			bfr.tryRun()
		case <-bfr.run: // 主动写入了run信号进行处理
			bfr.tryRun()
		case <-bfr.retry: // 写入了重试信号进行重试 
			bfr.doRetry()
		}
	}
}

tryRun 真正执行方法,会尝试获取限流器,如果已经被限流不允许执行则重新计算下一次可能执行的时间

go 复制代码
func (bfr *BoundedFrequencyRunner) tryRun() {
	bfr.mu.Lock()
	defer bfr.mu.Unlock()

	// 限流器允许执行
	if bfr.limiter.TryAccept() {
		bfr.fn()
		bfr.lastRun = bfr.timer.Now()
		bfr.timer.Stop()
		bfr.timer.Reset(bfr.maxInterval)
		return
	}

	// 限流器没有token了,不允许执行,则计算下一次可能执行的时间
	elapsed := bfr.timer.Since(bfr.lastRun)   // 上一次运行的时间到这一次运行时间的间隔
	nextPossible := bfr.minInterval - elapsed // 通过最小间隔计算下一次可能执行的时间
	nextScheduled := bfr.timer.Remaining()    // 上一次设置的下一次执行的时间
	// 下一次可能执行的时间小于下一次调度的时间,则用可能执行的时间进行替换
	if nextPossible < nextScheduled {
		nextScheduled = nextPossible
	}
	
	// 重置下一次执行的时间
	bfr.timer.Stop()
	bfr.timer.Reset(nextScheduled)
}

Run 主动触发

run信号的写入,如果已经有数据在run channel中的话则会丢弃信号量,如果 Loop 循环没有被启动,同样不会立刻执行方法

go 复制代码
func (bfr *BoundedFrequencyRunner) Run() {
	select {
	case bfr.run <- struct{}{}:
	default:
	}
}

retry 在实现后并没有真正使用,这里不做分析

Timer 周期性触发

realTimer 主要用于控制周期性执行

go 复制代码
type realTimer struct {
	timer *time.Timer
	next  time.Time // 用作计时,可以返回下一次执行的时间
}

// 用于周期性的时钟
func (rt *realTimer) C() <-chan time.Time {
	return rt.timer.C
}

// 下一次执行的时间
func (rt *realTimer) Remaining() time.Duration {
	return rt.next.Sub(time.Now())
}

// 重置下次执行的时间
func (rt *realTimer) Reset(d time.Duration) bool {
	rt.next = time.Now().Add(d)
	return rt.timer.Reset(d)
}

其他的方法是将 time 直接进行暴露,没有做特殊封装

runtime 包为什么要使用 go:linkname?

因为runtime包内不是所有功能都会暴露成公共的API,但是又希望在内部模块之间实现信息的共享,所以通过 go:linkname 这种方式来进行导出

注意到这个是由于在 NewTimer 的包中看到 startTime 没有方法体,而是把实现放在了 runtime 包中,然后通过 go:linkname 来进行链接

go 复制代码
package time

func NewTimer(d Duration) *Timer {
	// ...
	startTimer(&t.r)
	return t
}

func startTimer(*runtimeTimer)

runtime 包内的具体实现

go 复制代码
package runtime 

//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
	if raceenabled {
		racerelease(unsafe.Pointer(t))
	}
	addtimer(t)
}

限流器的实现

先看一下整体流程:

RateLimiter 为抽象出的限流器接口,来限制执行的并发数

go 复制代码
type RateLimiter interface {
	PassiveRateLimiter
}

type PassiveRateLimiter interface {
	// 尝试是否可以获取token
	TryAccept() bool
}

限流器的构造

go 复制代码
type tokenBucketPassiveRateLimiter struct {
	limiter *rate.Limiter
	qps     float32
	clock   clock.PassiveClock
}

func NewTokenBucketPassiveRateLimiterWithClock(qps float32, burst int, c clock.PassiveClock) PassiveRateLimiter {
	limiter := rate.NewLimiter(rate.Limit(qps), burst)
	return newTokenBucketRateLimiterWithPassiveClock(limiter, c, qps)
}

func newTokenBucketRateLimiterWithPassiveClock(limiter *rate.Limiter, c clock.PassiveClock, qps float32) *tokenBucketPassiveRateLimiter {
	return &tokenBucketPassiveRateLimiter{
		limiter: limiter,
		qps:     qps,
		clock:   c,
	}
}

尝试获取执行的令牌,获取成功才可以执行

go 复制代码
func (tbprl *tokenBucketPassiveRateLimiter) TryAccept() bool {
	return tbprl.limiter.AllowN(tbprl.clock.Now(), 1)
}

func (lim *Limiter) AllowN(t time.Time, n int) bool {
	return lim.reserveN(t, n, 0).ok
}

func (lim *Limiter) reserveN(t time.Time, n int, maxFutureReserve time.Duration) Reservation {
	lim.mu.Lock()
	defer lim.mu.Unlock()
	// 未限流则直接返回成功
	if lim.limit == Inf {
		return Reservation{
			ok:        true,
			lim:       lim,
			tokens:    n,
			timeToAct: t,
		}
	} else if lim.limit == 0 {
		// 如果并发度设置为0但是允许n个并发则返回ok
		var ok bool
		if lim.burst >= n {
			ok = true
			lim.burst -= n
		}
		return Reservation{
			ok:        ok,
			lim:       lim,
			tokens:    lim.burst,
			timeToAct: t,
		}
	}

	//获取当前桶中有的令牌
	t, tokens := lim.advance(t)

	// 计算剩余的令牌是否够用
	tokens -= float64(n)

	// 如果不够用则计算需要等待的时间
	var waitDuration time.Duration
	if tokens < 0 {
		waitDuration = lim.limit.durationFromTokens(-tokens)
	}

	// 看并发度是否超过最大限度,并且是否进行等待
	ok := n <= lim.burst && waitDuration <= maxFutureReserve

	// 返回限流的结果
	r := Reservation{
		ok:    ok,
		lim:   lim,
		limit: lim.limit,
	}
	if ok {
		r.tokens = n
		r.timeToAct = t.Add(waitDuration)
		lim.last = t
		lim.tokens = tokens
		lim.lastEvent = r.timeToAct
	}

	return r
}

advance 来计算令牌数,超过最大值则丢弃多余的令牌

go 复制代码
func (lim *Limiter) advance(t time.Time) (newT time.Time, newTokens float64) {
	// 更新最后获取的时间,方便下次进行计算
	last := lim.last
	if t.Before(last) {
		last = t
	}

	// 计算经过这段时间产生的令牌书
	elapsed := t.Sub(last)
	delta := lim.limit.tokensFromDuration(elapsed)
	tokens := lim.tokens + delta

	// 如果token已经溢出,那么就设置成burst的值,丢掉多余的tokens
	if burst := float64(lim.burst); tokens > burst {
		tokens = burst
	}
	return t, tokens
}

回顾

最后回顾一下整个流程

  1. Loop 循环不断监听 runtimer的信号量。
  2. 获取信号量后尝试执行逻辑,如果限流器没有token则不执行
  3. 执行成功后重置timer,进入下一次循环。
相关推荐
福大大架构师每日一题2 小时前
22.1 k8s不同role级别的服务发现
容器·kubernetes·服务发现
weixin_453965003 小时前
[单master节点k8s部署]30.ceph分布式存储(一)
分布式·ceph·kubernetes
weixin_453965003 小时前
[单master节点k8s部署]32.ceph分布式存储(三)
分布式·ceph·kubernetes
tangdou3690986553 小时前
1分钟搞懂K8S中的NodeSelector
云原生·容器·kubernetes
later_rql6 小时前
k8s-集群部署1
云原生·容器·kubernetes
weixin_453965008 小时前
[单master节点k8s部署]31.ceph分布式存储(二)
分布式·ceph·kubernetes
大G哥11 小时前
记一次K8S 环境应用nginx stable-alpine 解析内部域名失败排查思路
运维·nginx·云原生·容器·kubernetes
妍妍的宝贝11 小时前
k8s 中微服务之 MetailLB 搭配 ingress-nginx 实现七层负载
nginx·微服务·kubernetes
福大大架构师每日一题13 小时前
23.1 k8s监控中标签relabel的应用和原理
java·容器·kubernetes
程序那点事儿13 小时前
k8s 之动态创建pv失败(踩坑)
云原生·容器·kubernetes