Service Resilience是指當服務的的運行環境出現了問題,例如網路故障或服務過載或某些微服務宕機的情況下,程式仍能夠提供部分或大部分服務,這時我們就說服務的韌性很強,它是微服務中很重要的一部分內容,并被廣泛討論,它是衡量服務質量的一個重要指標,Service Resilience從內容上講翻譯成“容錯”可能更接近, 但“容錯”英文是“Fault Tolerance”,它的含義與“Service Resilience”是不同的,因此我覺得翻譯成“服務韌性“比較合適,服務韌性在微服務體系中非常重要,它通過提高服務的韌性來彌補環境上的不足,
服務韌性通過下面幾種技術來提升服務的可靠性:
- 服務超時 (Timeout)
- 服務重試 (Retry)
- 服務限流(Rate Limiting)
- 熔斷器 (Circuit Breaker)
- 故障注入(Fault Injection)
- 艙壁隔離技術(Bulkhead)
程式實作:
服務韌性能通過不同的方式來實作,我們先用代碼的來實作,程式并不復雜,但問題是服務韌性的代碼會和業務代碼混在一起,這帶來了以下問題:
- 誤改業務邏輯:當你修改服務韌性的代碼時有可能會失手誤改業務邏輯,
- 系統架構不夠靈活:將來如果要改成別的架構會很困難,例如將來要改成由基礎設施來完成這部分功能的話,需要把服務韌性的代碼摘出來,這會非常麻煩,
- 程式可讀性差:因為業務邏輯和非功能性需求混在一起,很難看懂這段程式到底需要完成什么功能,有些人可能覺得這不很重要,但對我來說這個是一個致命的問題,
- 加重測驗負擔:不管你是要修改業務邏輯還是非功能性需求,你都要進行系統的回歸測驗, 這大大加重了測驗負擔,
多數情況下我們要面對的問題是現在已經有了實作業務邏輯的函式,但要把上面提到的功能加入到這個業務函式中,又不想修改業務函式本身的代碼,我們采用的方法叫修飾模式(Decorator Pattern),在Go中一般叫他中間件模式(Middleware Pattern),修飾模式(Decorator Pattern)的關鍵是定義一系列修飾函式,每個函式都完成一個不同的功能,但他們的回傳型別(是一個Interface)都相同,因此我們可以把這些函式一個個疊加上去,來完成全部功能,下面看一下具體實作,
我們用一個簡單的gRPC微服務來展示服務韌性的功能,下圖是程式結構,它分為客戶端(client)和服務端(server),它們的內部結構是類似的,“middleware”包是實作服務韌性功能的包, “service”包是業務邏輯,在服務端就是微服務的實作函式,客戶端就是呼叫服務端的函式,在客戶端(client)下的“middleware”包中包含四個檔案并實作了三個功能:服務超時,服務重試和熔斷器,“clientMiddleware.go"是總入口,在服務端(server)下的“middleware”包中包含兩個檔案并實作了一個功能,服務限流,“serverMiddleware.go"是總入口,

修飾模式:
修飾模式有不同的實作方式,本程式中的方式是從Go kit中學來的,它是我看到的是一種最靈活的實作方式,
下面是“service”包中的“cacheClient.go", 它是用來呼叫服務端函的,“CacheClient”是一個空結構,是為了實作“CallGet()”函式,也就實作了“callGetter”介面(下面會講到),所有的業務邏輯都在這里,它是修飾模式要完成的主要功能,其余的功能都是對它的修飾,
type CacheClient struct {
}
func (cc *CacheClient) CallGet(ctx context.Context, key string, csc pb.CacheServiceClient) ( []byte, error) {
getReq:=&pb.GetReq{Key:key}
getResp, err :=csc.Get(ctx, getReq )
if err != nil {
return nil, err
}
value := getResp.Value
return value, err
}
func (cc *CacheClient) CallStore(key string, value []byte, client pb.CacheServiceClient) ( *pb.StoreResp, error) {
storeReq := pb.StoreReq{Key: key, Value: value}
storeResp, err := client.Store(context.Background(), &storeReq)
if err != nil {
return nil, err
}
return storeResp, err
}
下面是客戶端的入口檔案“clientMiddleware.go". 它定義了”callGetter“介面,這個是修飾模式的核心,每一個修飾(功能)都要實作這個介面,介面里只有一個函式“CallGet”,就是這個函式會被每個修飾功能不斷呼叫, 這個函式的簽名是按照業務函式來定義的,它還定義了一個結構(struct)CallGetMiddleware,里面只有一個成員“Next”, 它的型別是修飾模式介面(callGetter),這樣我們就可以通過“Next”來呼叫下一個修飾功能,每一個修飾功能結構都會有一個相應的修飾結構,我們需要把他們串起來,才能完成依次疊加呼叫,
“BuildGetMiddleware()”就是用來實作這個功能的,CircuitBreakerCallGet,RetryCallGet和TimeoutCallGet分別是熔斷器,服務重試和服務超時的實作結構,它們每個里面也都只有一個成員“Next”,在創建時,要把它們一個個依次帶入,要注意順序,最先創建的 “CircuitBreakerCallGet” 最后執行,在每一個修飾功能的最后都要呼叫“Next.CallGet()”,這樣就把控制傳給了下一個修飾功能,
type callGetter interface {
CallGet(ctx context.Context, key string, c pb.CacheServiceClient) ( []byte, error)
}
type CallGetMiddleware struct {
Next callGetter
}
func BuildGetMiddleware(cc callGetter) callGetter {
cbcg := CircuitBreakerCallGet{cc}
tcg := TimeoutCallGet{&cbcg}
rcg := RetryCallGet{&tcg}
return &rcg
}
func (cg *CallGetMiddleware) CallGet(ctx context.Context, key string, csc pb.CacheServiceClient) ( []byte, error) {
return cg.Next.CallGet(ctx, key, csc)
}
服務重試:
當網路不穩定時,服務有可能暫時失靈,這種情況一般持續時間很短,只要重試一下就能解決問題,下面是程式,它的邏輯比較簡單,就是如果有錯的話就不斷地呼叫“tcg.Next.CallGet(ctx, key, csc)”,直到沒錯了或達到了重試上限,每次重試之間有一個重試間隔(retry_interval),
const (
retry_count = 3
retry_interval = 200
)
type RetryCallGet struct {
Next callGetter
}
func (tcg *RetryCallGet) CallGet(ctx context.Context, key string, csc pb.CacheServiceClient) ( []byte, error) {
var err error
var value []byte
for i:=0; i<retry_count; i++ {
value, err = tcg.Next.CallGet(ctx, key, csc)
log.Printf("Retry number %v|error=%v", i+1, err)
if err == nil {
break
}
time.Sleep(time.Duration(retry_interval)*time.Millisecond)
}
return value, err
}
服務重試跟其他功能不同的地方在于它有比較大的副作用,因此要小心使用,因為重試會成倍地增加系統負荷,甚至會造成系統雪崩,有兩點要注意:
- 重試次數:一般來講次數不要過多,這樣才不會給系統帶來過大負擔
- 重試間隔時間:重試間隔時間要越來越長,這樣能錯開重試時間,而且越往后失敗的可能性越高,因此間隔時間要越長,一般是用斐波那契數列(Fibonacci sequence)或2的冪,當然如果重試次數少的話可酌情調整,示例中用了最簡單的方式,恒定間隔,生產環境中最好不要這樣設定,
并不是所有函式都需要重試功能,只有非常重要的,不能失敗的才需要,
服務超時:
服務超時給每個服務設定一個最大時間限制,超過之后服務停止,回傳錯誤資訊,它的好處是第一可以減少用戶等待時間,因為如果一個普通操作幾秒之后還不出結果就多半出了問題,沒必要再等了,第二,一個請求一般都會占用系統資源,如執行緒,資料庫鏈接,如果有大量等待請求會耗盡系統資源,導致系統宕機或性能降低,提前結束請求可以盡快釋放系統資源,下面是程式,它在context里設定了超時,并通過通道選擇器來判斷運行結果,當超時時,ctx的通道被關(ctx.Done()),函式停止運行,并呼叫cancelFunc()停止下游操作,如果沒有超時,則程式正常完成,
type TimeoutCallGet struct {
Next callGetter
}
func (tcg *TimeoutCallGet) CallGet(ctx context.Context, key string, c pb.CacheServiceClient) ( []byte, error) {
var cancelFunc context.CancelFunc
var ch = make(chan bool)
var err error
var value []byte
ctx, cancelFunc= context.WithTimeout(ctx, get_timeout*time.Millisecond)
go func () {
value, err = tcg.Next.CallGet(ctx, key, c)
ch<- true
} ()
select {
case <-ctx.Done():
log.Println("ctx timeout")
//ctx timeout, call cancelFunc to cancel all the sub-processes in the calling chain
cancelFunc()
err = ctx.Err()
case <-ch:
log.Println("call finished normally")
}
return value, err
}
這個功能應該設定在客戶端還是服務端?服務重試沒有問題只能在客戶端,服務超時在服務端和客戶端都可以設定,但設定在客戶端更好,這樣命運是掌握在自己手里,
下一個問題是順序選擇, 你是先做服務重試還是先做服務超時?結果是不一樣的,先做服務重試時,超時是設定在所有重試上;先做服務超時,超時是設定在每一次重試上,這個要根據你的具體需求來決定,我是把超時定在每一次重試上,
服務限流(Rate Limiting):
服務限流根據服務的承載能力來設定一個請求數量的上限,一般是每秒鐘能處理多少個并發請求,超過之后,其他所有請求全部回傳錯誤資訊,這樣可以保證服務質量,不能得到服務的請求也能快速得到結果,這個功能與其他不同,它定義在服務端,當然你也可以給客戶端限流,但最侄訓是要限制在服務端才更有意義,
下面是服務端“service”包中的“cacheServer.go", 它是服務端的介面函式,“CacheService”是它的結構,它實作了“Get”函式,也就服務端的業務邏輯,其他的修飾功能都是對他的補充修飾,
// CacheService struct
type CacheService struct {
Storage map[string][]byte
}
// Get function
func (c *CacheService) Get(ctx context.Context, req *pb.GetReq) (*pb.GetResp, error) {
fmt.Println("start server side Get called: ")
//time.Sleep(3000*time.Millisecond)
key := req.GetKey()
value := c.Storage[key]
resp := &pb.GetResp{Value: value}
fmt.Println("Get called with return of value: ", value)
return resp, nil
}
...
下面是“serverMiddleware.go”,它是服務端middleware的入口,它定義了結構“CacheServiceMiddleware”, 里面只有一個成員“Next", 它的型別是 “pb.CacheServiceServer”,是gRPC服務端的介面,注意這里我們的處理方式與客戶端不同,它沒有創建另外的介面, 而是直接使用了gRPC的服務端介面,客戶端的做法是每個函式建立一個入口(介面),這樣控制的顆粒度更細,但代碼量更大,服務端所有函式共用一個入口,控制的顆粒度較粗,但代碼量較少,這樣做的原因是客戶端需要更精準的控制,具體實作時,你可以根據應用程式的需求來決定選哪種方式,“BuildGetMiddleware”是服務端創建修飾結構的函式,ThrottleMiddleware是服務限流的實作結構,它里面也只有一個成員“Next”,在創建時,要把具體的middleware功能依次帶入,現在只有一個就是“ThrottleMiddleware”,
type CacheServiceMiddleware struct {
Next pb.CacheServiceServer
}
func BuildGetMiddleware(cs pb.CacheServiceServer ) pb.CacheServiceServer {
tm := ThrottleMiddleware{cs}
csm := CacheServiceMiddleware{&tm}
return &csm
}
func (csm *CacheServiceMiddleware) Get(ctx context.Context, req *pb.GetReq) (*pb.GetResp, error) {
return csm.Next.Get(ctx, req)
}
func (csm *CacheServiceMiddleware) Store(ctx context.Context, req *pb.StoreReq) (*pb.StoreResp, error) {
return csm.Next.Store(ctx, req)
}
func (csm *CacheServiceMiddleware) Dump(dr *pb.DumpReq, csds pb.CacheService_DumpServer) error {
return csm.Next.Dump(dr, csds)
}
下面是服務限流的實作程式,它比其他的功能要稍微復雜一些,其他功能使用的的控制引數(例如重試次數)在執行程序中是不會被修改的,而它的(throttle)是可以并行讀寫的,因此需要用“sync.RWMutex”來控制,具體的限流功能在“Get”函式中,它首先判斷是否超過閥值(throttle),超過則回傳錯誤資訊,反之則運行,
const (
service_throttle = 5
)
var tm throttleMutex
type ThrottleMiddleware struct {
Next pb.CacheServiceServer
}
type throttleMutex struct {
mu sync.RWMutex
throttle int
}
func (t *throttleMutex )getThrottle () int {
t.mu.RLock()
defer t.mu.RUnlock()
return t.throttle
}
func (t *throttleMutex) changeThrottle(delta int ) {
t.mu.Lock()
defer t.mu.Unlock()
t.throttle =t.throttle+delta
}
func (tg *ThrottleMiddleware) Get(ctx context.Context, req *pb.GetReq) (*pb.GetResp, error) {
if tm.getThrottle()>=service_throttle {
log.Printf("Get throttle=%v reached\n", tm.getThrottle())
return nil, errors.New("service throttle reached, please try later")
} else {
tm.changeThrottle(1)
resp, err := tg.Next.Get(ctx, req)
tm.changeThrottle(-1)
return resp, err
}
}
熔斷器 (Circuit Breaker):
熔斷器是最復雜的, 它的主要功能是當系統檢測到下游服務不暢通時對這個服務進行熔斷,這樣就阻斷了所有對此服務的呼叫,減少了下游服務的負載,讓下游服務有個緩沖來恢復功能,與之相關的就是服務降級,下游服務沒有了,需要的資料怎么辦?一般是定義一個降級函式,或者是從快取里取舊的資料或者是直接回傳空值給上游函式,這要根據業務邏輯來定,下面是它的服務示意圖,服務A有三個下游服務,服務B,服務C,服務D,其中前兩個服務的熔斷器是關閉的,也就是服務是暢通的,服務D的熔斷器是打開的,也就是服務例外,

圖片來源
熔斷器用狀態機(State Machine)來進行管理,它會監測對下游服務的呼叫失敗情況,并設立一個失敗上限閥值,由閥值來控制狀態轉換,它有三個狀態:關閉,打開和半打開,這里的“關閉“是熔斷器的關閉,服務是打開的,下面是它的簡單示意圖,

圖片來源
正常情況熔斷器是關閉的,當失敗請求數超過閥值時,熔斷器打開,下游服務關閉,熔斷器打開是有時間限制的,超時之后自動變成半打開狀態,這時只放一小部分請求通過,當請求失敗時,回傳打開狀態,當請求成功并且數量超過閥值時,熔斷器狀態變成關閉,恢復正常,下面是它的詳細示意圖,它圖里有偽程式,可以仔細讀一下,讀懂了,就明白了實作原理,

圖片來源
當有多個修飾功能時,咋一看熔斷器應該是第一個執行的,因為如果服務端出了問題最好的對策就是屏蔽掉請求,其他的就不要試了,例如服務重試,但仔細一想,這樣的話服務重試就沒有被熔斷器監控,因此熔斷器還是最后一個執行好,不過具體情況還是要根據你有哪些修飾功能來決定,
熔斷器有很多不同的實作,其中最出名的可能是Netflix的“Hystrix”,本程式用的是一個go開源庫叫gobreaker, 因為它比較純粹(Hystrix把很多東西都集成在一起了),當然熔斷的原理都是一樣的,下面是熔斷器的程式,其中“cb”是“CircuitBreaker”的變數,它是在“init()”里面創建的,這樣保證它只創建一次,在創建時,就設定了具體引數,“MaxRequests”是在半開狀態下允許通過的最大請求數,”Timeout“是關閉狀態的超時時間(超時后自動變成半開狀態),這里沒有設定,取預設值60秒,“ReadyToTrip”函式用來控制狀態轉換,當它回傳true時,熔斷器由關閉變成打開,熔斷器的功能是在函式“CallGet”中實作的,它的執行函式是“cb.Execute”, 只要把要運行的函式傳入就行了,如果熔斷器是打開狀態,它就回傳預設值,如果熔斷器是關閉狀態,它就運行傳入的請求,熔斷器自己會監測請求的執行狀態并根據它的資訊來控制開關轉換,
var cb *gobreaker.CircuitBreaker
type CircuitBreakerCallGet struct {
Next callGetter
}
func init() {
var st gobreaker.Settings
st.Name = "CircuitBreakerCallGet"
st.MaxRequests = 2
st.ReadyToTrip = func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 2 && failureRatio >= 0.6
}
cb = gobreaker.NewCircuitBreaker(st)
}
func (tcg *CircuitBreakerCallGet) CallGet(ctx context.Context, key string, c pb.CacheServiceClient) ( []byte, error) {
var err error
var value []byte
var serviceUp bool
log.Printf("state:%v", cb.State().String())
_, err = cb.Execute(func() (interface{}, error) {
value, err = tcg.Next.CallGet(ctx, key, c)
if err != nil {
return nil, err
}
serviceUp = true
return value, nil
})
if !serviceUp {
//return a default value here. You can also run a downgrade function here
log.Printf("circuit breaker return error:%v\n", err)
return []byte{0}, nil
}
return value, nil
}
本示例中熔斷器是設定在客戶端的,從本質上來講它是針對客戶端的功能,當客戶端察覺要呼叫的服務失效時,它暫時屏蔽掉這個服務,但我覺得它其實設定在服務端更有優勢, 設想一下當有很多不同節點訪問一個服務時,當然也有可能別人都能訪問,只有我不能訪問,這基本可以肯定是我的節點和服務端節點之間的鏈接出了問題,根本不需要熔斷器來處理(容器就可以處理了),因此,熔斷器要處理的大部分問題是某個微服務宕機了,這時監測服務端更有效,而不是監測客戶端,當然最后的效果還是要屏蔽客戶端請求,這樣才能有效減少網路負載,這就需要服務端和客戶端之間進行協調,因此有一定難度,
另外還有一個問題就是如何判斷服務宕機了,這個是整個熔斷器的關鍵,如果服務回傳錯誤結果,那么是否意味著服務失效呢?這會有很多不同的情況,熔斷器幾乎不可能做出完全準確的判斷,從這點上來講,熔斷器還是有瑕疵的,我覺得要想做出準確的判斷,必須網路,容器和Service Mesh進行聯合診斷才行,
故障注入(Fault Injection)
故障注入通過人為地注入錯誤來模擬生產環境中的各種故障和不穩定性,你可以模擬10%的錯誤率,或服務回應延遲,使用的技術跟上面講的差不多,可以通過修飾模式來對服務請求進行監控和控制,來達到模擬錯誤的目的,故障注入既可以在服務端也可以在客戶端,
艙壁隔離技術(Bulkhead)
艙壁隔離技術指的是對系統資源進行隔離,這樣當一個請求出現問題時不會導致整個系統的癱瘓,比較常用的是Thread pool和Connection pool,比如系統里有資料庫的Connection Pool,它一般都有一個上限值,如果請求超出,多余的請求就只能處于等待狀態,如果你的系統中既有運行很慢的請求(例如報表),也有運行很快的請求(例如修改一個資料庫欄位),那么一個好的辦法就是設立兩個Connection Pool, 一個給快的一個給慢的,這樣當慢的請求很多時,占用了所有Connection Pool,但不會影響到快的請求,下面是它的示意圖,它用的是Thread Pool,

圖片來源
Netflix的Hystrix同時集成了艙壁隔離技術和熔斷器,它通過限制訪問一個服務的資源(一般是Thread)來達到隔離的目的,它有兩種方式,第一種是通過Thread Pool, 第二種是信號隔離(Semaphore Isolation),也就是每個請求都要先得到授權才能訪問資源,詳細情況請參閱這里.
艙壁隔離的實際應用方式要比Hystrix的廣泛得多,它不是一種單一的技術,而是可以應用在許多不同的方向(詳細情況請參閱這里) ,
新一代技術--自適應并發限制(Adaptive Concurreny Limit)
上面講的技術都是基于靜態閥值的,多數都是每秒多少請求,但是對于擁有自動伸縮(Auto-scaling)的大型分布式系統,這種方式并不適用,自動伸縮的云系統會根據服務負載來調整服務器的個數,這樣服務的閥值就變成動態的,而不是靜態的,Netflix的新一代技術可以建立動態閥值,它叫自適應并發限制(Adaptive Concurrency Limit),它的原理是根據服務的延遲來計算負載,從而動態地找出服務的閥值,一旦找出動態閥值,這項技術是很容易執行的,困難的地方是如何找出閥值,這項技術可以應用在下面幾個方向:
- RPC(gRPC):既可以應用在客戶端,也可以應用在服務端,可以使用攔截器(Interceptor)來實作
- Servlet: 可以使用過濾器來實作(Filter)
- Thread Pool:這是一種更通用的方式,它可以根據服務延遲來自動調節Thread Pool的大小,從而達到并發限制,
詳細情況請參見Netflix/concurrency-limits.
Service Mesh實作:
從上面的程式實作可以看出,它們的每個功能并不復雜,而且不會對業務邏輯進行侵入,但上面只是實作了一個微服務呼叫的一個函式,如果你有很多微服務,每個微服務又有不少函式的話,那它的作業量還是相當大的,有沒有更好的辦法呢?當我接觸服務韌性時,就覺得直接把功能放到程式里對業務邏輯侵入太大,就用了攔截器(Interceptor),但它不夠靈活,也不方便,后來終于找到了比較靈活的修飾模式的實作方式,這個問題終于解決了,但作業量還是太大,后來看到了Service Mesh才發現問題的根源,因為服務韌性本來就不是應用程式應該解決的問題,
而是基礎設施或中間件的主場,這里面涉及到的許多資料都和網路和基礎設施相關,應用程式本來就不掌握這些資訊,因此處理起來就束手束腳,應用程式還是應該主要關注業務邏輯,而把這些跨領域的問題交給基礎設施來處理,
我們知道容器技術(Docker和Kubernetes)的出現大大簡化了程式的部署,特別是對微服務而言,但一開始服務韌性這部分還是由應用程式來做,最有名的應該是“Netflix OSS”,現在我們把這部分功能抽出來, 就是Service Mesh, 比較有名的是Istio和Linkerd,當然Service Mesh還有其它功能,包括網路流量控制,權限控制與授權,應用程式監測等,它是在容器的基礎上,加強了對應用程式的管理并提供了更多的服務,
當用Service Mesh來實作服務韌性時,你基本不用編程,只需要寫些組態檔,這樣更加徹底地把它與業務邏輯分開了,也減輕了碼農的負擔,但它也不是沒有缺點的,撰寫組態檔實際上是另一種變向的編程,當檔案大了之后很容易出錯,現在已經有了比較好的支持容器的IDE,但對Service Mesh的支持還不是太理想,另外,就是這個技識訓比較新,有很多人都在測驗它,但在生產環境中應用的好像不是特別多,如果想要在生產環境中使用,需要做好準備去應對各種問題,當然Service Mesh是一個不可阻擋的趨勢,就像容器技術一樣,也許將來它會融入到容器中,成為容器的一部分,有關生產環境中使用Service Mesh請參閱“下一代的微服務架構基礎是ServiceMesh?”
Service Mesh的另一個好處是它無需編程,這樣就不需要每一種語言都有一套服務韌性的庫,當然Service Mesh也有不同的實作,每一種實作在設定引數時都有它自己的語法格式,理想的情況是它們都遵守統一的介面,希望以后會是這樣,
什么時候需要這些技術?
上面提到的技術都不錯,但不論你是用程式還是用Service Mesh來實作,都有大量的作業要做,尤其是當服務眾多,并且之間的呼叫關系復雜時,那么你是否應該讓所有的服務都具有這些功能呢?我的建議是,在開始時只給最重要的服務增加這些功能,而其他服務可以先放一放,當在生產環境運行一段時間之后,就能發現那些是經常出問題的服務,再根據問題的性質來考慮是否增加這些功能,應用本文中的修飾模式的好處是,它增加和洗掉修飾功能都非常容易,并且不會影響業務邏輯,
結論:
服務韌性是微服務里非常重要的一項技術,它可以在服務環境不可靠的情況下仍能提供適當的服務,大大提高了服務的容錯能力,它使用的技術包括服務超時 (Timeout),服務重試 (Retry),服務限流(Rate Limiting),熔斷器 (Circuit Breaker),故障注入(Fault Injection)和艙壁隔離技術(Bulkhead),你可以用代碼也可以用Service Mesh來實作這些功能,Service Mesh是將來的方向,但實作它們需要大量的作業,因此需要考慮清楚到底哪些服務真正需要它們,
原始碼:
完整源碼的github鏈接
索引:
[1]Go kit
http://gokit.io/examples/stringsvc.html
[2] Circuit Breaker Pattern
https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
[3]gobreaker
https://github.com/sony/gobreaker
[4]Bulkhead pattern
https://docs.microsoft.com/en-us/azure/architecture/patterns/bulkhead
[5]How it Works
https://github.com/Netflix/Hystrix/wiki/How-it-Works
[6]It takes more than a Circuit Breaker to create a resilient application
https://developers.redhat.com/blog/2017/05/16/it-takes-more-than-a-circuit-breaker-to-create-a-resilient-application/
[7]Netflix/concurrency-limits
https://github.com/Netflix/concurrency-limits
[8]Istio
https://istio.io/
[9]Linkerd
https://linkerd.io/
[10]下一代的微服務架構基礎是ServiceMesh?
https://www.sohu.com/a/271138706_355140
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/71600.html
標籤:Go
上一篇:golang知識精要(一)
下一篇:Go語言開發環境搭建
