关于golang:gozero源码阅读过载保护第三期

入口源码地址:github.com/zeromicro/go-zero/rest/handler/sheddinghandler.go

在看文章之前能够看看万总的这篇文章《服务自适应降载爱护设计》,文章曾经给咱们介绍很分明了,从根底原理到架构需要再到代码正文,无不细致入微,感激万总。

之前在设计架构的时候对于服务过载爱护只会想到在客户端、网关层来实现,没思考过在服务端也能够达到这种成果,一来波及这种技术的文章较少(可能是我见多识广了),二来服务端不确定的状况比拟多,比方服务器呈现问题,或者其余在同一台服务器运行的软件把服务器间接搞挂,这样在服务端实现过载爱护在某些层面来说鲁棒性可能不太好 ,但在和熔断器联合后,用服务端来实现过载爱护也是荒诞不经的。

咱们来看下过载爱护设计到的几个算法

自旋锁
  • 原理

问:假如有1个变量lock,2个协程怎么用锁实现lock++lock的后果最初为2

答:

  1. 锁也是1个变量,初值设为0;
  2. 1个协程将锁原子性的置为1;
  3. 操作变量lock
  4. 操作实现后,将锁原子性的置为0,开释锁。
  5. 在1个协程获取锁时,另一个协程始终尝试,直到可能获取锁(一直循环),这就是自旋锁。

2、自旋锁的毛病

某个协程持有锁工夫长,期待的协程始终在循环期待,耗费CPU资源。

不偏心,有可能存在有的协程等待时间过程,呈现线程饥饿(这里就是协程饥饿)

  • go-zero 自旋锁源码
type SpinLock struct {
    // 锁变量
    lock uint32
}

// Lock locks the SpinLock.
func (sl *SpinLock) Lock() {
    for !sl.TryLock() {
        // 暂停以后goroutine,让其余goroutine后行运算
        runtime.Gosched()
    }
}

// TryLock tries to lock the SpinLock.
func (sl *SpinLock) TryLock() bool {
    // 原子替换,0换成1
    return atomic.CompareAndSwapUint32(&sl.lock, 0, 1)
}

// Unlock unlocks the SpinLock.
func (sl *SpinLock) Unlock() {
    // 原子置零
    atomic.StoreUint32(&sl.lock, 0)
}

源码中还应用了 golang 的运行时操作包 runtime

runtime.Gosched()暂停以后goroutine,让其余goroutine后行运算

留神:只是暂停,不是挂起。

当工夫片轮转到该协程时,Gosched()前面的操作将主动复原

咱们来写写几行代码,看看他的作用是啥

func output(s string) {
    for i := 0; i < 3; i++ {
        fmt.Println(s)
    }
}
// 未应用Gosched的代码
func Test_GoschedDisable(t *testing.T) {
    go output("goroutine 2")
    output("goroutine 1")
}
// === RUN   Test_GoschedDisable
// goroutine 1
// goroutine 1
// goroutine 1
// --- PASS: Test_GoschedDisable (0.00s)

论断:还没等到子协程执行,主协程就曾经执行完退出了,子协程将不再执行,所以打印的全副是主协程的数据。当然,实际上这个执行后果也是不确定的,只是大概率呈现以上输入,因为主协程和子协程间并没有相对的程序关系

func output(s string) {
    for i := 0; i < 3; i++ {
        fmt.Println(s)
    }
}
// 应用Gosched的代码
func Test_GoschedEnable(t *testing.T) {
    go output("goroutine 2")
    runtime.Gosched()
    output("goroutine 1")
}

// === RUN   Test_GoschedEnable
// goroutine 2
// goroutine 2
// goroutine 2
// goroutine 1
// goroutine 1
// goroutine 1
// --- PASS: Test_GoschedEnable (0.00s)

论断:在打印goroutine 1之前,主协程调用了runtime.Gosched()办法,暂停了主协程。子协程取得了调度,从而后行打印了goroutine 2。主协程不是肯定要等其余协程执行完才会继续执行,而是肯定工夫。如果这个工夫内其余协程没有执行完,那么主协程将继续执行,例如以下例子

func output(s string) {
    for i := 0; i < 3; i++ {
        fmt.Println(s)
    }
}
// 应用Gosched的代码,并成心缩短子协程的执行工夫,看主协程是否始终期待
func Test_GoschedEnableAndSleep(t *testing.T) {
    go func() {
        time.Sleep(5000)
        output("goroutine 2")
    }()
    runtime.Gosched()
    output("goroutine 1")
}
// === RUN   Test_GoschedEnableAndSleep
// goroutine 2
// goroutine 2
// goroutine 2
// goroutine 1
// goroutine 1
// goroutine 1
// --- PASS: Test_GoschedEnableAndSleep (0.00s)

论断:即便咱们成心缩短子协程的执行工夫,主协程还是会始终期待子协程执行完才会执行。

源码中还应用了 golang 的原子操作包 atomic

atomic.CompareAndSwapUint32()函数用于对uint32值执行比拟和替换操作,此函数是并发平安的。

// addr 示意地址
// old  示意uint32值,它是旧的,
// new  示意uint32新值,它将与旧值替换本身。
// 如果替换实现,则返回true,否则返回false。
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)

atomic.StoreUint32() 函数用于将val原子存储到* addr中,此函数是并发平安的。

// addr 示意地址
// val  示意uint32值,它是旧的,
func StoreUint32(addr *uint32, val uint32)

过载爱护外围还应用了滑动窗口,滑动窗口的原理和细节能够看前一篇文章,外面有具体解答。

援用文章:

  • [微服务治理之如何优雅应答突发流量洪峰](

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理