RPC框架的可靠性设计
1. 背景
1.1 分布式調(diào)用引入的故障
在傳統(tǒng)的單體架構(gòu)中,業(yè)務(wù)服務(wù)調(diào)用都是本地方法調(diào)用,不會涉及到網(wǎng)絡(luò)通信、協(xié)議棧、消息序列化和反序列化等,當(dāng)使用RPC框架將業(yè)務(wù)由單體架構(gòu)改造成分布式系統(tǒng)之后,本地方法調(diào)用將演變成跨進(jìn)程的遠(yuǎn)程調(diào)用,會引入一些新的故障點,如下所示:
圖1 RPC調(diào)用引入的潛在故障點新引入的潛在故障點包括:
1.消息的序列化和反序列化故障,例如,不支持的數(shù)據(jù)類型。
2.路由故障:包括服務(wù)的訂閱、發(fā)布故障,服務(wù)實例故障之后沒有及時刷新路由表,導(dǎo)致RPC調(diào)用仍然路由到故障節(jié)點。
3.網(wǎng)絡(luò)通信故障,包括網(wǎng)絡(luò)閃斷、網(wǎng)絡(luò)單通、丟包、客戶端浪涌接入等。
1.2 第三方服務(wù)依賴
RPC服務(wù)通常會依賴第三方服務(wù),包括數(shù)據(jù)庫服務(wù)、文件存儲服務(wù)、緩存服務(wù)、消息隊列服務(wù)等,這種第三方依賴同時也引入了潛在的故障:
1.網(wǎng)絡(luò)通信類故障,如果采用BIO調(diào)用第三方服務(wù),很有可能被阻塞。
2.“雪崩效用”導(dǎo)致的級聯(lián)故障,例如服務(wù)端處理慢導(dǎo)致客戶端線程被阻塞。
3.第三方不可用導(dǎo)致RPC調(diào)用失敗。
典型的第三方依賴示例如下:
圖2 RPC服務(wù)端的第三方依賴2. 通信層的可靠性設(shè)計
2.1 鏈路有效性檢測
當(dāng)網(wǎng)絡(luò)發(fā)生單通、連接被防火墻Hang住、長時間GC或者通信線程發(fā)生非預(yù)期異常時,會導(dǎo)致鏈路不可用且不易被及時發(fā)現(xiàn)。特別是異常發(fā)生在凌晨業(yè)務(wù)低谷期間,當(dāng)早晨業(yè)務(wù)高峰期到來時,由于鏈路不可用會導(dǎo)致瞬間的大批量業(yè)務(wù)失敗或者超時,這將對系統(tǒng)的可靠性產(chǎn)生重大的威脅。
從技術(shù)層面看,要解決鏈路的可靠性問題,必須周期性的對鏈路進(jìn)行有效性檢測。目前最流行和通用的做法就是心跳檢測。
心跳檢測機(jī)制分為三個層面:
1.TCP層面的心跳檢測,即TCP的Keep-Alive機(jī)制,它的作用域是整個TCP協(xié)議棧。
2.協(xié)議層的心跳檢測,主要存在于長連接協(xié)議中。例如MQTT協(xié)議。
3.應(yīng)用層的心跳檢測,它主要由各業(yè)務(wù)產(chǎn)品通過約定方式定時給對方發(fā)送心跳消息實現(xiàn)。
心跳檢測的目的就是確認(rèn)當(dāng)前鏈路可用,對方活著并且能夠正常接收和發(fā)送消息。做為高可靠的NIO框架,Netty也提供了心跳檢測機(jī)制,下面我們一起熟悉下心跳的檢測原理。
心跳檢測的原理示意圖如下:
圖3 鏈路心跳檢測不同的協(xié)議,心跳檢測機(jī)制也存在差異,歸納起來主要分為兩類:
1.Ping-Pong型心跳:由通信一方定時發(fā)送Ping消息,對方接收到Ping消息之后,立即返回Pong應(yīng)答消息給對方,屬于請求-響應(yīng)型心跳。
2.Ping-Ping型心跳:不區(qū)分心跳請求和應(yīng)答,由通信雙方按照約定定時向?qū)Ψ桨l(fā)送心跳Ping消息,它屬于雙向心跳。
心跳檢測策略如下:
1.連續(xù)N次心跳檢測都沒有收到對方的Pong應(yīng)答消息或者Ping請求消息,則認(rèn)為鏈路已經(jīng)發(fā)生邏輯失效,這被稱作心跳超時。
2.讀取和發(fā)送心跳消息的時候如何直接發(fā)生了IO異常,說明鏈路已經(jīng)失效,這被稱為心跳失敗。
無論發(fā)生心跳超時還是心跳失敗,都需要關(guān)閉鏈路,由客戶端發(fā)起重連操作,保證鏈路能夠恢復(fù)正常。
Netty的心跳檢測實際上是利用了鏈路空閑檢測機(jī)制實現(xiàn)的,它的空閑檢測機(jī)制分為三種:
1.讀空閑,鏈路持續(xù)時間t沒有讀取到任何消息。
2.寫空閑,鏈路持續(xù)時間t沒有發(fā)送任何消息。
3.讀寫空閑,鏈路持續(xù)時間t沒有接收或者發(fā)送任何消息。
Netty的默認(rèn)讀寫空閑機(jī)制是發(fā)生超時異常,關(guān)閉連接,但是,我們可以定制它的超時實現(xiàn)機(jī)制,以便支持不同的用戶場景,鏈路空閑接口定義如下:
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception { ctx.fireUserEventTriggered(evt); }鏈路空閑的時候并沒有關(guān)閉鏈路,而是觸發(fā)IdleStateEvent事件,用戶訂閱IdleStateEvent事件,用于自定義邏輯處理,例如關(guān)閉鏈路、客戶端發(fā)起重新連接、告警和打印日志等。利用Netty提供的鏈路空閑檢測機(jī)制,可以非常靈活的實現(xiàn)鏈路空閑時的有效性檢測。
2.2 客戶端斷連重連
當(dāng)發(fā)生如下異常時,客戶端需要釋放資源,重新發(fā)起連接:
1.服務(wù)端因為某種原因,主動關(guān)閉連接,客戶端檢測到鏈路被正常關(guān)閉。
2.服務(wù)端因為宕機(jī)等故障,強(qiáng)制關(guān)閉連接,客戶端檢測到鏈路被Rest掉。
3.心跳檢測超時,客戶端主動關(guān)閉連接。
4.客戶端因為其它原因(例如解碼失敗),強(qiáng)制關(guān)閉連接。
5.網(wǎng)絡(luò)類故障,例如網(wǎng)絡(luò)丟包、超時、單通等,導(dǎo)致鏈路中斷。
客戶端檢測到鏈路中斷后,等待INTERVAL時間,由客戶端發(fā)起重連操作,如果重連失敗,間隔周期INTERVAL后再次發(fā)起重連,直到重連成功。
為了保證服務(wù)端能夠有充足的時間釋放句柄資源,在首次斷連時客戶端需要等待INTERVAL時間之后再發(fā)起重連,而不是失敗后就立即重連。
為了保證句柄資源能夠及時釋放,無論什么場景下的重連失敗,客戶端都必須保證自身的資源被及時釋放,包括但不限于SocketChannel、Socket等。重連失敗后,需要打印異常堆棧信息,方便后續(xù)的問題定位。
利用Netty Channel提供的CloseFuture,可以非常方便的檢測鏈路狀態(tài),一旦鏈路關(guān)閉,相關(guān)事件即被觸發(fā),可以重新發(fā)起連接操作,代碼示例如下:
future.channel().closeFuture().sync(); } finally { // 所有資源釋放完成之后,清空資源,再次發(fā)起重連操作 executor.execute(new Runnable() { public void run() { try { TimeUnit.SECONDS.sleep(3);//3秒之后發(fā)起重連,等待句柄釋放 try { // 發(fā)起重連操作 connect(NettyConstant.PORT, NettyConstant.REMOTEIP); } catch (Exception e) { ......異常處理相關(guān)代碼省略 } });2.3 緩存重發(fā)
當(dāng)我們調(diào)用消息發(fā)送接口的時候,消息并沒有真正被寫入到Socket中,而是先放入NIO通信框架的消息發(fā)送隊列中,由Reactor線程掃描待發(fā)送的消息隊列,異步的發(fā)送給通信對端。假如很不幸,消息隊列中積壓了部分消息,此時鏈路中斷,這會導(dǎo)致部分消息并沒有真正發(fā)送給通信對端,示例如下:
圖4 鏈路中斷導(dǎo)致積壓消息沒有發(fā)送發(fā)生此故障時,我們希望NIO框架能夠自動實現(xiàn)消息緩存和重新發(fā)送,遺憾的是作為基礎(chǔ)的NIO通信框架,無論是Mina還是Netty,都沒有提供該功能,需要通信框架自己封裝實現(xiàn),基于Netty的實現(xiàn)策略如下:
1.調(diào)用Netty ChannelHandlerContext的write方法時,返回ChannelFuture對象,我們在ChannelFuture中注冊發(fā)送結(jié)果監(jiān)聽Listener。
2.在Listener的operationComplete方法中判斷操作結(jié)果,如果操作不成功,將之前發(fā)送的消息對象添加到重發(fā)隊列中。
3.鏈路重連成功之后,根據(jù)策略,將緩存隊列中的消息重新發(fā)送給通信對端。
需要指出的是,并非所有場景都需要通信框架做重發(fā),例如服務(wù)框架的客戶端,如果某個服務(wù)提供者不可用,會自動切換到下一個可用的服務(wù)提供者之上。假定是鏈路中斷導(dǎo)致的服務(wù)提供者不可用,即便鏈路重新恢復(fù),也沒有必要將之前積壓的消息重新發(fā)送,因為消息已經(jīng)通過FailOver機(jī)制切換到另一個服務(wù)提供者處理。所以,消息緩存重發(fā)只是一種策略,通信框架應(yīng)該支持鏈路級重發(fā)策略。
2.4 客戶端超時保護(hù)
在傳統(tǒng)的同步阻塞編程模式下,客戶端Socket發(fā)起網(wǎng)絡(luò)連接,往往需要指定連接超時時間,這樣做的目的主要有兩個:
1.在同步阻塞I/O模型中,連接操作是同步阻塞的,如果不設(shè)置超時時間,客戶端I/O線程可能會被長時間阻塞,這會導(dǎo)致系統(tǒng)可用I/O線程數(shù)的減少。
2.業(yè)務(wù)層需要:大多數(shù)系統(tǒng)都會對業(yè)務(wù)流程執(zhí)行時間有限制,例如WEB交互類的響應(yīng)時間要小于3S。客戶端設(shè)置連接超時時間是為了實現(xiàn)業(yè)務(wù)層的超時。
對于NIO的SocketChannel,在非阻塞模式下,它會直接返回連接結(jié)果,如果沒有連接成功,也沒有發(fā)生I/O異常,則需要將SocketChannel注冊到Selector上監(jiān)聽連接結(jié)果。所以,異步連接的超時無法在API層面直接設(shè)置,而是需要通過用戶自定義定時器來主動監(jiān)測。
Netty在創(chuàng)建NIO客戶端時,支持設(shè)置連接超時參數(shù)。Netty的客戶端連接超時參數(shù)與其它常用的TCP參數(shù)一起配置,使用起來非常方便,上層用戶不用關(guān)心底層的超時實現(xiàn)機(jī)制。這既滿足了用戶的個性化需求,又實現(xiàn)了故障的分層隔離。
2.5 針對客戶端的并發(fā)連接數(shù)流控
以Netty的HTTPS服務(wù)端為例,針對客戶端的并發(fā)連接數(shù)流控原理如下所示:
圖5 服務(wù)端HTTS連接數(shù)流控基于Netty的Pipeline機(jī)制,可以對SSL握手成功、SSL連接關(guān)閉做切面攔截(類似于Spring的AOP機(jī)制,但是沒采用反射機(jī)制,性能更高),通過流控切面接口,對HTTPS連接做計數(shù),根據(jù)計數(shù)器做流控,服務(wù)端的流控算法如下:
1.獲取流控閾值。
2.從全局上下文中獲取當(dāng)前的并發(fā)連接數(shù),與流控閾值對比,如果小于流控閾值,則對當(dāng)前的計數(shù)器做原子自增,允許客戶端連接接入。
3.如果等于或者大于流控閾值,則拋出流控異常給客戶端。
4.SSL連接關(guān)閉時,獲取上下文中的并發(fā)連接數(shù),做原子自減。
在實現(xiàn)服務(wù)端流控時,需要注意如下幾點:
1.流控的ChannelHandler聲明為@ChannelHandler.Sharable,這樣全局創(chuàng)建一個流控實例,就可以在所有的SSL連接中共享。
2.通過userEventTriggered方法攔截SslHandshakeCompletionEvent和SslCloseCompletionEvent事件,在SSL握手成功和SSL連接關(guān)閉時更新流控計數(shù)器。
3.流控并不是單針對ESTABLISHED狀態(tài)的HTTP連接,而是針對所有狀態(tài)的連接,因為客戶端關(guān)閉連接,并不意味著服務(wù)端也同時關(guān)閉了連接,只有SslCloseCompletionEvent事件觸發(fā)時,服務(wù)端才真正的關(guān)閉了NioSocketChannel,GC才會回收連接關(guān)聯(lián)的內(nèi)存。
4.流控ChannelHandler會被多個NioEventLoop線程調(diào)用,因此對于相關(guān)的計數(shù)器更新等操作,要保證并發(fā)安全性,避免使用全局鎖,可以通過原子類等提升性能。
2.6 內(nèi)存保護(hù)
NIO通信的內(nèi)存保護(hù)主要集中在如下幾點:
1.鏈路總數(shù)的控制:每條鏈路都包含接收和發(fā)送緩沖區(qū),鏈路個數(shù)太多容易導(dǎo)致內(nèi)存溢出。
2.單個緩沖區(qū)的上限控制:防止非法長度或者消息過大導(dǎo)致內(nèi)存溢出。
3.緩沖區(qū)內(nèi)存釋放:防止因為緩沖區(qū)使用不當(dāng)導(dǎo)致的內(nèi)存泄露。
4.NIO消息發(fā)送隊列的長度上限控制。
當(dāng)我們對消息進(jìn)行解碼的時候,需要創(chuàng)建緩沖區(qū)。緩沖區(qū)的創(chuàng)建方式通常有兩種:
1.容量預(yù)分配,在實際讀寫過程中如果不夠再擴(kuò)展。
2.根據(jù)協(xié)議消息長度創(chuàng)建緩沖區(qū)。
在實際的商用環(huán)境中,如果遇到畸形碼流攻擊、協(xié)議消息編碼異常、消息丟包等問題時,可能會解析到一個超長的長度字段。筆者曾經(jīng)遇到過類似問題,報文長度字段值竟然是2G多,由于代碼的一個分支沒有對長度上限做有效保護(hù),結(jié)果導(dǎo)致內(nèi)存溢出。系統(tǒng)重啟后幾秒內(nèi)再次內(nèi)存溢出,幸好及時定位出問題根因,險些釀成嚴(yán)重的事故。
Netty提供了編解碼框架,因此對于解碼緩沖區(qū)的上限保護(hù)就顯得非常重要。下面,我們看下Netty是如何對緩沖區(qū)進(jìn)行上限保護(hù)的:
首先,在內(nèi)存分配的時候指定緩沖區(qū)長度上限:
/** * Allocate a {@link ByteBuf} with the given initial capacity and the given * maximal capacity. If it is a direct or heap buffer depends on the actual * implementation. */ ByteBuf buffer(int initialCapacity, int maxCapacity);其次,在對緩沖區(qū)進(jìn)行寫入操作的時候,如果緩沖區(qū)容量不足需要擴(kuò)展,首先對最大容量進(jìn)行判斷,如果擴(kuò)展后的容量超過上限,則拒絕擴(kuò)展:
@Override public ByteBuf capacity(int newCapacity) { ensureAccessible(); if (newCapacity \u0026lt; 0 || newCapacity \u0026gt; maxCapacity()) { throw new IllegalArgumentException(\u0026quot;newCapacity: \u0026quot; + newCapacity); }在消息解碼的時候,對消息長度進(jìn)行判斷,如果超過最大容量上限,則拋出解碼異常,拒絕分配內(nèi)存,以LengthFieldBasedFrameDecoder的decode方法為例進(jìn)行說明:
if (frameLength \u0026gt; maxFrameLength) { long discard = frameLength - in.readableBytes(); tooLongFrameLength = frameLength; if (discard \u0026lt; 0) { in.skipBytes((int) frameLength); } else { discardingTooLongFrame = true; bytesToDiscard = discard; in.skipBytes(in.readableBytes()); } failIfNecessary(true); return null; }3. RPC調(diào)用層的可靠性設(shè)計
3.1 RPC調(diào)用異常場景
RPC調(diào)用過程中除了通信層的異常,通常也會遇到如下幾種故障:
服務(wù)路由失敗。
服務(wù)端超時。
服務(wù)端調(diào)用失敗。
RPC框架需要能夠針對上述常見的異常做容錯處理,以提升業(yè)務(wù)調(diào)用的可靠性。
3.1.1 服務(wù)路由失敗
RPC客戶端通常會基于訂閱/發(fā)布的機(jī)制獲取服務(wù)端的地址列表,并將其緩存到本地,RPC調(diào)用時,根據(jù)負(fù)載均衡策略從本地緩存的路由表中獲取到一個唯一的服務(wù)端節(jié)點發(fā)起調(diào)用,原理如下所示:
圖6 基于訂閱發(fā)布機(jī)制的RPC調(diào)用通過緩存的機(jī)制能夠提升RPC調(diào)用的性能,RPC客戶端不需要每次調(diào)用都向注冊中心查詢目標(biāo)服務(wù)的地址信息,但是也可能會發(fā)生如下兩類潛在故障:
1.某個RPC服務(wù)端發(fā)生故障,或者下線,客戶端沒有及時刷新本地緩存的服務(wù)地址列表,就會導(dǎo)致RPC調(diào)用失敗。
2.RPC客戶端和服務(wù)端都工作正常,但是RPC客戶端和服務(wù)端的連接或者網(wǎng)絡(luò)發(fā)生了故障,如果沒有鏈路的可靠性檢測機(jī)制,就會導(dǎo)致RPC調(diào)用失敗。
3.1.2 服務(wù)端超時
當(dāng)服務(wù)端無法在指定的時間內(nèi)返回應(yīng)答給客戶端,就會發(fā)生超時,導(dǎo)致超時的原因主要有:
1.服務(wù)端的I/O線程沒有及時從網(wǎng)絡(luò)中讀取客戶端請求消息,導(dǎo)致該問題的原因通常是I/O線程被意外阻塞或者執(zhí)行長周期操作。
2.服務(wù)端業(yè)務(wù)處理緩慢,或者被長時間阻塞,例如查詢數(shù)據(jù)庫,由于沒有索引導(dǎo)致全表查詢,耗時較長。
3.服務(wù)端發(fā)生長時間Full GC,導(dǎo)致所有業(yè)務(wù)線程暫停運(yùn)行,無法及時返回應(yīng)答給客戶端。
3.1.3 服務(wù)端調(diào)用失敗
有時會發(fā)生服務(wù)端調(diào)用失敗,導(dǎo)致服務(wù)端調(diào)用失敗的原因主要有如下幾種:
1.服務(wù)端解碼失敗,會返回消息解碼失敗異常。
2.服務(wù)端發(fā)生動態(tài)流控,返回流控異常。
3.服務(wù)端消息隊列積壓率超過最大閾值,返回系統(tǒng)擁塞異常。
4.訪問權(quán)限校驗失敗,返回權(quán)限相關(guān)異常。
5.違反SLA策略,返回SLA控制相關(guān)異常。
6.其他系統(tǒng)異常。
需要指出的是,服務(wù)調(diào)用異常不包括業(yè)務(wù)層面的處理異常,例如數(shù)據(jù)庫操作異常、用戶記錄不存在異常等。
3.2 RPC調(diào)用可靠性方案
3.2.1注冊中心與鏈路檢測雙保險機(jī)制
因為注冊中心有集群內(nèi)所有RPC客戶端和服務(wù)端的實例信息,因此通過注冊中心向每個服務(wù)端和客戶端發(fā)送心跳消息,檢測對方是否在線,如果連續(xù)N次心跳超時,或者心跳發(fā)送失敗,則判斷對方已經(jīng)發(fā)生故障或者下線(下線可以通過優(yōu)雅停機(jī)的方式主動告知注冊中心,實時性會更好)。注冊中心將故障節(jié)點的服務(wù)實例信息通過心跳消息發(fā)送給客戶端,由客戶端將故障的服務(wù)實例信息從本地緩存的路由表中刪除,后續(xù)消息調(diào)用不再路由到該節(jié)點。
在一些特殊場景下,盡管注冊中心與服務(wù)端、客戶端的連接都沒有問題,但是服務(wù)端和客戶端之間的鏈路發(fā)生了異常,由于發(fā)生鏈路異常的服務(wù)端仍然在緩存表中,因此消息還會繼續(xù)調(diào)度到故障節(jié)點上,所以,利用RPC客戶端和服務(wù)端之間的雙向心跳檢測,可以及時發(fā)現(xiàn)雙方之間的鏈路問題,利用重連等機(jī)制可以快速的恢復(fù)連接,如果重連N次都失敗,則服務(wù)路由時不再將消息發(fā)送到連接故障的服務(wù)節(jié)點上。
利用注冊中心對服務(wù)端的心跳檢測和通知機(jī)制、以及服務(wù)端和客戶端針對鏈路層的雙向心跳檢測機(jī)制,可以有效檢測出故障節(jié)點,提升RPC調(diào)用的可靠性,它的原理如下所示:
圖7 注冊中心與鏈路雙向心跳檢測機(jī)制原理3.2.2 集群容錯策略
常用的集群容錯策略包括:
1.失敗自動切換(Failover)。
2.失敗通知(Failback)。
3.失敗緩存(Failcache)。
4.快速失敗(Failfast)。
失敗自動切換策略:服務(wù)調(diào)用失敗自動切換策略指的是當(dāng)發(fā)生RPC調(diào)用異常時,重新選路,查找下一個可用的服務(wù)提供者。
服務(wù)發(fā)布的時候,可以指定服務(wù)的集群容錯策略。消費(fèi)者可以覆蓋服務(wù)提供者的通用配置,實現(xiàn)個性化的容錯策略。
Failover策略的設(shè)計思路如下:消費(fèi)者路由操作完成之后,獲得目標(biāo)地址,調(diào)用通信框架的消息發(fā)送接口發(fā)送請求,監(jiān)聽服務(wù)端應(yīng)答。如果返回的結(jié)果是RPC調(diào)用異常(超時、流控、解碼失敗等系統(tǒng)異常),根據(jù)消費(fèi)者集群容錯的策略進(jìn)行容錯路由,如果是Failover,則重新返回到路由Handler的入口,從路由節(jié)點繼續(xù)執(zhí)行。選路完成之后,對目標(biāo)地址進(jìn)行比對,防止重新路由到故障服務(wù)節(jié)點,過濾掉上次的故障服務(wù)提供者之后,調(diào)用通信框架的消息發(fā)送接口發(fā)送請求消息。
RPC框架提供Failover容錯策略,但是用戶在使用時需要自己保證用對地方,下面對Failover策略的應(yīng)用場景進(jìn)行總結(jié):
1.讀操作,因為通常它是冪等的。
2.冪等性服務(wù),保證調(diào)用1次與N次效果相同。
需要特別指出的是,失敗重試會增加服務(wù)調(diào)用時延,因此框架必須對失敗重試的最大次數(shù)做限制,通常默認(rèn)為3,防止無限制重試導(dǎo)致服務(wù)調(diào)用時延不可控。
失敗通知(Failback):在很多業(yè)務(wù)場景中,客戶端需要能夠獲取到服務(wù)調(diào)用失敗的具體信息,通過對失敗錯誤碼等異常信息的判斷,決定后續(xù)的執(zhí)行策略,例如非冪等性的服務(wù)調(diào)用。
Failback的設(shè)計方案如下:RPC框架獲取到服務(wù)提供者返回的RPC異常響應(yīng)之后,根據(jù)策略進(jìn)行容錯。如果是Failback模式,則不再重試其它服務(wù)提供者,而是將RPC異常通知給客戶端,由客戶端捕獲異常進(jìn)行后續(xù)處理。
失敗緩存(Failcache):Failcache策略是失敗自動恢復(fù)的一種,在實際項目中它的應(yīng)用場景如下:
1.服務(wù)有狀態(tài)路由,必須定點發(fā)送到指定的服務(wù)提供者。當(dāng)發(fā)生鏈路中斷、流控等服務(wù)暫時不可用時,RPC框架將消息臨時緩存起來,等待周期T,重新發(fā)送,直到服務(wù)提供者能夠正常處理該消息。
2.對時延要求不敏感的服務(wù)。系統(tǒng)服務(wù)調(diào)用失敗,通常是鏈路暫時不可用、服務(wù)流控、GC掛住服務(wù)提供者進(jìn)程等,這種失敗不是永久性的失敗,它的恢復(fù)是可預(yù)期的。如果客戶端對服務(wù)調(diào)用時延不敏感,可以考慮采用自動恢復(fù)模式,即先緩存,再等待,最后重試。
3.通知類服務(wù)。例如通知粉絲積分增長、記錄接口日志等,對服務(wù)調(diào)用的實時性要求不高,可以容忍自動恢復(fù)帶來的時延增加。
為了保證可靠性,Failcache策略在設(shè)計的時候需要考慮如下幾個要素:
1.緩存時間、緩存對象上限數(shù)等需要做出限制,防止內(nèi)存溢出。
2.緩存淘汰算法的選擇,是否支持用戶配置。
3.定時重試的周期T、重試的最大次數(shù)等需要做出限制并支持用戶指定。
4.重試達(dá)到最大上限仍失敗,需要丟棄消息,記錄異常日志。
快速失敗(Failfast):在業(yè)務(wù)高峰期,對于一些非核心的服務(wù),希望只調(diào)用一次,失敗也不再重試,為重要的核心服務(wù)節(jié)約寶貴的運(yùn)行資源。此時,快速失敗是個不錯的選擇。快速失敗策略的設(shè)計比較簡單,獲取到服務(wù)調(diào)用異常之后,直接忽略異常,記錄異常日志。
4. 第三方服務(wù)依賴故障隔離
4.1 總體策略
盡管很多第三方服務(wù)會提供SLA,但是RPC服務(wù)本身并不能完全依賴第三方服務(wù)自身的可靠性來保障自己的高可靠,第三方服務(wù)依賴隔離的總體策略如下:
1.第三方依賴隔離可以采用線程池 + 響應(yīng)式編程(例如RxJava)的方式實現(xiàn)。
2.對第三方依賴進(jìn)行分類,每種依賴對應(yīng)一個獨立的線程/線程池。
3.服務(wù)不直接調(diào)用第三方依賴的API,而是使用異步封裝之后的API接口。
4.異步調(diào)用第三方依賴API之后,獲取Future對象。利用響應(yīng)式編程框架,
可以訂閱后續(xù)的事件,接收響應(yīng),針對響應(yīng)進(jìn)行編程。
4.2 異步化
如果第三方服務(wù)提供的是標(biāo)準(zhǔn)的HTTP/Restful服務(wù),則利用異步HTTP客戶端,例如Netty、Vert.x、異步RestTemplate等發(fā)起異步服務(wù)調(diào)用,這樣無論是服務(wù)端自身處理慢還是網(wǎng)絡(luò)慢,都不會導(dǎo)致調(diào)用方被阻塞。
如果對方是私有或者定制化的協(xié)議,SDK沒有提供異步接口,則需要采用線程池或者利用一些開源框架實現(xiàn)故障隔離。
異步化示例圖如下所示:
圖8 異步化原理示意圖異步化的幾個關(guān)鍵技術(shù)點:
1.異步具有依賴和傳遞性,如果想在某個業(yè)務(wù)流程的某個過程中做異步化,則入口處就需要做異步。例如如果想把Redis服務(wù)調(diào)用改造成異步,則調(diào)用Redis服務(wù)之前的流程也需要同時做異步化,否則意義不大(除非調(diào)用方不需要返回值)。
2.通常而言,全棧異步對于業(yè)務(wù)性能和可靠性提升的意義更大,全棧異步會涉及到內(nèi)部服務(wù)調(diào)用、第三方服務(wù)調(diào)用、數(shù)據(jù)庫、緩存等平臺中間件服務(wù)的調(diào)用,異步化改造成本比較高,但是收益也比較明顯。
3.不同框架、服務(wù)的異步編程模型盡量保持一致,例如統(tǒng)一采取RxJava風(fēng)格的接口、或者JDK8的CompletableFuture。如果不同服務(wù)SDK的異步API接口風(fēng)格差異過大,會增加業(yè)務(wù)的開發(fā)成本,也不利用線程模型的歸并和整合。
4.3.基于Hystrix的第三方依賴故障隔離
集成Netflix開源的Hystrix框架,可以非常方便的實現(xiàn)第三方服務(wù)依賴故障隔離,它提供的主要功能包括:
1.依賴隔離。
2.熔斷器。
3.優(yōu)雅降級。
4.Reactive編程。
5.信號量隔離。
建議的集成策略如下:
1.第三方依賴隔離:使用HystrixCommand做一層異步封裝,實現(xiàn)業(yè)務(wù)的RPC服務(wù)調(diào)用線程和第三方依賴的線程隔離。
2.依賴分類管理:對第三方依賴進(jìn)行分類、分組管理,根據(jù)依賴的特點設(shè)置熔斷策略、優(yōu)雅降級策略、超時策略等,以實現(xiàn)差異化的處理。
總體集成視圖如下所示:
圖9 基于Hystrix的第三方故障隔離框架基于Hystrix可以非常方便的實現(xiàn)第三方依賴服務(wù)的熔斷降級,它的工作原理如下:
1.熔斷判斷:服務(wù)調(diào)用時,對熔斷開關(guān)狀態(tài)進(jìn)行判斷,當(dāng)熔斷器開關(guān)關(guān)閉時, 請求被允許通過熔斷器。
2.熔斷執(zhí)行:當(dāng)熔斷器開關(guān)打開時,服務(wù)調(diào)用請求被禁止通過,執(zhí)行失敗回調(diào)接口。
3.自動恢復(fù):熔斷之后,周期T之后允許一條消息通過,如果成功,則取消熔斷狀態(tài),否則繼續(xù)處于熔斷狀態(tài)。
流程如下所示:
圖10 基于Hystrix的熔斷降級5. 作者簡介
李林鋒,10年Java NIO、平臺中間件設(shè)計和開發(fā)經(jīng)驗,精通Netty、Mina、分布式服務(wù)框架、API Gateway、PaaS等,《Netty進(jìn)階之路》、《分布式服務(wù)框架原理與實踐》作者。目前在華為終端應(yīng)用市場負(fù)責(zé)業(yè)務(wù)微服務(wù)化、云化、全球化等相關(guān)設(shè)計和開發(fā)工作。
聯(lián)系方式:新浪微博 Nettying 微信:Nettying
Email:neu_lilinfeng@sina.com
總結(jié)
以上是生活随笔為你收集整理的RPC框架的可靠性设计的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [洛谷P1822]魔法指纹
- 下一篇: 爆破专业学生任母校爆破工作总指挥走红