云原生系統之彈性模式
本文轉載自微信公眾號「精益碼農」,作者小碼甲 。轉載本文請聯系精益碼農公眾號。
大綱
1.云原生系統的彈性模式resiliency pattern
- 1.1 服務故障的雪崩效應
- 1.2 回應之前云原生--彈性請求的疑問?
2. 彈性模式:作用在下游請求消息上
3. 短期中斷的響應碼
4. Polly經典策略
5. Golang 斷路器模式
德國哲學家尼采說過:那些殺不死我的東西,只會讓我更加強大。
01云原生系統的彈性模式
結合最近的工作經驗,本次繼續聊一聊云原生的彈性模式 (resilience not scale), 這也是回應《現代云原生設計理念》中
“在分布式體系結構中,當服務B不響應來自服務A的網絡請求會發生什么?
當服務C暫時不可用,其他調用C的服務被阻塞時該怎么辦?”
由于網絡原因或自身原因,B、C服務不能及時響應,服務A發起的請求將被阻塞(直到B、C響應),此時若大量請求涌入,服務A的線程資源將被消耗殆盡,服務A的處理性能受到極大影響,進而影響下游依賴的external clients/backend srv。
故障會傳播,造成連鎖反應,對整個分布式結構造成災難性后果,這就是服務故障的“雪崩效應”。
當B、C服務不可用,下游客戶端/backend srv能做什么?
客觀上請求不通,執行預定的彈性策略:重試/斷路?
02彈性模式:作用在下游的請求消息上
彈性模式是系統面對故障仍然保持工作狀態的能力,它不是為了避免故障,而是接受故障并嘗試去面對它。
Polly是一個全面的.NET彈性和瞬時錯誤處理庫,允許開發者以流暢和線程安全的方式表達彈性策略。
策略 | 場景 | 行為 |
Retry | 抖動/瞬時錯誤,短時間內自動恢復 | 在特定操作上配置重試行為 |
Circuit Breaker | 在短期內不大可能恢復 | 當故障超過閾值,在一段時間內快速失敗 |
Timeout | 限制調用者等待響應的時間 | |
Bulkhead | 將操作限制在固定的資源池,防止故障傳播 | |
Cache | 自動存儲響應 | |
Bulkhead | 一旦失敗,定義結構化的行為 |
一般將彈性策略作用到各種請求消息上(外部客戶端請求或后端服務請求)。
其目的是補償暫時不可用的服務請求。
03短期中斷的響應碼
Http Status code
原因
404
not found
408
request timeout
429
two many requests
502
bad gateway
503
service unavailable
504
gateway timeout
正確規范的響應碼能幫助開發者盡快確認故障。
執行故障策略時,也能有的放矢,比如只重試那些由失敗引起的操作,對于403UnAuthorized不可重試。
04Polly的經典策略
- Retry:對網絡抖動/瞬時錯誤可以執行retry策略(預期故障可以很快恢復),
- Circuit Breaker:為避免無效重試導致的故障傳播,在特定時間內如果失敗次數到達閾值,斷路器打開(在一定時間內快速失敗);
同時啟動一個timer,斷路器進入半開模式(發出少量請求,請求成功則認為故障已經修復,進入關閉狀態,重置失敗計數器。)
- 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
② 某佚名大牛,使用反射加配置實現的PollyHttpClientServiceCollectionExtension擴展類, 支持在配置文件指定HttpClientName
05Golang的斷路器
- 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))
- }
總結
本文記錄了云原生系統的彈性模式:通過預設策略直面失敗,補償暫時不可用的請求、避免故障傳播, 這對于實現微服務高可用、彈性容錯相當重要。