微服務(wù)過(guò)載保護(hù)原理與實(shí)戰(zhàn)

在微服務(wù)中由于服務(wù)間相互依賴很容易出現(xiàn)連鎖故障踊东,連鎖故障可能是由于整個(gè)服務(wù)鏈路中的某一個(gè)服務(wù)出現(xiàn)故障,進(jìn)而導(dǎo)致系統(tǒng)的其他部分也出現(xiàn)故障。例如某個(gè)服務(wù)的某個(gè)實(shí)例由于過(guò)載出現(xiàn)故障,導(dǎo)致其他實(shí)例負(fù)載升高个从,從而導(dǎo)致這些實(shí)例像多米諾骨牌一樣一個(gè)個(gè)全部出現(xiàn)故障,這種連鎖故障就是所謂的雪崩現(xiàn)象

比如歪沃,服務(wù)A依賴服務(wù)C嗦锐,服務(wù)C依賴服務(wù)D,服務(wù)D依賴服務(wù)E沪曙,當(dāng)服務(wù)E過(guò)載會(huì)導(dǎo)致響應(yīng)時(shí)間變慢甚至服務(wù)不可用奕污,這個(gè)時(shí)候調(diào)用方D會(huì)出現(xiàn)大量超時(shí)連接資源被大量占用得不到釋放,進(jìn)而資源被耗盡導(dǎo)致服務(wù)D也過(guò)載液走,從而導(dǎo)致服務(wù)C過(guò)載以及整個(gè)系統(tǒng)雪崩

<img src="https://gitee.com/kevwan/static/raw/master/doc/images/service_dependency.png" alt="service_dependency" style="zoom: 80%;" />

某一種資源的耗盡可以導(dǎo)致高延遲碳默、高錯(cuò)誤率或者相應(yīng)數(shù)據(jù)不符合預(yù)期的情況發(fā)生,這些的確是在資源耗盡時(shí)應(yīng)該出現(xiàn)的情況育灸,在負(fù)載不斷上升直到過(guò)載時(shí)腻窒,服務(wù)器不可能一直保持完全的正常。而CPU資源的不足導(dǎo)致的負(fù)載上升是我們工作中最常見的磅崭,如果CPU資源不足以應(yīng)對(duì)請(qǐng)求負(fù)載,一般來(lái)說(shuō)所有的請(qǐng)求都會(huì)變慢瓦哎,CPU負(fù)載過(guò)高會(huì)造成一系列的副作用砸喻,主要包括以下幾項(xiàng):

  • 正在處理的(in-flight) 的請(qǐng)求數(shù)量上升
  • 服務(wù)器逐漸將請(qǐng)求隊(duì)列填滿柔逼,意味著延遲上升,同時(shí)隊(duì)列會(huì)用更多的內(nèi)存
  • 線程卡住割岛,無(wú)法處理請(qǐng)求
  • cpu死鎖或者請(qǐng)求卡主
  • rpc服務(wù)調(diào)用超時(shí)
  • cpu的緩存效率下降

由此可見防止服務(wù)器過(guò)載的重要性不言而喻愉适,而防止服務(wù)器過(guò)載又分為下面幾種常見的策略:

  • 提供降級(jí)結(jié)果
  • 在過(guò)載情況下主動(dòng)拒絕請(qǐng)求
  • 調(diào)用方主動(dòng)拒絕請(qǐng)求
  • 提前進(jìn)行壓測(cè)以及合理的容量規(guī)劃

今天我們主要討論的是第二種防止服務(wù)器過(guò)載的方案,即在過(guò)載的情況下主動(dòng)拒絕請(qǐng)求癣漆,下面我統(tǒng)一使用”過(guò)載保護(hù)“來(lái)表述维咸,過(guò)載保護(hù)的大致原理是當(dāng)探測(cè)到服務(wù)器已經(jīng)處于過(guò)載時(shí)則主動(dòng)拒絕請(qǐng)求不進(jìn)行處理,一般做法是快速返回error

<img src="https://gitee.com/kevwan/static/raw/master/doc/images/fail_fast.png" alt="fail_fast" style="zoom: 50%;" />

很多微服務(wù)框架中都內(nèi)置了過(guò)載保護(hù)能力惠爽,本文主要分析go-zero中的過(guò)載保護(hù)功能癌蓖,我們先通過(guò)一個(gè)例子來(lái)感受下go-zero的中的過(guò)載保護(hù)是怎么工作的

首先,我們使用官方推薦的goctl生成一個(gè)api服務(wù)和一個(gè)rpc服務(wù)婚肆,生成服務(wù)的過(guò)程比較簡(jiǎn)單租副,在此就不做介紹,可以參考官方文檔较性,我的環(huán)境是兩臺(tái)服務(wù)器用僧,api服務(wù)跑在本機(jī),rpc服務(wù)跑在遠(yuǎn)程服務(wù)器

遠(yuǎn)程服務(wù)器為單核CPU赞咙,首先通過(guò)壓力工具模擬服務(wù)器負(fù)載升高责循,把CPU打滿

stress -c 1 -t 1000

此時(shí)通過(guò)uptime工具查看服務(wù)器負(fù)載情況,-d參數(shù)可以高亮負(fù)載的變化情況攀操,此時(shí)的負(fù)載已經(jīng)大于CPU核數(shù)院仿,說(shuō)明服務(wù)器正處于過(guò)載狀態(tài)

watch -d uptime

19:47:45 up 5 days, 21:55,  3 users,  load average: 1.26, 1.31, 1.44

此時(shí)請(qǐng)求api服務(wù),其中ap服務(wù)內(nèi)部依賴rpc服務(wù)崔赌,查看rpc服務(wù)的日志意蛀,級(jí)別為stat,可以看到cpu是比較高的

"level":"stat","content":"(rpc) shedding_stat [1m], cpu: 986, total: 4, pass: 2, drop: 2"

并且會(huì)打印過(guò)載保護(hù)丟棄請(qǐng)求的日志健芭,可以看到過(guò)載保護(hù)已經(jīng)生效县钥,主動(dòng)丟去了請(qǐng)求

adaptiveshedder.go:185 dropreq, cpu: 990, maxPass: 87, minRt: 1.00, hot: true, flying: 2, avgFlying: 2.07

這個(gè)時(shí)候調(diào)用方會(huì)收到 "service overloaded" 的報(bào)錯(cuò)

通過(guò)上面的試驗(yàn)我們可以看到當(dāng)服務(wù)器負(fù)載過(guò)高就會(huì)觸發(fā)過(guò)載保護(hù),從而避免連鎖故障導(dǎo)致雪崩慈迈,接下來(lái)我們從源碼來(lái)分析下過(guò)載保護(hù)的原理若贮,go-zero在http和rpc框架中都內(nèi)置了過(guò)載保護(hù)功能,代碼路徑分別在go-zero/rest/handler/sheddinghandler.go和go-zero/zrpc/internal/serverinterceptors/sheddinginterceptor.go下面痒留,我們就以rpc下面的過(guò)載保護(hù)進(jìn)行分析谴麦,在server啟動(dòng)的時(shí)候回new一個(gè)shedder 代碼路徑: go-zero/zrpc/server.go:119, 然后當(dāng)收到每個(gè)請(qǐng)求都會(huì)通過(guò)Allow方法判斷是否需要進(jìn)行過(guò)載保護(hù)伸头,如果err不等于nil說(shuō)明需要過(guò)載保護(hù)則直接返回error

promise, err = shedder.Allow()
if err != nil {
  metrics.AddDrop()
  sheddingStat.IncrementDrop()
  return
}

實(shí)現(xiàn)過(guò)載保護(hù)的代碼路徑為: go-zero/core/load/adaptiveshedder.go匾效,這里實(shí)現(xiàn)的過(guò)載保護(hù)基于滑動(dòng)窗口可以防止毛刺,有冷卻時(shí)間防止抖動(dòng)恤磷,當(dāng)CPU>90%的時(shí)候開始拒絕請(qǐng)求面哼,Allow的實(shí)現(xiàn)如下

func (as *adaptiveShedder) Allow() (Promise, error) {
    if as.shouldDrop() {
        as.dropTime.Set(timex.Now())
        as.droppedRecently.Set(true)

        return nil, ErrServiceOverloaded  // 返回過(guò)載錯(cuò)誤
    }

    as.addFlying(1) // flying +1

    return &promise{
        start:   timex.Now(),
        shedder: as,
    }, nil
}

sholdDrop實(shí)現(xiàn)如下野宜,該函數(shù)用來(lái)檢測(cè)是否符合觸發(fā)過(guò)載保護(hù)條件,如果符合的話會(huì)記錄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是否達(dá)到預(yù)設(shè)值魔策,默認(rèn)90%

systemOverloadChecker = func(cpuThreshold int64) bool {
    return stat.CpuUsage() >= cpuThreshold
}

CPU的負(fù)載統(tǒng)計(jì)代碼如下匈子,每隔250ms會(huì)進(jìn)行一次統(tǒng)計(jì),每一分鐘沒(méi)記錄一次統(tǒng)計(jì)日志

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??1 * beta + cpu? * (1 - beta)
                    usage := int64(float64(prevUsage)*beta + float64(curUsage)*(1-beta))
                    atomic.StoreInt64(&cpuUsage, usage)
                })
            case <-allTicker.C:
                printUsage()
            }
        }
    }()
}

其中CPU統(tǒng)計(jì)實(shí)現(xiàn)的代碼路徑為: go-zero/core/stat/internal闯袒,在該路徑下使用linux結(jié)尾的文件虎敦,因?yàn)樵趃o語(yǔ)言中會(huì)根據(jù)不同的系統(tǒng)編譯不同的文件,當(dāng)為linux系統(tǒng)時(shí)會(huì)編譯以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中政敢,通過(guò)/proc虛擬文件系統(tǒng)向用戶控件提供了系統(tǒng)內(nèi)部狀態(tài)的信息其徙,而/proc/stat提供的就是系統(tǒng)的CPU等的任務(wù)統(tǒng)計(jì)信息,這里主要原理就是通過(guò)/proc/stat來(lái)計(jì)算CPU的使用率

本文主要介紹了過(guò)載保護(hù)的原理堕仔,以及通過(guò)實(shí)驗(yàn)觸發(fā)了過(guò)載保護(hù)擂橘,最后分析了實(shí)現(xiàn)過(guò)載保護(hù)功能的代碼,相信通過(guò)本文大家對(duì)過(guò)載保護(hù)會(huì)有進(jìn)一步的認(rèn)識(shí)摩骨,過(guò)載保護(hù)不是萬(wàn)金油通贞,對(duì)服務(wù)來(lái)說(shuō)是有損的,所以在服務(wù)上線前我們最好是進(jìn)行壓測(cè)做好資源規(guī)劃恼五,盡量避免服務(wù)過(guò)載

寫作不易昌罩,如果覺(jué)得文章不錯(cuò),歡迎 github star ??

項(xiàng)目地址:https://github.com/tal-tech/go-zero

項(xiàng)目地址:
https://github.com/tal-tech/go-zero

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末灾馒,一起剝皮案震驚了整個(gè)濱河市茎用,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌睬罗,老刑警劉巖轨功,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異容达,居然都是意外死亡古涧,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門花盐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)羡滑,“玉大人,你說(shuō)我怎么就攤上這事算芯∑饣瑁” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵熙揍,是天一觀的道長(zhǎng)职祷。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么堪旧? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任削葱,我火速辦了婚禮奖亚,結(jié)果婚禮上淳梦,老公的妹妹穿的比我還像新娘。我一直安慰自己昔字,他們只是感情好爆袍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著作郭,像睡著了一般陨囊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上夹攒,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天蜘醋,我揣著相機(jī)與錄音,去河邊找鬼咏尝。 笑死压语,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的编检。 我是一名探鬼主播胎食,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼允懂!你這毒婦竟也來(lái)了抬吟?” 一聲冷哼從身側(cè)響起亭姥,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后簿盅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肋坚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年吆玖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片置侍。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡映之,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜡坊,到底是詐尸還是另有隱情杠输,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布秕衙,位于F島的核電站蠢甲,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏据忘。R本人自食惡果不足惜鹦牛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一搞糕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧曼追,春花似錦窍仰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至晶伦,卻和暖如春碟狞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背婚陪。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工族沃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泌参。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓脆淹,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親及舍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子未辆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容