云原生系統(tǒng)之彈性模式
本文轉(zhuǎn)載自微信公眾號「精益碼農(nóng)」,作者小碼甲 。轉(zhuǎn)載本文請聯(lián)系精益碼農(nóng)公眾號。
大綱
1.云原生系統(tǒng)的彈性模式resiliency pattern
- 1.1 服務(wù)故障的雪崩效應(yīng)
- 1.2 回應(yīng)之前云原生--彈性請求的疑問?
2. 彈性模式:作用在下游請求消息上
3. 短期中斷的響應(yīng)碼
4. Polly經(jīng)典策略
5. Golang 斷路器模式
德國哲學(xué)家尼采說過:那些殺不死我的東西,只會讓我更加強大。
01云原生系統(tǒng)的彈性模式
結(jié)合最近的工作經(jīng)驗,本次繼續(xù)聊一聊云原生的彈性模式 (resilience not scale), 這也是回應(yīng)《現(xiàn)代云原生設(shè)計理念》中
“在分布式體系結(jié)構(gòu)中,當(dāng)服務(wù)B不響應(yīng)來自服務(wù)A的網(wǎng)絡(luò)請求會發(fā)生什么?
當(dāng)服務(wù)C暫時不可用,其他調(diào)用C的服務(wù)被阻塞時該怎么辦?”
由于網(wǎng)絡(luò)原因或自身原因,B、C服務(wù)不能及時響應(yīng),服務(wù)A發(fā)起的請求將被阻塞(直到B、C響應(yīng)),此時若大量請求涌入,服務(wù)A的線程資源將被消耗殆盡,服務(wù)A的處理性能受到極大影響,進而影響下游依賴的external clients/backend srv。
故障會傳播,造成連鎖反應(yīng),對整個分布式結(jié)構(gòu)造成災(zāi)難性后果,這就是服務(wù)故障的“雪崩效應(yīng)”。
當(dāng)B、C服務(wù)不可用,下游客戶端/backend srv能做什么?
客觀上請求不通,執(zhí)行預(yù)定的彈性策略:重試/斷路?
02彈性模式:作用在下游的請求消息上
彈性模式是系統(tǒng)面對故障仍然保持工作狀態(tài)的能力,它不是為了避免故障,而是接受故障并嘗試去面對它。
Polly是一個全面的.NET彈性和瞬時錯誤處理庫,允許開發(fā)者以流暢和線程安全的方式表達彈性策略。
策略 | 場景 | 行為 |
Retry | 抖動/瞬時錯誤,短時間內(nèi)自動恢復(fù) | 在特定操作上配置重試行為 |
Circuit Breaker | 在短期內(nèi)不大可能恢復(fù) | 當(dāng)故障超過閾值,在一段時間內(nèi)快速失敗 |
Timeout | 限制調(diào)用者等待響應(yīng)的時間 | |
Bulkhead | 將操作限制在固定的資源池,防止故障傳播 | |
Cache | 自動存儲響應(yīng) | |
Bulkhead | 一旦失敗,定義結(jié)構(gòu)化的行為 |
一般將彈性策略作用到各種請求消息上(外部客戶端請求或后端服務(wù)請求)。
其目的是補償暫時不可用的服務(wù)請求。
03短期中斷的響應(yīng)碼
Http Status code
原因
404
not found
408
request timeout
429
two many requests
502
bad gateway
503
service unavailable
504
gateway timeout
正確規(guī)范的響應(yīng)碼能幫助開發(fā)者盡快確認故障。
執(zhí)行故障策略時,也能有的放矢,比如只重試那些由失敗引起的操作,對于403UnAuthorized不可重試。
04Polly的經(jīng)典策略
- Retry:對網(wǎng)絡(luò)抖動/瞬時錯誤可以執(zhí)行retry策略(預(yù)期故障可以很快恢復(fù)),
- Circuit Breaker:為避免無效重試導(dǎo)致的故障傳播,在特定時間內(nèi)如果失敗次數(shù)到達閾值,斷路器打開(在一定時間內(nèi)快速失敗);
同時啟動一個timer,斷路器進入半開模式(發(fā)出少量請求,請求成功則認為故障已經(jīng)修復(fù),進入關(guān)閉狀態(tài),重置失敗計數(shù)器。)
- services.AddHttpClient("small")
- //降級
- .AddPolicyHandler(Policy<HttpResponseMessage>.HandleInner<Exception>().FallbackAsync(new HttpResponseMessage(),async b =>
- {
- // 1、降級打印異常
- Console.WriteLine($"服務(wù)開始降級,上游異常消息:{b.Exception.Message}");
- // 2、降級后的數(shù)據(jù)
- b.Result.Content= new StringContent("請求太多,請稍后重試", Encoding.UTF8, "text/html");
- b.Result.StatusCode = HttpStatusCode.TooManyRequests;
- await Task.CompletedTask;
- }))
- //熔斷
- .AddPolicyHandler(Policy<HttpResponseMessage>.Handle<Exception>()
- .CircuitBreakerAsync(
- 3, // 打開斷路器之前失敗的次數(shù)
- TimeSpan.FromSeconds(20), // 斷路器的開啟的時間間隔
- (ex, ts) => //熔斷器開啟
- {
- Console.WriteLine($"服務(wù)斷路器開啟,異常消息:{ex.Exception.Message}");
- Console.WriteLine($"服務(wù)斷路器開啟的時間:{ts.TotalSeconds}s");
- },
- () => { Console.WriteLine($"服務(wù)斷路器重置"); }, //斷路器重置事件
- () => { Console.WriteLine($"服務(wù)斷路器半開啟(一會開,一會關(guān))"); } //斷路器半開啟事件
- )
- )
- //重試
- .AddPolicyHandler(Policy<HttpResponseMessage>.Handle<Exception>().RetryAsync(3))
- // 超時
- .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(2)));
??當(dāng)一個應(yīng)用存在多個Http調(diào)用,按照上面的經(jīng)典寫法,代碼中會混雜大量重復(fù)、與業(yè)務(wù)無關(guān)的口水代碼,
思考如何優(yōu)雅的對批量HttpClient做彈性策略。
這里提供兩個實踐:
① 博客園馳名博主edisonchou: 使用AOP框架,動態(tài)織入Polly
② 某佚名大牛,使用反射加配置實現(xiàn)的PollyHttpClientServiceCollectionExtension擴展類, 支持在配置文件指定HttpClientName
05Golang的斷路器
- go get github.com/sony/gobreaker
func NewCircuitBreaker(st Settings) *CircuitBreaker 實例化斷路器對象, 參數(shù)如下:
- type Settings struct {
- Name string
- MaxRequests uint32 #半開狀態(tài)允許的最大請求數(shù)量,默認為0,允許1個請求
- Interval time.Duration
- Timeout time.Duration # 斷路器進入半開狀態(tài)的間隔,默認60s
- ReadyToTrip func(counts Counts) bool # 切換狀態(tài)的邏輯
- OnStateChange func(name string, from State, to State)
- }
下面這個示例演示了:請求谷歌網(wǎng)站,失敗比例達到60%,就切換到"打開"狀態(tài),同時開啟60sTimer,到60s進入“半開”狀態(tài)(允許發(fā)起一個請求),如果成功, 斷路器進入"關(guān)閉"狀態(tài);失敗則重新進入“打開”狀態(tài),并重置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))
- }
總結(jié)
本文記錄了云原生系統(tǒng)的彈性模式:通過預(yù)設(shè)策略直面失敗,補償暫時不可用的請求、避免故障傳播, 這對于實現(xiàn)微服務(wù)高可用、彈性容錯相當(dāng)重要。