乐趣区

关于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)

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

援用文章:

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