go-zero過載保護(hù)源碼解讀
引言
在看文章之前可以看看萬總的這篇文章《服務(wù)自適應(yīng)降載保護(hù)設(shè)計(jì)》,文章已經(jīng)給我們介紹很清楚了,從基礎(chǔ)原理到架構(gòu)需求再到代碼注釋,無不細(xì)致入微,感謝萬總。
之前在設(shè)計(jì)架構(gòu)的時(shí)候?qū)τ诜?wù)過載保護(hù)只會(huì)想到在客戶端、網(wǎng)關(guān)層來實(shí)現(xiàn),沒考慮過在服務(wù)端也可以達(dá)到這種效果,一來涉及這種技術(shù)的文章較少(可能是我孤陋寡聞了),二來服務(wù)端不確定的情況比較多,比如服務(wù)器出現(xiàn)問題,或者其他在同一臺(tái)服務(wù)器運(yùn)行的軟件把服務(wù)器直接搞掛,這樣在服務(wù)端實(shí)現(xiàn)過載保護(hù)在某些層面來說魯棒性可能不太好 ,但在和熔斷器結(jié)合后,用服務(wù)端來實(shí)現(xiàn)過載保護(hù)也是合情合理的。
我們來看下過載保護(hù)設(shè)計(jì)到的幾個(gè)算法
自旋鎖
- 原理
問:假設(shè)有1個(gè)變量lock
,2個(gè)協(xié)程怎么用鎖實(shí)現(xiàn)lock++
,lock
的結(jié)果最后為2
答:
- 鎖也是1個(gè)變量,初值設(shè)為0;
- 1個(gè)協(xié)程將鎖原子性的置為1;
- 操作變量
lock
; - 操作完成后,將鎖原子性的置為0,釋放鎖。
- 在1個(gè)協(xié)程獲取鎖時(shí),另一個(gè)協(xié)程一直嘗試,直到能夠獲取鎖(不斷循環(huán)),這就是自旋鎖。
自旋鎖的缺點(diǎn)
- 某個(gè)協(xié)程持有鎖時(shí)間長(zhǎng),等待的協(xié)程一直在循環(huán)等待,消耗CPU資源。
- 不公平,有可能存在有的協(xié)程等待時(shí)間過程,出現(xiàn)線程饑餓(這里就是協(xié)程饑餓)
go-zero 自旋鎖源碼
type SpinLock struct { // 鎖變量 lock uint32 } // Lock locks the SpinLock. func (sl *SpinLock) Lock() { for !sl.TryLock() { // 暫停當(dāng)前goroutine,讓其他goroutine先行運(yùn)算 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 的運(yùn)行時(shí)操作包 runtime
runtime.Gosched()
暫停當(dāng)前goroutine,讓其他goroutine先行運(yùn)算
注意:只是暫停,不是掛起。
當(dāng)時(shí)間片輪轉(zhuǎn)到該協(xié)程時(shí),Gosched()后面的操作將自動(dòng)恢復(fù)
我們來寫寫幾行代碼,看看他的作用是啥
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)
小結(jié)
還沒等到子協(xié)程執(zhí)行,主協(xié)程就已經(jīng)執(zhí)行完退出了,子協(xié)程將不再執(zhí)行,所以打印的全部是主協(xié)程的數(shù)據(jù)。當(dāng)然,實(shí)際上這個(gè)執(zhí)行結(jié)果也是不確定的,只是大概率出現(xiàn)以上輸出,因?yàn)橹鲄f(xié)程和子協(xié)程間并沒有絕對(duì)的順序關(guān)系
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)
結(jié)論:在打印goroutine 1之前,主協(xié)程調(diào)用了runtime.Gosched()方法,暫停了主協(xié)程。子協(xié)程獲得了調(diào)度,從而先行打印了goroutine 2。主協(xié)程不是一定要等其他協(xié)程執(zhí)行完才會(huì)繼續(xù)執(zhí)行,而是一定時(shí)間。如果這個(gè)時(shí)間內(nèi)其他協(xié)程沒有執(zhí)行完,那么主協(xié)程將繼續(xù)執(zhí)行,例如以下例子
func output(s string) { for i := 0; i < 3; i++ { fmt.Println(s) } } // 使用Gosched的代碼,并故意延長(zhǎng)子協(xié)程的執(zhí)行時(shí)間,看主協(xié)程是否一直等待 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)
結(jié)論:即使我們故意延長(zhǎng)子協(xié)程的執(zhí)行時(shí)間,主協(xié)程還是會(huì)一直等待子協(xié)程執(zhí)行完才會(huì)執(zhí)行。
源碼中還使用了 golang 的原子操作包 atomic
atomic.CompareAndSwapUint32()
函數(shù)用于對(duì)uint32
值執(zhí)行比較和交換操作,此函數(shù)是并發(fā)安全的。
// addr 表示地址 // old 表示uint32值,它是舊的, // new 表示uint32新值,它將與舊值交換自身。 // 如果交換完成,則返回true,否則返回false。 func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
atomic.StoreUint32()
函數(shù)用于將val
原子存儲(chǔ)到* addr
中,此函數(shù)是并發(fā)安全的。
// addr 表示地址 // val 表示uint32值,它是舊的, func StoreUint32(addr *uint32, val uint32)
過載保護(hù)核心還使用了滑動(dòng)窗口,滑動(dòng)窗口的原理和細(xì)節(jié)可以看前一篇文章,里面有詳細(xì)解答。
以上就是go-zero過載保護(hù)源碼解讀的詳細(xì)內(nèi)容,更多關(guān)于go-zero過載保護(hù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解golang 定時(shí)任務(wù)time.Sleep和time.Tick實(shí)現(xiàn)結(jié)果比較
本文主要介紹了golang 定時(shí)任務(wù)time.Sleep和time.Tick實(shí)現(xiàn)結(jié)果比較,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02深入分析Go?實(shí)現(xiàn)?MySQL?數(shù)據(jù)庫(kù)事務(wù)
本文深入分析了Go語言實(shí)現(xiàn)MySQL數(shù)據(jù)庫(kù)事務(wù)的原理和實(shí)現(xiàn)方式,包括事務(wù)的ACID特性、事務(wù)的隔離級(jí)別、事務(wù)的實(shí)現(xiàn)方式等。同時(shí),本文還介紹了Go語言中的事務(wù)處理機(jī)制和相關(guān)的API函數(shù),以及如何使用Go語言實(shí)現(xiàn)MySQL數(shù)據(jù)庫(kù)事務(wù)。2023-06-06Golang內(nèi)存泄露場(chǎng)景與定位方式的實(shí)現(xiàn)
Golang有自動(dòng)垃圾回收機(jī)制,但是仍然可能會(huì)出現(xiàn)內(nèi)存泄漏的情況,本文主要介紹了Golang內(nèi)存泄露場(chǎng)景與定位方式的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-04-04GO中的時(shí)間操作總結(jié)(time&dateparse)
日常開發(fā)過程中,對(duì)于時(shí)間的操作可謂是無處不在,但是想實(shí)現(xiàn)時(shí)間自由還是不簡(jiǎn)單的,多種時(shí)間格式容易混淆,本文為大家整理了一下GO中的時(shí)間操作,有需要的可以參考下2023-09-09Go語言使用釘釘機(jī)器人推送消息的實(shí)現(xiàn)示例
本文主要介紹了Go語言使用釘釘機(jī)器人推送消息的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09Golang切片連接成字符串的實(shí)現(xiàn)示例
本文主要介紹了Golang切片連接成字符串的實(shí)現(xiàn)示例,可以使用Go語言中的內(nèi)置函數(shù)"String()"可以將字節(jié)切片轉(zhuǎn)換為字符串,具有一定的參考價(jià)值,感興趣的可以了解一下2023-11-11