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

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