雲原生系統 之 彈性模式

大綱:

  1. 雲原生系統的彈性模式resiliency pattern

1.1 服務故障的雪崩效應
1.2 迴應之前雲原生 --彈性-- 疑問?

  1. 彈性模式: 作用在下游請求消息上
  2. 短期中斷的響應碼
  3. Polly經典策略
  4. Golang 斷路器模式

hi,好久不見,之前意譯並連載了《Microsoft Cloud-native toc.pdf》部分內容

  • 什麼是雲原生
  • 現代雲原生設計理念
  • .NET微服務
  • 談到雲原生,繞不開容器化
  • 支撐性服務 & 自動化能力

1. 雲原生系統的彈性模式

結合最近的工作經驗,本次繼續聊一聊雲原生的彈性請求策略 (resilience not scale),這也是迴應《現代雲原生設計理念》中

“在分佈式體系結構中,當服務B不響應來自服務A的網絡請求會發生什麼?
或者,當服務C暫時不可用,其他調用C的服務被阻塞時該怎麼辦?”

由於網絡原因或自身原因,B、C服務不能及時響應,服務A發起的請求將被阻塞(直到B、C響應),此時若大量請求湧入,服務A的線程資源將被消耗完畢,服務A的處理性能受到極大影響,進而影響下游依賴的external clients/backend srv。

故障會傳播,造成連鎖反應,對整個分佈式結構造成災難性後果,這就是服務故障的“雪崩效應”。

當B、C服務不可用,下游客戶端/backend srv能做什麼?
客觀上請求不通,執行預定的彈性策略: 重試/斷路?

2. 彈性模式: 作用在下游的請求消息上

彈性模式是系統面對故障仍然保持工作狀態的能力,它不是爲了避免故障,而是接受故障並嘗試去面對它。

Polly是一個全面的.NET彈性和瞬時錯誤處理庫,允許開發者以流暢和線程安全的方式表達彈性策略。

策略 場景 行爲
Retry 抖動/瞬時錯誤,短時間內自動恢復 在特定操作上配置重試行爲
Circuit Breaker 在短期內不大可能恢復 當故障超過閾值,在一段時間內快速失敗
Timeout 限制調用者等待響應的時間
Bulkhead 將操作限制在固定的資源池,防止故障傳播
Cache 自動存儲響應
Bulkhead 一旦失敗,定義結構化的行爲

一般將彈性策略作用到各種請求消息上(外部客戶端請求或後端服務請求)

其目的是補償暫時不可用的服務請求。

這裏我想着重聊一聊斷路器模式:Circuit Breaker
斷路器模式有三種狀態:

  • Close: 請求無故障,斷路器關閉
  • Open: 出現故障到達閾值,切換到[Open]狀態, 快速失敗。
  • Half-Open:進入Open狀態後,開啓一個Timer, 到達Timer, 會進入Half-Open狀態,會嘗試發送少量請求。
    成功進入 ----> Close
    失敗----> Open,開啓新的Timer

3. 短期中斷的響應碼

Http Status code 原因
404 not found
408 request timeout
429 two many requests
502 bad gateway
503 service unavailable
504 gateway timeout

正確規範的響應碼能給開發者足夠的信息,儘快確認故障。

執行故障策略時,也能有的放矢,比如只重試那些由失敗引起的操作,對於403UnAuthorized不可重試。

4. Polly的經典策略

  • retry: 對網絡抖動/瞬時錯誤可以執行retry策略(預期故障可以很快恢復),
  • Circuit Breaker:爲避免無效重試導致的故障傳播,在特定時間內如果失敗次數到達閾值,斷路器打開(在一定時間內快速失敗); 同時啓動一個timer,斷路器進入半開模式(發出少量請求,請求成功則認爲故障已經修復,錯誤計數器重置。)

install-package Microsoft.Extensions.Http.Polly

services.AddHttpClient("small")
        //降級
        .AddPolicyHandler(Policy<HttpResponseMessage>.HandleInner<Exception>().FallbackAsync(new HttpResponseMessage(),async b =>
        {
           // 1、降級打印異常
          Console.WriteLine($"服務開始降級,上游異常消息:{b.Exception.Message}");
          // 2、降級後的數據
          b.Result.Content= new StringContent("請求太多,請稍後重試", Encoding.UTF8, "text/html");
          b.Result.StatusCode = HttpStatusCode.TooManyRequests;
          await Task.CompletedTask;
        }))
        //熔斷                                                      
        .AddPolicyHandler(Policy<HttpResponseMessage>.Handle<Exception>() 
           .CircuitBreakerAsync(
              3,    // 打開斷路器之前失敗的次數
              TimeSpan.FromSeconds(20), // 斷路器的開啓的時間間隔
              (ex, ts) =>  //熔斷器開啓
              {
                  Console.WriteLine($"服務斷路器開啓,異常消息:{ex.Exception.Message}");
                  Console.WriteLine($"服務斷路器開啓的時間:{ts.TotalSeconds}s");
              }, 
              () => { Console.WriteLine($"服務斷路器重置"); },   //斷路器重置事件
              () => { Console.WriteLine($"服務斷路器半開啓(一會開,一會關)"); }  //斷路器半開啓事件
            )
        )
        //重試
        .AddPolicyHandler(Policy<HttpResponseMessage>.Handle<Exception>().RetryAsync(3))
       // 超時 
       .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(2)));

當一個應用存在多個Http調用,按照上面的經典寫法,代碼中會混雜大量重複、與業務無關的口水代碼,
思考如何優雅的對批量HttpClient做彈性策略。

這裏提供兩個實踐:

① 博客園馳名博主edisonchou: 使用AOP框架,動態織入Polly

② CSDN某佚名大牛,使用反射+配置 實現的PollyHttpClientServiceCollectionExtension擴展類, 支持在配置文件指定HttpClientName

5. Golang的Circuit Breaker pkg

go get github.com/sony/gobreaker

調用func NewCircuitBreaker(st Settings) *CircuitBreaker 實例化斷路器對象, 參數如下:

type Settings struct {
	Name          string
	MaxRequests   uint32       #半開狀態允許的最大請求數量,默認爲0,允許1個請求
	Interval      time.Duration
	Timeout       time.Duration  # 斷路器進入半開狀態的間隔,默認60s
	ReadyToTrip   func(counts Counts) bool   # 切換狀態的邏輯
	OnStateChange func(name string, from State, to State)
}

下面這個示例演示了: 請求谷歌網站,失敗比例達到60%,就切換到"打開"狀態,同時開啓60sTimer,到60s進入“半開”狀態(允許發起一個請求),如果成功, 斷路器進入"關閉"狀態;失敗則重新進入“打開”狀態,並重置60sTimer

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"github.com/sony/gobreaker"
)

var cb *gobreaker.CircuitBreaker

func init() {
	var st gobreaker.Settings
	st.Name = "HTTP GET"
	st.ReadyToTrip = func(counts gobreaker.Counts) bool {
		failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
		return counts.Requests >= 3 && failureRatio >= 0.6
	}

	cb = gobreaker.NewCircuitBreaker(st)
}

// Get wraps http.Get in CircuitBreaker.
func Get(url string) ([]byte, error) {
	body, err := cb.Execute(func() (interface{}, error) {
		resp, err := http.Get(url)
		if err != nil {
			return nil, err
		}

		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return nil, err
		}

		return body, nil
	})
	if err != nil {
		return nil, err
	}

	return body.([]byte), nil
}

func main() {
	body, err := Get("http://www.google.com/robots.txt")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(body))
}

總結

本文記錄了雲原生系統的彈性模式:通過預設策略直面失敗,補償暫時不可用的請求、避免故障傳播, 這對於實現微服務高可用、彈性容錯相當重要。

注意,Circuit Breaker與Retry的意圖不同,Retry模式的目的是讓應用程序重試一個可能成功的操作;
Circuit Breaker 防止程序應用程序執行可能失敗的操作。

一般情況下,應用程序會結合這兩種模式:斷路器包裝 重試操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章