前文我们学习了 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
}
回顾
最后回顾一下整个流程
Loop
循环不断监听run
和timer
的信号量。- 获取信号量后尝试执行逻辑,如果限流器没有token则不执行
- 执行成功后重置timer,进入下一次循环。