胡喜明,胡 淼
(杭州電子科技大學(xué) 通信工程學(xué)院,浙江 杭州 310018)
當(dāng)前互聯(lián)網(wǎng)開發(fā)中,網(wǎng)站業(yè)務(wù)流量的增加以及業(yè)務(wù)復(fù)雜度的提升使得傳統(tǒng)單體架構(gòu)不足以滿足當(dāng)前的需求,大部分互聯(lián)網(wǎng)開發(fā)者采用分布式架構(gòu)來構(gòu)建當(dāng)前復(fù)雜的業(yè)務(wù)[1]。該架構(gòu)中采用業(yè)務(wù)拆分的理念,根據(jù)功能拆分為多個服務(wù),通過RPC系統(tǒng)實現(xiàn)業(yè)務(wù)間通信,該系統(tǒng)肩負(fù)著服務(wù)間協(xié)調(diào)以及數(shù)據(jù)交換的責(zé)任[2]。
響應(yīng)式編程作為一種新生的編程范式,提供了基于事件驅(qū)動的形式對異步化數(shù)據(jù)進行處理。該范式下允許開發(fā)人員去實現(xiàn)擁有時間驅(qū)動、異步化、可擴展的響應(yīng)式系統(tǒng)。該范式下認(rèn)為同步阻塞的開發(fā)形式是對資源的一種浪費,而原生異步化雖然能解決單一的事務(wù)異步問題,但是對多個事務(wù)組合性回調(diào)問題無法處理,因此引入響應(yīng)式編程范式來解決該問題[3]。當(dāng)前幾乎所有的開發(fā)語言和框架都在向著響應(yīng)式編程跟進,Java在1.8后也引入了響應(yīng)式編程的開發(fā)模式,然而當(dāng)前主流的RPC系統(tǒng)均采用Netty作為底層通訊框架,該框架無法支持響應(yīng)式方法的遠(yuǎn)程調(diào)用,使得響應(yīng)式編程在分布式項目中實現(xiàn)難度加大[4]。
為解決該問題,本文提出了一種基于響應(yīng)式的RPC系統(tǒng),系統(tǒng)中采用基于響應(yīng)式編程的Reactor-netty組件作為跨進程通信手段,實現(xiàn)服務(wù)間非阻塞調(diào)用。項目中還設(shè)計了一種動態(tài)負(fù)載均衡策略,實現(xiàn)根據(jù)服務(wù)調(diào)用情況的動態(tài)化選擇。該系統(tǒng)采用優(yōu)化后的SPI機制實現(xiàn)良好的功能擴展。最后,通過與Netty作為底層通信框架的系統(tǒng)對比,該系統(tǒng)具有明顯的性能優(yōu)勢。
響應(yīng)式編程是一種關(guān)注傳輸中的數(shù)據(jù)流以及數(shù)據(jù)變化傳遞的異步編程模式,這也意味著可以應(yīng)用編程語言進行靜態(tài)和動態(tài)數(shù)據(jù)流的表示。Reactor是響應(yīng)式編程的第四代函數(shù)庫,同時也是一種為了實現(xiàn)響應(yīng)式形式的具體編程規(guī)范,其主要為了解決jvm中異步方法的缺點,具有協(xié)調(diào)多個異步任務(wù)、較好的可讀性、豐富的數(shù)據(jù)流運算符、懶加載訂閱模式、背壓等優(yōu)點。該框架主要用于在JVM平臺上基于響應(yīng)式流規(guī)范構(gòu)建非阻塞異步應(yīng)用。java語言在1.8版本后引入了Reactor框架,其中可組合形式的反應(yīng)類型Flux和Mono是其核心對象。Flux對象表示0到N項的反應(yīng)序列,而一個Mono對象表示單值或空(0或1)的結(jié)果[3]。
Reactor庫函數(shù)的下屬分支Reactor-netty作為響應(yīng)式編程家族的一員支持上述兩種反應(yīng)類型的封裝,其底層基于Netty框架,并對netty進行響應(yīng)式編程封裝,將其轉(zhuǎn)換為異步事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用程序框架。Reactor-netty內(nèi)部仍然保留了Netty的主從多線程模型,擁有Netty框架的全部優(yōu)勢[5]。同時Reactor-netty內(nèi)部直接繼承了Java函數(shù)式的API接口,支持HTTP、TCP等協(xié)議的調(diào)用。Reactor-netty 通過Reactor Streams中的背壓理論進行數(shù)據(jù)流量控制,發(fā)布者和訂閱者可以進行數(shù)據(jù)流量協(xié)商,保證服務(wù)高質(zhì)量傳輸。其中背壓分為4種策略:
(1)OnBackpressureBuffer策略:對下游的請求數(shù)據(jù)采用緩存的形式,保證系統(tǒng)不會壓力過大。
(2)OnBackpressureDrop策略:元素就緒時,根據(jù)下游是否有未滿足的request來發(fā)出當(dāng)前數(shù)據(jù)。
(3)OnBackpressureLatest策略:一直發(fā)送當(dāng)前最新的數(shù)據(jù)。
(4)OnBackpressureError策略:當(dāng)前數(shù)據(jù)已經(jīng)滿了,再次添加請求直接報錯[3]。
圖1為OnBackpressureError策略的背壓原理,當(dāng)訂閱者的消費能力遠(yuǎn)小于發(fā)布者,訂閱者可以通知發(fā)布者進行服務(wù)的取消和終止功能,保證傳輸數(shù)據(jù)流量合理。
圖1 OnBackpressureError策略背壓原理
RPC是分布式系統(tǒng)的核心通信組件,肩負(fù)著實現(xiàn)高效服務(wù)間通信的職責(zé)[6]。如圖2所示,一個完整的RPC流程包括3類角色:服務(wù)提供者、服務(wù)消費者以及服務(wù)注冊中心[7-9]。分布式系統(tǒng)的服務(wù)調(diào)用流程如下:
圖2 RPC架構(gòu)
(1)服務(wù)提供方實現(xiàn)相應(yīng)的發(fā)布接口,并將服務(wù)信息注冊到注冊中心。
(2)注冊中心接收到注冊信息后,對信息進行內(nèi)部存儲并開啟服務(wù)監(jiān)控功能,保證服務(wù)下線后及時通知服務(wù)消費方。
(3)服務(wù)消費方向注冊中心發(fā)送服務(wù)訂閱請求,通過注冊中心所提供的信息在本地實現(xiàn)負(fù)載均衡選取指定地址。
(4)獲取到指定地址后,服務(wù)消費者向該地址發(fā)送服務(wù)調(diào)用請求,服務(wù)提供方對接收到的請求進行解析并向消費方返回請求結(jié)果[10]。
圖3為響應(yīng)式RPC功能層次,根據(jù)RPC各個角色的功能將架構(gòu)分為:數(shù)據(jù)傳輸層、服務(wù)治理層、服務(wù)調(diào)用層以及Ext層4部分。
圖3 響應(yīng)式RPC功能層次
數(shù)據(jù)傳輸層主要用于對服務(wù)提供者以及服務(wù)消費者提供數(shù)據(jù)通信,需要保證雙方在數(shù)據(jù)傳輸過程中高效、正確。為保證該需求,RPC系統(tǒng)在服務(wù)雙方需要定義統(tǒng)一的數(shù)據(jù)管理標(biāo)準(zhǔn)。其中主要功能包括:通訊協(xié)議、通訊框架、編碼以及對象序列化方式。
服務(wù)治理層主要用于對服務(wù)進行管控,在進行服務(wù)遠(yuǎn)程調(diào)用時,可以方便獲取到服務(wù)注冊信息。項目中采用注冊中心的形式實現(xiàn)服務(wù)治理,其主要功能包括:服務(wù)持久化注冊表、服務(wù)發(fā)現(xiàn)、服務(wù)注冊、服務(wù)下線、服務(wù)監(jiān)控。
服務(wù)調(diào)用層是對服務(wù)消費者所發(fā)起的請求進行調(diào)用,與本地服務(wù)調(diào)用不同,遠(yuǎn)程調(diào)用需要根據(jù)服務(wù)的注冊信息進行具體地址的選擇。同時RPC框架需要對接收到的服務(wù)信息進行解析。因此該層主要提供的功能為:代理調(diào)用以及服務(wù)負(fù)載均衡管理。
Ext層是對當(dāng)前服務(wù)的擴展層,從圖中可以看到Ext層是貫穿于其它層之間,對上述3層中的組件進行擴展,根據(jù)企業(yè)需求實現(xiàn)RPC系統(tǒng)定制化。
數(shù)據(jù)傳輸層是RPC系統(tǒng)的核心基礎(chǔ)功能,一個RPC傳輸系統(tǒng)的性能好壞取決于當(dāng)前傳輸?shù)姆绞揭约胺€(wěn)定性。該層的設(shè)計主要圍繞如何提高性能以及減少無效字段傳輸?shù)姆矫婵紤]。此時需要對傳輸協(xié)議、通信框架、數(shù)據(jù)編碼以及序列化方式進行優(yōu)化選取。
3.1.1 數(shù)據(jù)傳輸協(xié)議
對于一個完善的RPC框架,數(shù)據(jù)的傳輸不僅要考慮粘包拆包的問題,而且還需要考慮到一些擴展性的需求,因此需要自定義一套私有化協(xié)議,在協(xié)議中需要保證基本業(yè)務(wù)的實現(xiàn),同時要考慮到對數(shù)據(jù)的擴展性支持,保證開發(fā)人員可以基于當(dāng)前的協(xié)議進行業(yè)務(wù)上的擴展。
圖4為協(xié)議整體設(shè)計,其中字段解釋如下:
圖4 私有化協(xié)議
(1)Protocal:該字段是標(biāo)識當(dāng)前傳輸?shù)膮f(xié)議,可以通過該字段方便地轉(zhuǎn)換所傳輸?shù)膮f(xié)議。
(2)Type:該字段是標(biāo)識當(dāng)前協(xié)議可以傳輸?shù)念愋?,分為request、response、oneway這3種。其中request為請求是發(fā)送的報文類型,response為接收到請求并處理結(jié)束進行返回時的報文類型,而oneway為客戶端單方面請求無需服務(wù)端響應(yīng)的報文。
(3)Command:該字段標(biāo)識當(dāng)前請求所對應(yīng)的命令,分為request、heart、response。其中heart為心跳檢測請求,定時檢測服務(wù)是否存活。
(4)Id:該id字段是對當(dāng)前請求的唯一標(biāo)識,每個請求都會初始化一個requestId保證請求的唯一性。
(5)ClassNameLen:該字段標(biāo)識當(dāng)前請求class名稱的長度,可提前解析出要調(diào)用的class。
(6)Timeout:該字段標(biāo)識當(dāng)前請求的超時時間。
(7)BodyLen:該字段標(biāo)識在序列化后傳入的信息長度。
(8)Body:該字段標(biāo)識在序列化后的二進制信息。其中主要包括請求的方法名、方法參數(shù)類型、方法具體參數(shù)。
上述請求協(xié)議字段中,ClassNameLen字標(biāo)識了當(dāng)前存入的Class的長度,在高性能的網(wǎng)絡(luò)框架中系統(tǒng)都是采用Reactor多線程模式,即一個IO線程去掛載多個連接,如果將數(shù)據(jù)處理都放在當(dāng)前這個線程將會影響到其它掛載的數(shù)據(jù),因此在當(dāng)前的服務(wù)框架中BodyLen以及Body字段的解析可以不放在當(dāng)前的IO線程中,而是在后續(xù)開啟的業(yè)務(wù)線程中進行,保證IO線程的流暢。
3.1.2 Reactor-netty通訊框架設(shè)計
Reactor-netty是在原生Netty框架上的響應(yīng)式封裝,其底層仍然沿用Netty的handler機制。handler是針對不同事件的處理模式,根據(jù)傳入的事件進行處理并通過Pipeline實現(xiàn)handler之間的連接,當(dāng)前handler在處理完事件后傳遞給下一個handler。handler分為input以及output兩種,其中input為接收時所需要處理的事件,output為向外傳輸時所需要處理的事件。Reactor-netty在隱藏了大部分netty內(nèi)部細(xì)節(jié)的同時增加響應(yīng)式背壓,只向外暴露簡潔的API方法。項目中主要在TCP協(xié)議基礎(chǔ)上進行私有化設(shè)計,Reactor-netty對外提供了易于使用的TcpServer模塊以及TcpClient模塊。
項目中TcpServer模塊的核心代碼如下所示,首先需要進行服務(wù)開啟以及端口配置,其內(nèi)置了create以及port方法,在創(chuàng)建后需要綁定netty底層的bootStrap進行事件的輪訓(xùn)監(jiān)聽。在設(shè)置了啟動端口后需要對netty的channel管道進行配置,通過ChannelOption類進行參數(shù)選取,之后需要對生命周期中的連接初始化進行回調(diào)綁定,其中doOnConnection方法是在連接了遠(yuǎn)程服務(wù)器的時候會被回調(diào),其內(nèi)部需要對讀寫超時、序列化、粘包解析、心跳、連接處理進行配置,該方法是框架提供的生命周期事件管理中的一種,其它的還包括doOnBind、doOnBound、doOnUnbound、doOnLifecycle。上述方法都是在產(chǎn)生了對應(yīng)事件時自動調(diào)用。最后需要為Reactor-netty注入事件處理器,通過handle方法對NettyInbound和NettyOutbound進行初始化,實現(xiàn)業(yè)務(wù)模塊。
TcpServer
//創(chuàng)建服務(wù)
.create()
//綁定事務(wù)組
.doOnBind(bootstrap-> bootstrap.option(Chan-nelOption.SO_BACKLOG, serverProperties.getBacklog()))
//綁定端口
.port(serverProperties.getPort())
.runOn(loopResources)
//綁定channel配置值
.option(ChannelOption.SO_KEEPALIVE, ser-verProperties.isKeepAlive())
.option(ChannelOption.SO_REUSEADDR, true)
//其余的option值設(shè)定,省略
……
//初始化設(shè)置
.doOnConnection(this::initConnection)
//一系列handler綁定
.handle(this::handler);
//異步回調(diào)組合
Mono.just(pkg)
.map(RpcDataPackage::getData)
.handle(data, synchronousSink)-> {}
.publish(rpcHandlerFunction::handle)
.onErrorMap()
.
.onErrorResume()
.publish(nettyOutbound::send)
.doOnError(e-> log.error("conn catch error.", e));
項目中的客戶端采用TcpClient封裝,其內(nèi)部配置流程與TcpServer類似,差別在于handle處理事件不同,其主要是作為請求封裝以及相應(yīng)的接收,因此在handler中需要對動態(tài)代理進行封裝,將數(shù)據(jù)封裝為傳輸所需要的代碼,并通過doOnSubscribe方法實現(xiàn)事件的注冊監(jiān)聽,對請求響應(yīng)的數(shù)據(jù)包裝,最后將包裝后的消息響應(yīng)給客戶端。
數(shù)據(jù)整體傳輸過程中,由于有了Reactor-netty的封裝可以在請求的參數(shù)以及返回值中使用Mono以及Flux類型的對象,實現(xiàn)響應(yīng)式支持,如上代碼所示,在異步回調(diào)組合中,通過流式編程對方法進行組合式編寫,Mono的組合形式是對Future異步回調(diào)機制的優(yōu)化,實現(xiàn)基于事件的異步通知。
3.1.3 編碼以及序列化功能
由3.1.1節(jié)的數(shù)據(jù)傳輸層協(xié)議設(shè)計可知,在序列化之前,需要對前置自定義協(xié)議字段進行編碼操作。首先需要創(chuàng)建一個ByteBuf字節(jié)緩沖區(qū),用于存儲整體編碼后的數(shù)據(jù)。從當(dāng)前傳入的對象中解析出Protocal協(xié)議字段插入到緩沖區(qū)中并獲取對應(yīng)的協(xié)議對象,然后將協(xié)議中的Type、Command、Id、ClassName、Timeout按照順序進行解析存儲在ByteBuf中。此時對數(shù)據(jù)的編碼階段已經(jīng)結(jié)束,后續(xù)請求中的詳細(xì)數(shù)據(jù)需要對其進行序列化處理。在Reactor-netty中通過實現(xiàn)MessageToMessageDecoder接口中的decode方法,在方法內(nèi)部對數(shù)據(jù)進行預(yù)處理以及調(diào)用反序列化方法。通過實現(xiàn)MessageToByteEncoder接口中的encode方法對傳入的對象進行獲取以及序列化方法調(diào)用,將二進制數(shù)據(jù)存入Reactor-Netty內(nèi)置的Buffer緩沖區(qū)中。
在實現(xiàn)數(shù)據(jù)編碼的過程中離不開序列化的支持,序列化是將java對象向著二進制對象轉(zhuǎn)化,反序列化是序列化的逆過程。在當(dāng)前RPC框架中,數(shù)據(jù)均以二進制進行傳輸,因此詳細(xì)請求信息需要進行序列化操作。
系統(tǒng)中采用接口+實現(xiàn)類形式進行序列化機制的擴展。
@SPI
public interface SerializerInterface {
//序列化
//反序列化
}
在序列化的選取中,考慮當(dāng)前主流的4種序列化方式j(luò)ava、hession、Kryo、ProtoBuf。原生java序列化性能方面存在不足,Hession框架在速度上相對java原生有所提高但是數(shù)據(jù)序列化后長度不理想、ProtoBuf雖然性能方面很出眾但是需要編寫特定的IDL文件,在代碼接入方面不理想。
本項目最終選取Kryo作為當(dāng)前框架的序列化方式,Kryo性能好,兼容性強,可提前對數(shù)據(jù)長度進行設(shè)置,提高資源利用率。
(1)Kryo序列化
在類KryoSerializer中,通過對SerializerInterface接口的serialize方法重寫,實現(xiàn)數(shù)據(jù)的序列化功能。核心代碼如下:
ByteArrayOutputStream oss=null;
Output out1=null;
private final ThreadLocal
@Override
protected Kryo initialValue(){
Kryo kryo = new Kryo();
//循環(huán)引用
kryo.setReferences(true);
kryo.setRegistrationRequired(false);
return kryo;
}
};
//序列化操作
public
//生成對應(yīng)Stream流
oss = new ByteArrayOutputStream();
//生成Out輸出流
out1 = new Output(oss);
try {
kryoLocal.get().writeObject(output,seri);
out1.flush();
byte[]result = oss.toByteArray();
return result;
} catch(Exception e){
//處理報錯信息
……
}
}
Kryo是線程不安全的,在序列化過程中首先需要對Kryo進行線程私有化管理,通過ThreadLocal實現(xiàn)線程隔離,保證每個線程拿到自己的Kryo實例。初始化后創(chuàng)建ByteArrayOutputStream字節(jié)流緩沖區(qū)以及OutPut輸出流,Kryo通過writeObject方法將數(shù)據(jù)寫入到OutPut輸出流中的字節(jié)流緩沖區(qū)中,最后從字符流緩沖區(qū)獲取當(dāng)前的二進制字節(jié)序列結(jié)束整個序列化流程,并關(guān)閉釋放所有資源。
(2)Kryo反序列化
在類KryoSerializer中的deserialize方法實現(xiàn)了數(shù)據(jù)的反序列化功能,通過該方法實現(xiàn)了二進制到類對象的轉(zhuǎn)化。核心代碼如下:
ByteArrayInputStream in1=null;
Input in1=null;
//反序列化實現(xiàn)
public
{
//初始化輸入流緩沖區(qū)
is1= new ByteArrayInputStream(bytes);
//初始化輸入流
in1 = new Input(is);
try {
Object result = kryoLocal.get().readObject(input, clazz);
return result;
} catch(Exception e){
//處理報錯信息
……
}
}
反序列化方法中傳入了二進制字節(jié)序列,首先創(chuàng)建ByteArrayInputStream字節(jié)輸入流緩沖區(qū)以及Input輸入流對象,并對傳入的二進制字節(jié)序列進行裝載。通過Kryo的readObject方法實現(xiàn)二進制字節(jié)流到對象的轉(zhuǎn)換,然后釋放所有資源。
以上是基于Kryo實現(xiàn)的編解碼以及序列化過程,該框架可以有效解決原生java序列化所帶來的性能問題,提高了RPC服務(wù)的相應(yīng)速度以及效率。
服務(wù)治理在RPC中主要以注冊中心的形式存在,用于對服務(wù)的發(fā)布、查詢、監(jiān)控以及下線處理。通過注冊中心將服務(wù)提供方以及服務(wù)消費方兩者連接在一起,服務(wù)提供方注冊服務(wù)信息到注冊中心,服務(wù)消費方從注冊中心獲取信息,實現(xiàn)兩者通信。同時注冊中心還兼具了服務(wù)的心跳管理功能,保證了服務(wù)與注冊中心之間保持連接。
本系統(tǒng)中采用Zookeeper作為注冊中心,其內(nèi)部采用樹形結(jié)構(gòu)的目錄,與傳統(tǒng)的文件系統(tǒng)不同,Zookeeper的節(jié)點可以設(shè)置為臨時以及持久化兩種,同時在其內(nèi)部擁有Watcher機制,Zookeeper允許開發(fā)者對某一些節(jié)點添加Watcher監(jiān)控,根據(jù)節(jié)點的變化來進行操作觸發(fā),并且在Watcher機制對時間僅做一次響應(yīng),做到了輕量級的數(shù)據(jù)通知[11]。系統(tǒng)在整合中主要分為服務(wù)注冊、服務(wù)發(fā)現(xiàn)、服務(wù)移除。
(1)服務(wù)注冊
在服務(wù)注冊階段,Zookeeper獲取到當(dāng)前注冊的信息,檢測當(dāng)前父節(jié)點是否存在,不存在則創(chuàng)建父節(jié)點。在父節(jié)點下將當(dāng)前注冊的服務(wù)作為子節(jié)點。將當(dāng)前的注冊信息緩存在本地內(nèi)存中并對節(jié)點注冊Watcher監(jiān)聽,如果服務(wù)服務(wù)下線,直接將當(dāng)前內(nèi)存中的數(shù)據(jù)清除,保證本地內(nèi)存和Zookeeper的一致性。
(2)服務(wù)發(fā)現(xiàn)
在服務(wù)注冊階段已經(jīng)將注冊好的服務(wù)緩存在本地內(nèi)存中,調(diào)用服務(wù)發(fā)現(xiàn)請求時可以直接從本地拉取所需要的服務(wù),并響應(yīng)給服務(wù)消費方。
(3)服務(wù)移除
根據(jù)傳入的信息進行地址拼接,系統(tǒng)對拼接后的地址進行檢測,查詢傳入的URL地址信息是否在Zookeeper中,若信息存在則進行節(jié)點刪除操作。服務(wù)已經(jīng)注冊了Watcher監(jiān)聽事件,對服務(wù)進行刪除后會觸發(fā)本地內(nèi)存移除操作,保證服務(wù)信息在本地與遠(yuǎn)程的一致性。
服務(wù)調(diào)用層主要是建立在數(shù)據(jù)傳輸層之上,在數(shù)據(jù)傳輸之前以及之后需要對數(shù)據(jù)的調(diào)用形式進行處理。其中分為Provider提供方以及Consumer消費方,Consumer對請求數(shù)據(jù)進行封裝,借助服務(wù)治理層獲取地址進行路由選擇,然后通過數(shù)據(jù)傳輸層將當(dāng)前請求傳送給Provider。Provider通過數(shù)據(jù)傳輸層對傳輸?shù)恼埱筮M行解析,采用動態(tài)代理形式對請求方法進行調(diào)用,將調(diào)用結(jié)果封裝后傳輸給Consumer完成整體調(diào)用。整體流程如圖5所示。
圖5 調(diào)用流程
圖5流程中可以看到,在調(diào)用階段需要對服務(wù)進行代理調(diào)用以及服務(wù)地址的負(fù)載均衡選擇,接下來將對這兩部分進行分析。
3.3.1 服務(wù)對象代理
系統(tǒng)在對請求進行反序列化后可以獲取到當(dāng)前的類信息,服務(wù)提供方無法直接對服務(wù)接口進行調(diào)用,需要采取服務(wù)代理的模式,根據(jù)獲取到的方法名稱,參數(shù)類型、參數(shù)實體調(diào)用真實方法。系統(tǒng)中基于java動態(tài)代理技術(shù),該技術(shù)是java內(nèi)置的方法,調(diào)用方法的內(nèi)部需要傳入最終方法的名稱以及參數(shù)類型,以此來保證唯一方法的獲取。在獲取到當(dāng)前的Method方法類后,調(diào)用其invoke方法并傳入真實參數(shù)得到返回結(jié)果。具體調(diào)用核心代碼如下:
try {
//參數(shù)賦值service的類信息、className、方法類型、方法參數(shù)
Class> serviceClass
String methodName
Class>[]parameterTypes
Object[]parameters
//jdk動態(tài)代理
Method method = serviceClass.getMethod(methodName, parameterTypes);
method.setAccessible(true);
//得到返回結(jié)果
Object result = method.invoke(serviceBean, parameters);
RpcResponse.setResult(result);
} catch(Throwable t){
//錯誤處理
RpcResponse.setErrorMsg(t);
}
//返回結(jié)果
return RpcResponse;
3.3.2 服務(wù)負(fù)載均衡
負(fù)載均衡的目的是將當(dāng)前的服務(wù)請求均衡地分布給所屬的網(wǎng)絡(luò)系統(tǒng)[12]。單節(jié)點項目中由于大批量的請求全部都在一臺機器上處理會導(dǎo)致性能上的瓶頸,因此系統(tǒng)必然需要集群化配置。而在集群環(huán)境下如何選擇每次請求的地址是需要根據(jù)負(fù)載均衡的配置策略決定的。當(dāng)前主流的選取策略有輪詢、隨機權(quán)重選擇、最少連接數(shù)選擇等[13]。上述負(fù)載均衡策略均為靜態(tài)化選擇,是通過管理員手動配置參數(shù)來實現(xiàn)服務(wù)器的負(fù)載均衡,靜態(tài)化的優(yōu)勢在于可以方便的配置,并且算法實現(xiàn)相對容易,在小型系統(tǒng)中開銷比較小,可以滿足小系統(tǒng)的開發(fā)。然而靜態(tài)化的節(jié)點選取是無法考慮到后續(xù)在實際運行過程中系統(tǒng)真實的負(fù)載情況[14]。為解決此問題,項目中提出了動態(tài)負(fù)載均衡的思想,該思想的核心是根據(jù)服務(wù)器在處理數(shù)據(jù)時的請求成功以及超時情況進行地址選取。如果當(dāng)前服務(wù)處理時間在閾值范圍內(nèi),可以認(rèn)為當(dāng)前服務(wù)器正常運行。如果出現(xiàn)異?;蛘叱瑫r,說明當(dāng)前服務(wù)器可能負(fù)載壓力過大,需要做出調(diào)整。該方法基于最大權(quán)值策略,每次選取當(dāng)前權(quán)值最大的服務(wù)器進行路由。
該方法的執(zhí)行流程如圖6所示。
圖6 負(fù)載均衡流程
首先對每個服務(wù)的權(quán)值設(shè)置初始值為100,開啟一個循環(huán)的線程,等待客戶端請求。當(dāng)接收到RPC的請求后根據(jù)權(quán)值計算出被選擇的概率,計算規(guī)則為當(dāng)前服務(wù)每次請求成功一次,其對應(yīng)的權(quán)值加一,而如果當(dāng)前服務(wù)出現(xiàn)了異常或者超時,權(quán)值減少5。一般出現(xiàn)異?;蛘叱瑫r的情況相對較少,除非是服務(wù)器直接宕機無法接收請求。權(quán)值需要設(shè)置上下界限,以100作為權(quán)值上界,0作為權(quán)值下界,如果當(dāng)前權(quán)值降為0,等待1 min,給服務(wù)器緩沖的時間,如果1 min后仍然出現(xiàn)調(diào)用超時,問題直接進行服務(wù)器的下線操作,如果成功則將權(quán)值回復(fù)到60。修改的權(quán)值信息都會自動更新到注冊中心,可以從注冊中心獲取到當(dāng)前權(quán)值信息。
Java SPI是一種允許開發(fā)人員在接入端實現(xiàn)外部擴展的模式,該模式對定義好的接口進行擴展,開發(fā)者可以采用代碼無侵入的形式對當(dāng)前框架增添功能,實現(xiàn)了整體服務(wù)的插拔式配置。
使用Java SPI模式需要遵循如下流程:
(1)服務(wù)提供者需要定義所需要擴展的接口,并且在當(dāng)前項目的resources資源目錄中新建META-INF/services文件夾,該文件夾主要作為記錄擴展點文件的地址,在其內(nèi)部創(chuàng)建一個以接口完整路徑命名的文件,內(nèi)容是當(dāng)前接口的實現(xiàn)類路徑。
(2)主程序內(nèi)部通過ServiceLoader類進行動態(tài)裝載,其內(nèi)部掃描上一步文件中配置的所有類名,一次性全部加載到JVM中。
(3)將加載到的類文件信息全部進行初始化操作并生成對應(yīng)的類。
(4)根據(jù)傳入的參數(shù)進行輪詢比較,找到匹配的類進行獲取。
按照上述配置,Java開發(fā)者可以對服務(wù)進行擴展。然而Java SPI仍然存在如下的缺點:
(1)Java內(nèi)置的SPI機制對所有的擴展類進行了實例化操作,在開發(fā)中可能只是需要對部分類進行實例化操作,且無法實現(xiàn)單例模式。
(2)無法對擴展點進行排序以及分組。
(3)擴展點文件只被限制在META-INF/services目錄中無法額外擴展。
基于原生Java SPI的問題,對其進行了優(yōu)化處理,使得當(dāng)前系統(tǒng)中的SPI機制支持對自定義擴展來設(shè)置單例/多例、支持實現(xiàn)類排序功能、支持只創(chuàng)建所需要的類,解決原生全量加載問題、支持根據(jù)特征屬性值區(qū)分不同類別、支持基于注解支持自動掃描實現(xiàn)類。
如圖7所示為優(yōu)化后的SPI類關(guān)系,在啟動加載階段,RPC內(nèi)部會根據(jù)配置通過ExtensionLoader擴展類加載器進行數(shù)據(jù)加載,讀取服務(wù)配置的目錄,根據(jù)目錄信息查詢擴展接口對應(yīng)配置文件,在該文件內(nèi)部循環(huán)加載這個文件下的描述文件,并且按照行進行讀取,其中相同接口只會被加載進內(nèi)存一次,通過對注解的驗證以及解析生成對應(yīng)的Class文件,最后在使用擴展類時才會對其進行加載實例化操作。
圖7 優(yōu)化后SPI類關(guān)系
圖7中可以看到優(yōu)化后的SPI加入了兩個注解,分為@SPI以及@Extension,其中@SPI標(biāo)志著當(dāng)前擴展的接口,而@Extension標(biāo)志當(dāng)前擴展的實現(xiàn)類。系統(tǒng)通過對注解解析實現(xiàn)類加載功能,實現(xiàn)注解化配置。
public @interface SPI{
//擴展類是否使用單例,默認(rèn)使用
boolean singleton()default true;
}
public @interface Extension {
//擴展點名字
String value();
//優(yōu)先級排序,大的優(yōu)先級高
int order()default 0;
//擴展類是分類名稱
boolean category()default false;
}
在上述注解類代碼中可以看到,其內(nèi)部可配置多個屬性,通過SPI機制的優(yōu)化功能就是通過對屬性值的解析實現(xiàn)的?;趦?yōu)化后的SPI擴展機制,開發(fā)人員可實現(xiàn)自定義擴展功能,并且無需對RPC底層代碼進行修改,實現(xiàn)第三方自由化擴展。
系統(tǒng)功能測試主要是檢測RPC的基本調(diào)用功能以及響應(yīng)式編程方法是否可以正常調(diào)用。
在本地電腦搭建RPC環(huán)境,應(yīng)用虛擬機單獨開啟Zookeeper服務(wù),項目啟動后查看Zookeeper注冊中心可以看到當(dāng)前服務(wù)已經(jīng)注冊成功,圖8為當(dāng)前服務(wù)的樹形結(jié)構(gòu)展示,這里在服務(wù)端開啟集群形式,因此在Zookeeper的服務(wù)信息中,記錄了兩個地址端口號不同的地址。
圖8 Zookeeper樹形結(jié)構(gòu)
為驗證系統(tǒng)中響應(yīng)式編程的支持,客戶端編寫測試代碼發(fā)起遠(yuǎn)程服務(wù)調(diào)用,如下代碼所示有兩個方法,分為響應(yīng)式方法以及普通方法的調(diào)用,響應(yīng)式模式的編程方法中需要接收Mono對象作為返回值,而Mono對象在傳統(tǒng)RPC框架中并不支持,項目中通過編寫入?yún)⒁约胺祷刂刀紴镸ONO對象的方法,進行測定,經(jīng)測定系統(tǒng)可以正確的調(diào)用兩個方法并獲取返回值。該系統(tǒng)在功能方面可正常運行,符合預(yù)期要求。
//引入響應(yīng)式編程特有類MONO
import reactor.core.publisher.Mono;
@Service("testService")
public interface TestService {
//響應(yīng)式方法
Mono
//普通方法
Request testMethod2(Request request);
}
這一部分主要針對基于Reactor-netty的RPC系統(tǒng)和基于Netty的RPC系統(tǒng)的響應(yīng)性能進行測試。通過maven插件對接入了RPC項目的客戶端代碼以及服務(wù)端代碼進行打包,將打包好的JAR文件分別上傳到服務(wù)器中。測試中選取5臺服務(wù)器,其中兩臺服務(wù)器分別部署基于Netty的RPC服務(wù)提供方和服務(wù)消費方,再選取兩臺作為基于Rea-ctor-netty的RPC系統(tǒng)的服務(wù)方以及消費方,最后一臺單獨部署Zookeeper注冊中心。5臺機器配置見表1。
表1 服務(wù)器配置
實驗中采用并發(fā)測試工具Jmeter對兩個注冊中心系統(tǒng)進行性能壓力測試。Jmeter模擬高并發(fā)下服務(wù)調(diào)用,其內(nèi)部通過多線程機制設(shè)置并發(fā)訪問數(shù)量,系統(tǒng)中以1k大小的對象進行數(shù)據(jù)傳遞,并在控制臺打印傳輸?shù)淖址?,根?jù)并發(fā)數(shù)的不同對比兩個rpc數(shù)據(jù)響應(yīng)速度。
表2為客戶端并發(fā)模擬服務(wù)調(diào)用請求的響應(yīng)時間。由表中可知,在并發(fā)數(shù)量較小的情況下兩者的服務(wù)響應(yīng)時間相近。隨著并發(fā)訪問量的提升,基于Netty的RPC響應(yīng)時間遠(yuǎn)大于基于Reactor-netty的RPC系統(tǒng),可以看出在服務(wù)調(diào)用方面響應(yīng)式Reactor-netty的RPC系統(tǒng)有明顯的性能優(yōu)勢。
表2 并發(fā)性能測試結(jié)果
本文設(shè)計了一款基于響應(yīng)式的RPC系統(tǒng),解決了當(dāng)前java語言編寫的RPC系統(tǒng)無法支持響應(yīng)式流編程的問題。該系統(tǒng)采用Reactor-netty框架在Nett的基礎(chǔ)上實現(xiàn)性能優(yōu)化;通過響應(yīng)式編程提升代碼可讀性,通過Mono類實現(xiàn)基于事件的異步回調(diào)組合形式;系統(tǒng)集成Kryo序列化方法,提升整體數(shù)據(jù)傳輸性能;系統(tǒng)應(yīng)用Zookeeper實現(xiàn)注冊中心的功能,為服務(wù)的治理提供保障;系統(tǒng)在傳輸?shù)刂愤x擇方面采用動態(tài)負(fù)載均衡,提升系統(tǒng)在集群環(huán)境下的處理能力;通過優(yōu)化后的SPI機制實現(xiàn)服務(wù)擴展;最后,通過和Netty作為通訊框架的RPC系統(tǒng)對比可知,該系統(tǒng)在請求速度方面的性能更加優(yōu)越。