1. 程式人生 > >微服務:隔離和熔斷到底怎麼實現的??

微服務:隔離和熔斷到底怎麼實現的??

聊聊微服務的隔離和熔斷

原創: 碼農翻身劉欣 碼農翻身 原文連結:https://mp.weixin.qq.com/s/PmU14UsJOb4IiH_81RlJMA
今天來聊一聊微服務的隔離和熔斷是怎麼做的, 如果你的專案沒有用微服務,不要走開,可以看看對一個問題的解決思路。 如果實在是不感興趣,直接拉到文末去抽獎吧。

按照碼農翻身的慣例, 我們先用一個例子來丟擲問題:

假設Tomcat執行緒池有100個執行緒, 每次有新的使用者請求過來,Tomcat就會從中找出一個空閒的執行緒去執行, 拋開那些瑣碎的小細節,這些請求其實非常簡單, 無非就是這麼幾件事:

  1. 根據使用者ID呼叫使用者服務, 獲取使用者物件。

  2. 獲取該使用者的推薦商品

  3. 獲取該使用者的積分。

  4. 把這些資訊組合起來,返回給瀏覽器。

有意思的是前三件事情全是HTTP呼叫,需要呼叫某個地方的所謂“微服務”。

在這裡插入圖片描述

有一次,執行緒A去執行幾個邏輯,等它呼叫“推薦服務”的時候,“推薦服務”遲遲沒有返回,執行緒A也許很高興, 終於可以休息了!

新的使用者請求源源不斷地到來,執行緒池中越來越多的執行緒都在等待推薦服務返回。

很快,100個執行緒全部用光,Tomcat只好掛出一個牌子: “系統繁忙,暫停營業。”

總之, 一個服務的出錯竟然導致了整個Tomcat不可用,實在是難以忍受。

也許你會和運維商量一下,來個簡單粗暴的辦法: 給Tomcat執行緒池在增加100個執行緒兄弟, 可是這不能解決問題, 在高併發的情況下, 只要那些遠端的微服務有一個阻塞,無論多少執行緒,很快就會被用光。

於是,你只好重啟Tomcat,毀滅這個可愛的世界,但是重啟後問題還是有可能發生。

隔離

怎麼把一個微服務的故障給隔離起來呢?讓他們互不影響呢?

Netflix的程式設計師們想了一個點子, 對每個微服務,都分配一個執行緒池,像這樣:
在這裡插入圖片描述

比如說呼叫“推薦服務”的時候,就會從“推薦服務執行緒池” (假設有5個執行緒)中找到一個執行緒執行。如果這個HTTP系統呼叫遲遲沒有返回,那這個執行緒就會一直等待,新的請求就需用使用池中別的執行緒。

如果5個執行緒都用光了,會發生什麼情況?

這很簡單, 可以簡單地認為這個服務不可用了!馬上返回,絕不等待。
在這裡插入圖片描述

這些新的執行緒池,是一種隔離的手段, 一個微服務一旦出了問題,很快就會被識別出來。

熔斷器

但是上面這種方案,還是有一定的問題,如果這個推薦服務已經不可用了,還不斷地嘗試去呼叫,那肯定是一種浪費。

所以Netflix的程式設計師又想了一個辦法:使用熔斷器(也叫斷路器),注意:當這個熔斷器關閉的時候,外面的請求可以直接呼叫,如果開啟,就把外界的請求給阻斷了。

具體的做法是:系統會檢測請求失敗的比率(失敗數/總請求數), 一旦這個比率達到一個閾值的時候,熔斷器就開啟, 直接拒絕執行使用者請求。然後休眠一段時間,嘗試放過一部分流量(比如一個請求),如果呼叫成功,熔斷器閉合,恢復到正常狀態,否則繼續進行休眠週期。

API

現在有了新的執行緒池,對程式設計師來講,該如何使用呢? 原來是這麼做的:

UserService service = … 獲得使用者服務…
User user = service.getUser(userID);

現在,為了利用新的執行緒池, 需要做一層封裝:

UserService service = … 獲得使用者服務…
UserServiceCmd cmd = new UserServiceCmd(service, userID);
User user = cmd.execute();

看到沒有? UserService 被封裝了一層, 放到了一個UserServiceCmd中去執行。

這個Command程式碼是這個樣子的:

public class UserServiceCmd extends HystrixCommand {
private UserService userService = null;
private String userID = null;
……

public UserServiceCmd(UserService userService,
String userID) {
……
this.userService = userService;
this.userID = userID;
}

@Override
protected User run(){
return userService.getUser(userID);
}

@Override
protected User getFallback() {
return annonymousUser;
}
}

看起來非常簡單吧, 可是背後的魔法是什麼呢?

實際上,在這個UserServiceCmd執行的時候,會使用另外一個執行緒池的執行緒去呼叫那個run()方法。
在這裡插入圖片描述

(注:這是一種同步呼叫,實際上還可以非同步呼叫)

執行緒池的維護是在HystrixCommand這個父類中(命令模式),不需要程式設計師處理,程式設計師只需要告訴它: 我需要幾個執行緒,就可以了。

眼光敏銳的你也許已經猜到,這裡還採用了設計模式模板方法!

HystrixCommand它定義了一個抽象的方法: run(), 這個方法需要程式設計師去實現(例如前面的UserServiceCmd ), 父類的的execute方法會呼叫程式設計師寫的run()方法。
在這裡插入圖片描述

你也許還會注意到,還有一個叫做getFallback()的方法,這是幹嘛用的?

其實前面的例子中我們只說道了執行緒池耗盡的時候,直接返回。 但是大部分情況下總得返回一點兒東西吧,比如UserServiceCmd,我們也許可以返回一個匿名的使用者給呼叫方。

這就是所謂的撤退,退卻(Fallback)邏輯。

當然,這個邏輯也可以用在熔斷器開啟,呼叫失敗,超時等情況下。

一個粗略的、大致的流程圖是這樣的:
在這裡插入圖片描述

Netflix把這些功能(當然,這裡只是概要介紹,還有很多其他功能)給組裝起來,形成了一個開源的庫,叫做Hystrix,就是豪豬,渾身是刺,自我保護,還是挺貼切的。
在這裡插入圖片描述

後記

剛寫完這個文章,就得到了一個”悲慘“的訊息: Hystrix不再開發新功能,將進入維護模式。 考慮到Hystrix巨大的使用量,學習它還是非常有價值的。

Netflix推薦大家轉向Resilience4j,看來又有新的玩具可以研究下了,興奮!

這是個相對新的專案,影響力和使用量現在還不能和Hystrix相比。

Resilience4j全面擁抱了Java 8和函數語言程式設計, 他的核心功能包括:斷路器,限速,隔離(不再支援執行緒池),自動重試,響應的快取, 看,核心的功能還是類似的, resilience4j能發展到什麼程度,我們拭目以待吧。