概念篇
RPC 是什么?
RPC 稱遠(yuǎn)程過程調(diào)用(Remote Procedure Call),用于解決分布式系統(tǒng)中服務(wù)之間的調(diào)用問題。通俗地講,就是開發(fā)者能夠像調(diào)用本地方法一樣調(diào)用遠(yuǎn)程的服務(wù)。所以,RPC的作用主要體現(xiàn)在這兩個(gè)方面:
- 屏蔽遠(yuǎn)程調(diào)用跟本地調(diào)用的區(qū)別,讓我們感覺就是調(diào)用項(xiàng)目?jī)?nèi)的方法;
- 隱藏底層網(wǎng)絡(luò)通信的復(fù)雜性,讓我們更專注于業(yè)務(wù)邏輯。
RPC 框架基本架構(gòu)
下面我們通過一幅圖來(lái)說(shuō)說(shuō) RPC 框架的基本架構(gòu)
RPC 框架包含三個(gè)最重要的組件,分別是客戶端、服務(wù)端和注冊(cè)中心。在一次 RPC 調(diào)用流程中,這三個(gè)組件是這樣交互的:
- 服務(wù)端在啟動(dòng)后,會(huì)將它提供的服務(wù)列表發(fā)布到注冊(cè)中心,客戶端向注冊(cè)中心訂閱服務(wù)地址;
- 客戶端會(huì)通過本地代理模塊 Proxy 調(diào)用服務(wù)端,Proxy 模塊收到負(fù)責(zé)將方法、參數(shù)等數(shù)據(jù)轉(zhuǎn)化成網(wǎng)絡(luò)字節(jié)流;
- 客戶端從服務(wù)列表中選取其中一個(gè)的服務(wù)地址,并將數(shù)據(jù)通過網(wǎng)絡(luò)發(fā)送給服務(wù)端;
- 服務(wù)端接收到數(shù)據(jù)后進(jìn)行解碼,得到請(qǐng)求信息;
- 服務(wù)端根據(jù)解碼后的請(qǐng)求信息調(diào)用對(duì)應(yīng)的服務(wù),然后將調(diào)用結(jié)果返回給客戶端。
RPC 框架通信流程以及涉及到的角色
從上面這張圖中,可以看見 RPC 框架一般有這些組件:服務(wù)治理(注冊(cè)發(fā)現(xiàn))、負(fù)載均衡、容錯(cuò)、序列化/反序列化、編解碼、網(wǎng)絡(luò)傳輸、線程池、動(dòng)態(tài)代理等角色,當(dāng)然有的RPC框架還會(huì)有連接池、日志、安全等角色。
具體調(diào)用過程
- 服務(wù)消費(fèi)方(client)以本地調(diào)用方式調(diào)用服務(wù)
- client stub 接收到調(diào)用后負(fù)責(zé)將方法、參數(shù)等封裝成能夠進(jìn)行網(wǎng)絡(luò)傳輸?shù)南Ⅲw
- client stub 將消息進(jìn)行編碼并發(fā)送到服務(wù)端
- server stub 收到消息后進(jìn)行解碼
- server stub 根據(jù)解碼結(jié)果調(diào)用本地的服務(wù)
- 本地服務(wù)執(zhí)行并將結(jié)果返回給 server stub
- server stub 將返回導(dǎo)入結(jié)果進(jìn)行編碼并發(fā)送至消費(fèi)方
- client stub 接收到消息并進(jìn)行解碼
- 服務(wù)消費(fèi)方(client)得到結(jié)果
RPC 消息協(xié)議
RPC調(diào)用過程中需要將參數(shù)編組為消息進(jìn)行發(fā)送,接收方需要解組消息為參數(shù),過程處理結(jié)果同樣需要經(jīng)編組、解組。消息由哪些部分構(gòu)成及消息的表示形式就構(gòu)成了消息協(xié)議。
RPC調(diào)用過程中采用的消息協(xié)議稱為RPC消息協(xié)議。
實(shí)戰(zhàn)篇
從上面的概念我們知道一個(gè)RPC框架大概有哪些部分組成,所以在設(shè)計(jì)一個(gè)RPC框架也需要從這些組成部分考慮。從RPC的定義中可以知道,RPC框架需要屏蔽底層細(xì)節(jié),讓用戶感覺調(diào)用遠(yuǎn)程服務(wù)像調(diào)用本地方法一樣簡(jiǎn)單,所以需要考慮這些問題:
- 用戶使用我們的RPC框架時(shí)如何盡量少的配置
- 如何將服務(wù)注冊(cè)到ZK(這里注冊(cè)中心選擇ZK)上并且讓用戶無(wú)感知
- 如何調(diào)用透明(盡量用戶無(wú)感知)的調(diào)用服務(wù)提供者
- 啟用多個(gè)服務(wù)提供者如何做到動(dòng)態(tài)負(fù)載均衡
- 框架如何做到能讓用戶自定義擴(kuò)展組件(比如擴(kuò)展自定義負(fù)載均衡策略)
- 如何定義消息協(xié)議,以及編解碼
- ...等等
上面這些問題在設(shè)計(jì)這個(gè)RPC框架中都會(huì)給予解決。
技術(shù)選型
- 注冊(cè)中心 目前成熟的注冊(cè)中心有Zookeeper,Nacos,Consul,Eureka,這里使用ZK作為注冊(cè)中心,沒有提供切換以及用戶自定義注冊(cè)中心的功能。
- IO通信框架 本實(shí)現(xiàn)采用 Netty 作為底層通信框架,因?yàn)镹etty 是一個(gè)高性能事件驅(qū)動(dòng)型的非阻塞的IO(NIO)框架,沒有提供別的實(shí)現(xiàn),也不支持用戶自定義通信框架
- 消息協(xié)議 本實(shí)現(xiàn)使用自定義消息協(xié)議,后面會(huì)具體說(shuō)明
項(xiàng)目總體結(jié)構(gòu)
從這個(gè)結(jié)構(gòu)中可以知道,以rpc命名開頭的是rpc框架的模塊,也是本項(xiàng)目RPC框架的內(nèi)容,而consumer是服務(wù)消費(fèi)者,provider是服務(wù)提供者,provider-api是暴露的服務(wù)API。
整體依賴情況
項(xiàng)目實(shí)現(xiàn)介紹
要做到用戶使用我們的RPC框架時(shí)盡量少的配置,所以把rpc框架設(shè)計(jì)成一個(gè)starter,用戶只要依賴這個(gè)starter,基本那就可以了。
為什么要設(shè)計(jì)成兩個(gè) starter (client-starter/server-starter) ?
這個(gè)是為了更好的體現(xiàn)出客戶端和服務(wù)端的概念,消費(fèi)者依賴客戶端,服務(wù)提供者依賴服務(wù)端,還有就是最小化依賴。
為什么要設(shè)計(jì)成 starter ?
基于spring boot自動(dòng)裝配機(jī)制,會(huì)加載starter中的 spring.factories 文件,在文件中配置以下代碼,這里我們starter的配置類就生效了,在配置類里面配置一些需要的bean。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.rrtv.rpc.client.config.RpcClientAutoConfiguration
發(fā)布服務(wù)和消費(fèi)服務(wù)
- 對(duì)于發(fā)布服務(wù)
服務(wù)提供者需要在暴露的服務(wù)上增加注解 @RpcService,這個(gè)自定義注解是基于 @service 的,是一個(gè)復(fù)合注解,具備@service注解的功能,在@RpcService注解中指明服務(wù)接口和服務(wù)版本,發(fā)布服務(wù)到ZK上,會(huì)根據(jù)這個(gè)兩個(gè)元數(shù)據(jù)注冊(cè)
- 發(fā)布服務(wù)原理:
服務(wù)提供者啟動(dòng)之后,根據(jù)spring boot自動(dòng)裝配機(jī)制,server-starter的配置類就生效了,在一個(gè) bean 的后置處理器(RpcServerProvider)中獲取被注解 @RpcService 修飾的bean,將注解的元數(shù)據(jù)注冊(cè)到ZK上。
- 對(duì)于消費(fèi)服務(wù)
消費(fèi)服務(wù)需要使用自定義的 @RpcAutowired 注解標(biāo)識(shí),是一個(gè)復(fù)合注解,基于 @Autowired。
- 消費(fèi)服務(wù)原理
要讓客戶端無(wú)感知的調(diào)用服務(wù)提供者,就需要使用動(dòng)態(tài)代理,如上面所示, HelloWordService 沒有實(shí)現(xiàn)類,需要給它賦值代理類,在代理類中發(fā)起請(qǐng)求調(diào)用。
基于spring boot自動(dòng)裝配,服務(wù)消費(fèi)者啟動(dòng),bean 后置處理器 RpcClientProcessor 開始工作,它主要是遍歷所有的bean,判斷每個(gè)bean中的屬性是否有被 @RpcAutowired 注解修飾,有的話把該屬性動(dòng)態(tài)賦值代理類,這個(gè)再調(diào)用時(shí)會(huì)調(diào)用代理類的 invoke 方法。
代理類 invoke 方法通過服務(wù)發(fā)現(xiàn)獲取服務(wù)端元數(shù)據(jù),封裝請(qǐng)求,通過netty發(fā)起調(diào)用。
注冊(cè)中心
本項(xiàng)目注冊(cè)中心使用ZK,由于注冊(cè)中心被服務(wù)消費(fèi)者和服務(wù)提供者都使用。所以把ZK放在rpc-core模塊。
rpc-core 這個(gè)模塊如上圖所示,核心功能都在這個(gè)模塊。服務(wù)注冊(cè)在 register 包下。
服務(wù)注冊(cè)接口,具體實(shí)現(xiàn)使用ZK實(shí)現(xiàn)。
負(fù)載均衡策略
負(fù)載均衡定義在rpc-core中,目前支持輪詢(FullRoundBalance)和隨機(jī)(RandomBalance),默認(rèn)使用隨機(jī)策略。由rpc-client-spring-boot-starter指定。
通過ZK服務(wù)發(fā)現(xiàn)時(shí)會(huì)找到多個(gè)實(shí)例,然后通過負(fù)載均衡策略獲取其中一個(gè)實(shí)例
可以在消費(fèi)者中配置 rpc.client.balance=fullRoundBalance 替換,也可以自定義負(fù)載均衡策略,通過實(shí)現(xiàn)接口 LoadBalance,并將創(chuàng)建的類加入IOC容器即可。由于我們配置 @ConditionalOnMissingBean,所以會(huì)優(yōu)先加載用戶自定義的 bean。
自定義消息協(xié)議、編解碼
所謂協(xié)議,就是通信雙方事先商量好規(guī)則,服務(wù)端知道發(fā)送過來(lái)的數(shù)據(jù)將如何解析。
- 自定義消息協(xié)議
- 魔數(shù):魔數(shù)是通信雙方協(xié)商的一個(gè)暗號(hào),通常采用固定的幾個(gè)字節(jié)表示。魔數(shù)的作用是防止任何人隨便向服務(wù)器的端口上發(fā)送數(shù)據(jù)。例如 java Class 文件開頭就存儲(chǔ)了魔數(shù) 0xCAFEBABE,在加載 Class 文件時(shí)首先會(huì)驗(yàn)證魔數(shù)的正確性
- 協(xié)議版本號(hào):隨著業(yè)務(wù)需求的變化,協(xié)議可能需要對(duì)結(jié)構(gòu)或字段進(jìn)行改動(dòng),不同版本的協(xié)議對(duì)應(yīng)的解析方法也是不同的。
- 序列化算法:序列化算法字段表示數(shù)據(jù)發(fā)送方應(yīng)該采用何種方法將請(qǐng)求的對(duì)象轉(zhuǎn)化為二進(jìn)制,以及如何再將二進(jìn)制轉(zhuǎn)化為對(duì)象,如 JSON、Hessian、Java 自帶序列化等。
- 報(bào)文類型:在不同的業(yè)務(wù)場(chǎng)景中,報(bào)文可能存在不同的類型。RPC 框架中有請(qǐng)求、響應(yīng)、心跳等類型的報(bào)文。
- 狀態(tài):狀態(tài)字段用于標(biāo)識(shí)請(qǐng)求是否正常(SUCCESS、FAIL)。
- 消息ID:請(qǐng)求唯一ID,通過這個(gè)請(qǐng)求ID將響應(yīng)關(guān)聯(lián)起來(lái),也可以通過請(qǐng)求ID做鏈路追蹤。
- 數(shù)據(jù)長(zhǎng)度:標(biāo)明數(shù)據(jù)的長(zhǎng)度,用于判斷是否是一個(gè)完整的數(shù)據(jù)包
- 數(shù)據(jù)內(nèi)容:請(qǐng)求體內(nèi)容
編解碼
編解碼實(shí)現(xiàn)在 rpc-core 模塊,在包 com.rrtv.rpc.core.codec下。
自定義編碼器通過繼承 netty 的 MessageToByteEncoder
自定義解碼器通過繼承 netty 的 ByteToMessageDecoder類實(shí)現(xiàn)消息解碼。
解碼時(shí)需要注意TCP粘包、拆包問題
什么是TCP粘包、拆包
TCP 傳輸協(xié)議是面向流的,沒有數(shù)據(jù)包界限,也就是說(shuō)消息無(wú)邊界。客戶端向服務(wù)端發(fā)送數(shù)據(jù)時(shí),可能將一個(gè)完整的報(bào)文拆分成多個(gè)小報(bào)文進(jìn)行發(fā)送,也可能將多個(gè)報(bào)文合并成一個(gè)大的報(bào)文進(jìn)行發(fā)送。因此就有了拆包和粘包。
在網(wǎng)絡(luò)通信的過程中,每次可以發(fā)送的數(shù)據(jù)包大小是受多種因素限制的,如 MTU 傳輸單元大小、滑動(dòng)窗口等。
所以如果一次傳輸?shù)木W(wǎng)絡(luò)包數(shù)據(jù)大小超過傳輸單元大小,那么我們的數(shù)據(jù)可能會(huì)拆分為多個(gè)數(shù)據(jù)包發(fā)送出去。如果每次請(qǐng)求的網(wǎng)絡(luò)包數(shù)據(jù)都很小,比如一共請(qǐng)求了 10000 次,TCP 并不會(huì)分別發(fā)送 10000 次。TCP采用的 Nagle(批量發(fā)送,主要用于解決頻繁發(fā)送小數(shù)據(jù)包而帶來(lái)的網(wǎng)絡(luò)擁塞問題) 算法對(duì)此作出了優(yōu)化。
所以,網(wǎng)絡(luò)傳輸會(huì)出現(xiàn)這樣:
tcp_package.png
- 服務(wù)端恰巧讀到了兩個(gè)完整的數(shù)據(jù)包 A 和 B,沒有出現(xiàn)拆包/粘包問題;
- 服務(wù)端接收到 A 和 B 粘在一起的數(shù)據(jù)包,服務(wù)端需要解析出 A 和 B;
- 服務(wù)端收到完整的 A 和 B 的一部分?jǐn)?shù)據(jù)包 B-1,服務(wù)端需要解析出完整的 A,并等待讀取完整的 B 數(shù)據(jù)包;
- 服務(wù)端接收到 A 的一部分?jǐn)?shù)據(jù)包 A-1,此時(shí)需要等待接收到完整的 A 數(shù)據(jù)包;
- 數(shù)據(jù)包 A 較大,服務(wù)端需要多次才可以接收完數(shù)據(jù)包 A。
如何解決TCP粘包、拆包問題
解決問題的根本手段:找出消息的邊界:
- 消息長(zhǎng)度固定
每個(gè)數(shù)據(jù)報(bào)文都需要一個(gè)固定的長(zhǎng)度。當(dāng)接收方累計(jì)讀取到固定長(zhǎng)度的報(bào)文后,就認(rèn)為已經(jīng)獲得一個(gè)完整的消息。當(dāng)發(fā)送方的數(shù)據(jù)小于固定長(zhǎng)度時(shí),則需要空位補(bǔ)齊。
消息定長(zhǎng)法使用非常簡(jiǎn)單,但是缺點(diǎn)也非常明顯,無(wú)法很好設(shè)定固定長(zhǎng)度的值,如果長(zhǎng)度太大會(huì)造成字節(jié)浪費(fèi),長(zhǎng)度太小又會(huì)影響消息傳輸,所以在一般情況下消息定長(zhǎng)法不會(huì)被采用。
- 特定分隔符
在每次發(fā)送報(bào)文的尾部加上特定分隔符,接收方就可以根據(jù)特殊分隔符進(jìn)行消息拆分。分隔符的選擇一定要避免和消息體中字符相同,以免沖突。否則可能出現(xiàn)錯(cuò)誤的消息拆分。比較推薦的做法是將消息進(jìn)行編碼,例如 base64 編碼,然后可以選擇 64 個(gè)編碼字符之外的字符作為特定分隔符
- 消息長(zhǎng)度 + 消息內(nèi)容
消息長(zhǎng)度 + 消息內(nèi)容是項(xiàng)目開發(fā)中最常用的一種協(xié)議,接收方根據(jù)消息長(zhǎng)度來(lái)讀取消息內(nèi)容。
本項(xiàng)目就是利用 “消息長(zhǎng)度 + 消息內(nèi)容” 方式解決TCP粘包、拆包問題的。所以在解碼時(shí)要判斷數(shù)據(jù)是否夠長(zhǎng)度讀取,沒有不夠說(shuō)明數(shù)據(jù)沒有準(zhǔn)備好,繼續(xù)讀取數(shù)據(jù)并解碼,這里這種方式可以獲取一個(gè)個(gè)完整的數(shù)據(jù)包。
序列化和反序列化
序列化和反序列化在 rpc-core 模塊 com.rrtv.rpc.core.serialization 包下,提供了 HessianSerialization 和 JsonSerialization 序列化。
默認(rèn)使用 HessianSerialization 序列化。用戶不可以自定義。
序列化性能:
- 空間上
serialization_space.png
- 時(shí)間上
serialization_time.png
網(wǎng)絡(luò)傳輸,使用netty
netty 代碼固定的,值得注意的是 handler 的順序不能弄錯(cuò),以服務(wù)端為例,編碼是出站操作(可以放在入站后面),解碼和收到響應(yīng)都是入站操作,解碼要在前面。
image.png
客戶端 RPC 調(diào)用方式
成熟的 RPC 框架一般會(huì)提供四種調(diào)用方式,分別為同步 Sync、異步 Future、回調(diào) Callback和單向 Oneway。
- Sync 同步調(diào)用
客戶端線程發(fā)起 RPC 調(diào)用后,當(dāng)前線程會(huì)一直阻塞,直至服務(wù)端返回結(jié)果或者處理超時(shí)異常。
sync.png
- Future 異步調(diào)用
客戶端發(fā)起調(diào)用后不會(huì)再阻塞等待,而是拿到 RPC 框架返回的 Future 對(duì)象,調(diào)用結(jié)果會(huì)被服務(wù)端緩存,客戶端自行決定后續(xù)何時(shí)獲取返回結(jié)果。當(dāng)客戶端主動(dòng)獲取結(jié)果時(shí),該過程是阻塞等待的
future.png
- Callback 回調(diào)調(diào)用
客戶端發(fā)起調(diào)用時(shí),將 Callback 對(duì)象傳遞給 RPC 框架,無(wú)須同步等待返回結(jié)果,直接返回。當(dāng)獲取到服務(wù)端響應(yīng)結(jié)果或者超時(shí)異常后,再執(zhí)行用戶注冊(cè)的 Callback 回調(diào)
callback.png
- Oneway 單向調(diào)用
客戶端發(fā)起請(qǐng)求之后直接返回,忽略返回結(jié)果
oneway.png
這里使用的是第一種:客戶端同步調(diào)用,其他的沒有實(shí)現(xiàn)。邏輯在 RpcFuture 中,使用 CountDownLatch 實(shí)現(xiàn)阻塞等待(超時(shí)等待)
整體架構(gòu)和流程
流程分為三塊:服務(wù)提供者啟動(dòng)流程、服務(wù)消費(fèi)者啟動(dòng)、調(diào)用過程
服務(wù)提供者啟動(dòng)
- 服務(wù)提供者 provider 會(huì)依賴 rpc-server-spring-boot-starter
- ProviderApplication 啟動(dòng),根據(jù)springboot 自動(dòng)裝配機(jī)制,RpcServerAutoConfiguration 自動(dòng)配置生效
- RpcServerProvider 是一個(gè)bean后置處理器,會(huì)發(fā)布服務(wù),將服務(wù)元數(shù)據(jù)注冊(cè)到ZK上
- RpcServerProvider.run 方法會(huì)開啟一個(gè) netty 服務(wù)
服務(wù)消費(fèi)者啟動(dòng)
- 服務(wù)消費(fèi)者 consumer 會(huì)依賴 rpc-client-spring-boot-starter
- ConsumerApplication 啟動(dòng),根據(jù)springboot 自動(dòng)裝配機(jī)制,RpcClientAutoConfiguration 自動(dòng)配置生效
- 將服務(wù)發(fā)現(xiàn)、負(fù)載均衡、代理等bean加入IOC容器
- 后置處理器 RpcClientProcessor 會(huì)掃描 bean ,將被 @RpcAutowired 修飾的屬性動(dòng)態(tài)賦值為代理對(duì)象
調(diào)用過程
- 服務(wù)消費(fèi)者 發(fā)起請(qǐng)求http://localhost:9090/hello/world?name=hello
- 服務(wù)消費(fèi)者 調(diào)用 helloWordService.sayHello() 方法,會(huì)被代理到執(zhí)行 ClientStubInvocationHandler.invoke() 方法
- 服務(wù)消費(fèi)者 通過ZK服務(wù)發(fā)現(xiàn)獲取服務(wù)元數(shù)據(jù),找不到報(bào)錯(cuò)404
- 服務(wù)消費(fèi)者 自定義協(xié)議,封裝請(qǐng)求頭和請(qǐng)求體
- 服務(wù)消費(fèi)者 通過自定義編碼器 RpcEncoder 將消息編碼
- 服務(wù)消費(fèi)者 通過 服務(wù)發(fā)現(xiàn)獲取到服務(wù)提供者的ip和端口, 通過Netty網(wǎng)絡(luò)傳輸層發(fā)起調(diào)用
- 服務(wù)消費(fèi)者 通過 RpcFuture 進(jìn)入返回結(jié)果(超時(shí))等待
- 服務(wù)提供者 收到消費(fèi)者請(qǐng)求
- 服務(wù)提供者 將消息通過自定義解碼器 RpcDecoder 解碼
- 服務(wù)提供者 解碼之后的數(shù)據(jù)發(fā)送到 RpcRequestHandler 中進(jìn)行處理,通過反射調(diào)用執(zhí)行服務(wù)端本地方法并獲取結(jié)果
- 服務(wù)提供者 將執(zhí)行的結(jié)果通過 編碼器 RpcEncoder 將消息編碼。(由于請(qǐng)求和響應(yīng)的協(xié)議是一樣,所以編碼器和解碼器可以用一套)
- 服務(wù)消費(fèi)者 將消息通過自定義解碼器 RpcDecoder 解碼
- 服務(wù)消費(fèi)者 通過RpcResponseHandler將消息寫入 請(qǐng)求和響應(yīng) 池中,并設(shè)置 RpcFuture 的響應(yīng)結(jié)果
- 服務(wù)消費(fèi)者 獲取到結(jié)果
以上流程具體可以結(jié)合代碼分析,代碼后面會(huì)給出
環(huán)境搭建
- 操作系統(tǒng):Windows
- 集成開發(fā)工具:IntelliJ IDEA
- 項(xiàng)目技術(shù)棧:SpringBoot 2.5.2 + JDK 1.8 + Netty 4.1.42.Final
- 項(xiàng)目依賴管理工具:Maven 4.0.0
- 注冊(cè)中心:Zookeeeper 3.7.0
項(xiàng)目測(cè)試
- 啟動(dòng) Zookeeper 服務(wù)器:bin/zkServer.cmd
- 啟動(dòng) provider 模塊 ProviderApplication
- 啟動(dòng) consumer 模塊 ConsumerApplication
- 測(cè)試:瀏覽器輸入 http://localhost:9090/hello/world?name=hello,成功返回 您好:hello, rpc 調(diào)用成功
項(xiàng)目代碼地址
https://gitee.com/listen_w/rpc.git
原文地址:https://mp.weixin.qq.com/s/wqs7QjdzikH96Gl1TK6knA