微服務過載保護原理與實戰

在微服務中因爲服務間相互依賴很容易出現連鎖故障,連鎖故障多是因爲整個服務鏈路中的某一個服務出現故障,進而致使系統的其餘部分也出現故障。例如某個服務的某個實例因爲過載出現故障,致使其餘實例負載升高,從而致使這些實例像多米諾骨牌同樣一個個所有出現故障,這種連鎖故障就是所謂的雪崩現象linux

好比,服務A依賴服務C,服務C依賴服務D,服務D依賴服務E,當服務E過載會致使響應時間變慢甚至服務不可用,這個時候調用方D會出現大量超時鏈接資源被大量佔用得不到釋放,進而資源被耗盡致使服務D也過載,從而致使服務C過載以及整個系統雪崩git

service_dependency

某一種資源的耗盡能夠致使高延遲、高錯誤率或者相應數據不符合預期的狀況發生,這些的確是在資源耗盡時應該出現的狀況,在負載不斷上升直到過載時,服務器不可能一直保持徹底的正常。而CPU資源的不足致使的負載上升是咱們工做中最多見的,若是CPU資源不足以應對請求負載,通常來講全部的請求都會變慢,CPU負載太高會形成一系列的反作用,主要包括如下幾項:github

  • 正在處理的(in-flight) 的請求數量上升
  • 服務器逐漸將請求隊列填滿,意味着延遲上升,同時隊列會用更多的內存
  • 線程卡住,沒法處理請求
  • cpu死鎖或者請求卡主
  • rpc服務調用超時
  • cpu的緩存效率降低

因而可知防止服務器過載的重要性不言而喻,而防止服務器過載又分爲下面幾種常見的策略:api

  • 提供降級結果
  • 在過載狀況下主動拒絕請求
  • 調用方主動拒絕請求
  • 提早進行壓測以及合理的容量規劃

今天咱們主要討論的是第二種防止服務器過載的方案,即在過載的狀況下主動拒絕請求,下面我統一使用」過載保護「來表述,過載保護的大體原理是當探測到服務器已經處於過載時則主動拒絕請求不進行處理,通常作法是快速返回errorpromise

fail_fast

不少微服務框架中都內置了過載保護能力,本文主要分析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的使用率

本文主要介紹了過載保護的原理,以及經過實驗觸發了過載保護,最後分析了實現過載保護功能的代碼,相信經過本文你們對過載保護會有進一步的認識,過載保護不是萬金油,對服務來講是有損的,因此在服務上線前咱們最好是進行壓測作好資源規劃,儘可能避免服務過載

寫做不易,若是以爲文章不錯,歡迎 github star 🤝

項目地址:https://github.com/tal-tech/go-zero

項目地址:
https://github.com/tal-tech/go-zero

相關文章
相關標籤/搜索