大綱:
- 雲原生系統的彈性模式resiliency pattern
1.1 服務故障的雪崩效應
1.2 迴應之前雲原生 --彈性-- 疑問?
- 彈性模式: 作用在下游請求消息上
- 短期中斷的響應碼
- Polly經典策略
- 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 防止程序應用程序執行可能失敗的操作。
一般情況下,應用程序會結合這兩種模式:斷路器包裝 重試操作。
- https://blog.csdn.net/weixin_44588495/article/details/106361934
- https://blog.csdn.net/qq_26900081/article/details/108071374
- https://www.cnblogs.com/edisonchou/p/9159644.html
- https://docs.microsoft.com/en-us/dotnet/architecture/cloud-native/application-resiliency-patterns
- https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker