入口源码地址: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)

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

援用文章:

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