過載保護原理與實戰
在微服務中由於服務間相互依賴很容易出現連鎖故障,連鎖故障可能是由於整個服務鏈路中的某一個服務出現故障,進而導致系統的其他部分也出現故障。例如某個服務的某個例項由於過載出現故障,導致其他例項負載升高,從而導致這些例項像多米諾骨牌一樣一個個全部出現故障,這種連鎖故障就是所謂的雪崩現象
比如,服務 A 依賴服務 C,服務 C 依賴服務 D,服務 D 依賴服務 E,當服務 E 過載會導致響應時間變慢甚至服務不可用,這個時候呼叫方 D 會出現大量超時連線資源被大量佔用得不到釋放,進而資源被耗盡導致服務 D 也過載,從而導致服務 C 過載以及整個系統雪崩
某一種資源的耗盡可以導致高延遲、高錯誤率或者相應資料不符合預期的情況發生,這些的確是在資源耗盡時應該出現的情況,在負載不斷上升直到過載時,伺服器不可能一直保持完全的正常。而 CPU 資源的不足導致的負載上升是我們工作中最常見的,如果 CPU 資源不足以應對請求負載,一般來說所有的請求都會變慢,CPU 負載過高會造成一系列的副作用,主要包括以下幾項:
- 正在處理的 (in-flight) 的請求數量上升
- 伺服器逐漸將請求佇列填滿,意味著延遲上升,同時佇列會用更多的記憶體
- 執行緒卡住,無法處理請求
- cpu 死鎖或者請求卡主
- rpc 服務呼叫超時
- cpu 的快取效率下降
由此可見防止伺服器過載的重要性不言而喻,而防止伺服器過載又分為下面幾種常見的策略:
- 提供降級結果
- 在過載情況下主動拒絕請求
- 呼叫方主動拒絕請求
- 提前進行壓測以及合理的容量規劃
今天我們主要討論的是第二種防止伺服器過載的方案,即在過載的情況下主動拒絕請求,下面我統一使用” 過載保護 “來表述,過載保護的大致原理是當探測到伺服器已經處於過載時則主動拒絕請求不進行處理,一般做法是快速返回 error
很多微服務框架中都內建了過載保護能力,本文主要分析go-zero中的過載保護功能,我們先通過一個例子來感受下 go-zero 的中的過載保護是怎麼工作的
首先,我們使用官方推薦的goctl生成一個 api 服務和一個 rpc 服務,生成服務的過程比較簡單,在此就不做介紹,可以參考官方文件,我的環境是兩臺伺服器,api 服務跑在本機,rpc 服務跑在遠端伺服器
遠端伺服器為單核 CPU,首先通過壓力工具模擬伺服器負載升高,把 CPU 打滿
stress -c 1 -t 1000
此時通過 uptime 工具檢視伺服器負載情況,-d 引數可以高亮負載的變化情況,此時的負載已經大於 CPU 核數,說明伺服器正處於過載狀態
watch -d uptime
19:47:45 up 5 days, 21:55, 3 users, load average: 1.26, 1.31, 1.44
此時請求 api 服務,其中 ap 服務內部依賴 rpc 服務,檢視 rpc 服務的日誌,級別為 stat,可以看到 cpu 是比較高的
"level":"stat","content":"(rpc) shedding_stat [1m], cpu: 986, total: 4, pass: 2, drop: 2"
並且會列印過載保護丟棄請求的日誌,可以看到過載保護已經生效,主動丟去了請求
adaptiveshedder.go:185 dropreq, cpu: 990, maxPass: 87, minRt: 1.00, hot: true, flying: 2, avgFlying: 2.07
這個時候呼叫方會收到 "service overloaded" 的報錯
通過上面的試驗我們可以看到當伺服器負載過高就會觸發過載保護,從而避免連鎖故障導致雪崩,接下來我們從原始碼來分析下過載保護的原理,go-zero 在 http 和 rpc 框架中都內建了過載保護功能,程式碼路徑分別在 go-zero/rest/handler/sheddinghandler.go 和 go-zero/zrpc/internal/serverinterceptors/sheddinginterceptor.go 下面,我們就以 rpc 下面的過載保護進行分析,在 server 啟動的時候回 new 一個 shedder 程式碼路徑: go-zero/zrpc/server.go:119, 然後當收到每個請求都會通過 Allow 方法判斷是否需要進行過載保護,如果 err 不等於 nil 說明需要過載保護則直接返回 error
promise, err = shedder.Allow()
if err != nil {
metrics.AddDrop()
sheddingStat.IncrementDrop()
return
}
實現過載保護的程式碼路徑為: go-zero/core/load/adaptiveshedder.go,這裡實現的過載保護基於滑動視窗可以防止毛刺,有冷卻時間防止抖動,當 CPU>90% 的時候開始拒絕請求,Allow 的實現如下
func (as *adaptiveShedder) Allow() (Promise, error) {
if as.shouldDrop() {
as.dropTime.Set(timex.Now())
as.droppedRecently.Set(true)
return nil, ErrServiceOverloaded // 返回過載錯誤
}
as.addFlying(1) // flying +1
return &promise{
start: timex.Now(),
shedder: as,
}, nil
}
sholdDrop 實現如下,該函式用來檢測是否符合觸發過載保護條件,如果符合的話會記錄 error 日誌
func (as *adaptiveShedder) shouldDrop() bool {
if as.systemOverloaded() || as.stillHot() {
if as.highThru() {
flying := atomic.LoadInt64(&as.flying)
as.avgFlyingLock.Lock()
avgFlying := as.avgFlying
as.avgFlyingLock.Unlock()
msg := fmt.Sprintf(
"dropreq, cpu: %d, maxPass: %d, minRt: %.2f, hot: %t, flying: %d, avgFlying: %.2f",
stat.CpuUsage(), as.maxPass(), as.minRt(), as.stillHot(), flying, avgFlying)
logx.Error(msg)
stat.Report(msg)
return true
}
}
return false
}
判斷 CPU 是否達到預設值,預設 90%
systemOverloadChecker = func(cpuThreshold int64) bool {
return stat.CpuUsage() >= cpuThreshold
}
CPU 的負載統計程式碼如下,每隔 250ms 會進行一次統計,每一分鐘沒記錄一次統計日誌
func init() {
go func() {
cpuTicker := time.NewTicker(cpuRefreshInterval)
defer cpuTicker.Stop()
allTicker := time.NewTicker(allRefreshInterval)
defer allTicker.Stop()
for {
select {
case <-cpuTicker.C:
threading.RunSafe(func() {
curUsage := internal.RefreshCpu()
prevUsage := atomic.LoadInt64(&cpuUsage)
// cpu = cpuᵗ⁻¹ * beta + cpuᵗ * (1 - beta)
usage := int64(float64(prevUsage)*beta + float64(curUsage)*(1-beta))
atomic.StoreInt64(&cpuUsage, usage)
})
case <-allTicker.C:
printUsage()
}
}
}()
}
其中 CPU 統計實現的程式碼路徑為: go-zero/core/stat/internal,在該路徑下使用 linux 結尾的檔案,因為在 go 語言中會根據不同的系統編譯不同的檔案,當為 linux 系統時會編譯以 linux 為字尾的檔案
func init() {
cpus, err := perCpuUsage()
if err != nil {
logx.Error(err)
return
}
cores = uint64(len(cpus))
sets, err := cpuSets()
if err != nil {
logx.Error(err)
return
}
quota = float64(len(sets))
cq, err := cpuQuota()
if err == nil {
if cq != -1 {
period, err := cpuPeriod()
if err != nil {
logx.Error(err)
return
}
limit := float64(cq) / float64(period)
if limit < quota {
quota = limit
}
}
}
preSystem, err = systemCpuUsage()
if err != nil {
logx.Error(err)
return
}
preTotal, err = totalCpuUsage()
if err != nil {
logx.Error(err)
return
}
}
在 linux 中,通過/proc 虛擬檔案系統向使用者控制元件提供了系統內部狀態的資訊,而/proc/stat 提供的就是系統的 CPU 等的任務統計資訊,這裡主要原理就是通過/proc/stat 來計算 CPU 的使用率
本文主要介紹了過載保護的原理,以及通過實驗觸發了過載保護,最後分析了實現過載保護功能的程式碼,相信通過本文大家對過載保護會有進一步的認識,過載保護不是萬金油,對服務來說是有損的,所以在服務上線前我們最好是進行壓測做好資源規劃,儘量避免服務過載
專案地址
https://github.com/tal-tech/go-zero
框架地址
https://github.com/tal-tech/go-zero/tree/master/core/load
文件地址
https://www.yuque.com/tal-tech/go-zero/rhakzy
微信交流群
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 微服務過載保護原理與實戰微服務
- SpringCloud Alibaba實戰(9:Hystrix容錯保護)SpringGCCloud
- Keepalived 原理與實戰
- class-dump 混淆加固、保護與最佳化原理
- Oracle的過載保護-資料庫資源限制Oracle資料庫
- Maven實戰與原理分析(二):maven實戰Maven
- Nginx的程式管理與過載原理Nginx
- 直播原理與web直播實戰Web
- 圖解CPU的真實模式與保護模式圖解模式
- 企業檔案加密:資料保護的實戰策略加密
- 轉載 利用SEH異常處理機制繞過GS保護
- Istio 流量治理功能原理與實戰
- Metro拆包工作原理與實戰
- NetCore專案實戰篇07---服務保護之pollyNetCore
- 方法過載原理
- 流的破壞與保護
- [譯] 通過安全瀏覽保護 WebViewWebView
- 【科普】等級保護與分級保護的區別和聯絡!
- Hyperledger Fabric原理詳解與實戰1
- Hyperledger Fabric原理詳解與實戰4
- Android 除錯實戰與原理詳解Android除錯
- apache common pool2原理與實戰Apache
- 通過Dapr實現一個簡單的基於.net的微服務電商系統(十七)——服務保護之動態配置與熱過載微服務
- 基於gRPC的註冊發現與負載均衡的原理和實戰RPC負載
- 資料安全與PostgreSQL:保護策略SQL
- 保護模式篇——總結與提升模式
- 基於 gRPC 的服務註冊與發現和負載均衡的原理與實戰RPC負載
- 官方工具|MySQL Router 高可用原理與實戰MySql
- Selenium原理、安裝與自動打卡實戰
- 位元組碼引用檢測原理與實戰
- iOS加固原理與常見措施:保護移動應用程式安全的利器iOS
- ArkWeb智慧防跟蹤與廣告過濾 - 保護使用者隱私Web
- 大資料安全與隱私保護大資料
- win10實時保護關閉方法_WIN10的實時保護如何關閉Win10
- 保護模式模式
- 保護期限
- 固態硬碟掉電保護的原理及測試方法硬碟
- 分析及防護:Win10執行流保護繞過問題Win10