聊聊微服務(wù)的隔離和熔斷
今天來聊一聊微服務(wù)的隔離和熔斷是怎么做的, 如果你的項目沒有用微服務(wù),不要走開,可以看看對一個問題的解決思路。
按照碼農(nóng)翻身的慣例, 我們先用一個例子來拋出問題:
假設(shè)Tomcat線程池有100個線程, 每次有新的用戶請求過來,Tomcat就會從中找出一個空閑的線程去執(zhí)行, 拋開那些瑣碎的小細(xì)節(jié),這些請求其實非常簡單, 無非就是這么幾件事:
1. 根據(jù)用戶ID調(diào)用用戶服務(wù), 獲取用戶對象。
2. 獲取該用戶的推薦商品
3. 獲取該用戶的積分。
4. 把這些信息組合起來,返回給瀏覽器。
有意思的是前三件事情全是HTTP調(diào)用,需要調(diào)用某個地方的所謂“微服務(wù)”。
有一次,線程A去執(zhí)行幾個邏輯,等它調(diào)用“推薦服務(wù)”的時候,“推薦服務(wù)”遲遲沒有返回,線程A也許很高興, 終于可以休息了!
新的用戶請求源源不斷地到來,線程池中越來越多的線程都在等待推薦服務(wù)返回。
很快,100個線程全部用光,Tomcat只好掛出一個牌子: “系統(tǒng)繁忙,暫停營業(yè)。”
總之, 一個服務(wù)的出錯竟然導(dǎo)致了整個Tomcat不可用,實在是難以忍受。
也許你會和運維商量一下,來個簡單粗暴的辦法: 給Tomcat線程池在增加100個線程兄弟, 可是這不能解決問題, 在高并發(fā)的情況下, 只要那些遠(yuǎn)程的微服務(wù)有一個阻塞,無論多少線程,很快就會被用光。
于是,你只好重啟Tomcat,毀滅這個可愛的世界,但是重啟后問題還是有可能發(fā)生。
隔離
怎么把一個微服務(wù)的故障給隔離起來呢?讓他們互不影響呢?
Netflix的程序員們想了一個點子, 對每個微服務(wù),都分配一個線程池,像這樣:

比如說調(diào)用“推薦服務(wù)”的時候,就會從“推薦服務(wù)線程池” (假設(shè)有5個線程)中找到一個線程執(zhí)行。如果這個HTTP系統(tǒng)調(diào)用遲遲沒有返回,那這個線程就會一直等待,新的請求就需用使用池中別的線程。
如果5個線程都用光了,會發(fā)生什么情況?
這很簡單, 可以簡單地認(rèn)為這個服務(wù)不可用了!馬上返回,絕不等待。
這些新的線程池,是一種隔離的手段, 一個微服務(wù)一旦出了問題,很快就會被識別出來。
熔斷器
但是上面這種方案,還是有一定的問題,如果這個推薦服務(wù)已經(jīng)不可用了,還不斷地嘗試去調(diào)用,那肯定是一種浪費。
所以Netflix的程序員又想了一個辦法:使用熔斷器(也叫斷路器),注意:當(dāng)這個熔斷器關(guān)閉的時候,外面的請求可以直接調(diào)用,如果打開,就把外界的請求給阻斷了。
具體的做法是:系統(tǒng)會檢測請求失敗的比率(失敗數(shù)/總請求數(shù)), 一旦這個比率達(dá)到一個閾值的時候,熔斷器就開啟, 直接拒絕執(zhí)行用戶請求。然后休眠一段時間,嘗試放過一部分流量(比如一個請求),如果調(diào)用成功,熔斷器閉合,恢復(fù)到正常狀態(tài),否則繼續(xù)進(jìn)行休眠周期。
API
現(xiàn)在有了新的線程池,對程序員來講,該如何使用呢? 原來是這么做的:
- UserService service = ... 獲得用戶服務(wù)...
- User user = service.getUser(userID);
現(xiàn)在,為了利用新的線程池, 需要做一層封裝:
- UserService service = ... 獲得用戶服務(wù)...
- UserServiceCmd cmd = new UserServiceCmd(service, userID);
- User user = cmd.execute();
看到?jīng)]有? UserService 被封裝了一層, 放到了一個UserServiceCmd中去執(zhí)行。
這個Command代碼是這個樣子的:
- public class UserServiceCmd extends HystrixCommand<User> {
- 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執(zhí)行的時候,會使用另外一個線程池的線程去調(diào)用那個run()方法。
(注:這是一種同步調(diào)用,實際上還可以異步調(diào)用)
線程池的維護(hù)是在HystrixCommand這個父類中(命令模式),不需要程序員處理,程序員只需要告訴它: 我需要幾個線程,就可以了。
眼光敏銳的你也許已經(jīng)猜到,這里還采用了設(shè)計模式模板方法!
HystrixCommand它定義了一個抽象的方法: run(), 這個方法需要程序員去實現(xiàn)(例如前面的UserServiceCmd ), 父類的的execute方法會調(diào)用程序員寫的run()方法。
你也許還會注意到,還有一個叫做getFallback()的方法,這是干嘛用的?
其實前面的例子中我們只說道了線程池耗盡的時候,直接返回。 但是大部分情況下總得返回一點兒東西吧,比如UserServiceCmd,我們也許可以返回一個匿名的用戶給調(diào)用方。
這就是所謂的撤退,退卻(Fallback)邏輯。
當(dāng)然,這個邏輯也可以用在熔斷器開啟,調(diào)用失敗,超時等情況下。
一個粗略的、大致的流程圖是這樣的:
Netflix把這些功能(當(dāng)然,這里只是概要介紹,還有很多其他功能)給組裝起來,形成了一個開源的庫,叫做Hystrix,就是豪豬,渾身是刺,自我保護(hù),還是挺貼切的。
后記
剛寫完這個文章,就得到了一個”悲慘“的消息: Hystrix不再開發(fā)新功能,將進(jìn)入維護(hù)模式。 考慮到Hystrix巨大的使用量,學(xué)習(xí)它還是非常有價值的。
Netflix推薦大家轉(zhuǎn)向Resilience4j,看來又有新的玩具可以研究下了,興奮!
這是個相對新的項目,影響力和使用量現(xiàn)在還不能和Hystrix相比。
Resilience4j全面擁抱了Java 8和函數(shù)式編程, 他的核心功能包括:斷路器,限速,隔離(不再支持線程池),自動重試,響應(yīng)的緩存, 看,核心的功能還是類似的, resilience4j能發(fā)展到什么程度,我們拭目以待吧。
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請通過作者微信公眾號coderising獲取授權(quán)】