共计 2690 个字符,预计需要花费 7 分钟才能阅读完成。
入口源码地址: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)
过载爱护外围还应用了滑动窗口,滑动窗口的原理和细节能够看前一篇文章,外面有具体解答。
援用文章:
- [微服务治理之如何优雅应答突发流量洪峰](