入口源码地址:github.com/zeromicro/go-zero/rest/handler/sheddinghandler.go
在看文章之前能够看看万总的这篇文章《服务自适应降载爱护设计》,文章曾经给咱们介绍很分明了,从根底原理到架构需要再到代码正文,无不细致入微,感激万总。
之前在设计架构的时候对于服务过载爱护只会想到在客户端、网关层来实现,没思考过在服务端也能够达到这种成果,一来波及这种技术的文章较少(可能是我见多识广了),二来服务端不确定的状况比拟多,比方服务器呈现问题,或者其余在同一台服务器运行的软件把服务器间接搞挂,这样在服务端实现过载爱护在某些层面来说鲁棒性可能不太好 ,但在和熔断器联合后,用服务端来实现过载爱护也是荒诞不经的。
咱们来看下过载爱护设计到的几个算法
自旋锁
- 原理
问:假如有1个变量lock
,2个协程怎么用锁实现lock++
,lock
的后果最初为2
答:
- 锁也是1个变量,初值设为0;
- 1个协程将锁原子性的置为1;
- 操作变量
lock
; - 操作实现后,将锁原子性的置为0,开释锁。
- 在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)
过载爱护外围还应用了滑动窗口,滑动窗口的原理和细节能够看前一篇文章,外面有具体解答。
援用文章:
- [微服务治理之如何优雅应答突发流量洪峰](