经典项目|手撸一个高质量RPC框架
hi, 大家好,RPC是后端系統節點之間通信的核心技術,屬于后端開發必須要學習的技能。
后端技術趨勢指南|如何選擇自己的技術方向
如何從0搭建公司的后端技術棧
遠程過程調用(Remote Procedure Call,縮寫為 RPC)是一個計算機通信協議。該協議允許運行于一臺計算機的程序調用另一臺計算機的子程序,而程序員無需額外地為這個交互作用編程。如果涉及的軟件采用面向對象編程,那么遠程過程調用亦可稱作遠程調用或遠程方法調用。
微服務時代的遠程服務調用框架。如grpc, Thrift, 阿里的 HSF, Dubbo, SOFA-RPC;
未來RPC技術發展方向:
支持微服務技術演進
框架侵入性改進,語言無關,通信協議無關。
Service Mesh,Service Mesh是一個基礎設施層,其獨立運行在應用服務之外,提供應用服務之間安全、可靠、高效的通信,并為服務通信實現了微服務運行所需的基本組件功能,包括服務注冊發現、負載均衡、故障恢復、監控、權限控制等等
性能優化,序列化協議優化,消息編碼優化,網絡IO優化等。
項目來源:juejin.cn/post/6992867064952127524
概念篇
RPC 是什么?
RPC 稱遠程過程調用(Remote Procedure Call),用于解決分布式系統中服務之間的調用問題。通俗地講,就是開發者能夠像調用本地方法一樣調用遠程的服務。所以,RPC的作用主要體現在這兩個方面:
屏蔽遠程調用跟本地調用的區別,讓我們感覺就是調用項目內的方法;
隱藏底層網絡通信的復雜性,讓我們更專注于業務邏輯。
RPC 框架基本架構
下面我們通過一幅圖來說說 RPC 框架的基本架構
RPC 框架包含三個最重要的組件,分別是客戶端、服務端和注冊中心。在一次 RPC 調用流程中,這三個組件是這樣交互的:
服務端在啟動后,會將它提供的服務列表發布到注冊中心,客戶端向注冊中心訂閱服務地址;
客戶端會通過本地代理模塊 Proxy 調用服務端,Proxy 模塊收到負責將方法、參數等數據轉化成網絡字節流;
客戶端從服務列表中選取其中一個的服務地址,并將數據通過網絡發送給服務端;
服務端接收到數據后進行解碼,得到請求信息;
服務端根據解碼后的請求信息調用對應的服務,然后將調用結果返回給客戶端。
RPC 框架通信流程以及涉及到的角色
從上面這張圖中,可以看見 RPC 框架一般有這些組件:服務治理(注冊發現)、負載均衡、容錯、序列化/反序列化、編解碼、網絡傳輸、線程池、動態代理等角色,當然有的RPC框架還會有連接池、日志、安全等角色。
具體調用過程
服務消費方(client)以本地調用方式調用服務
client stub 接收到調用后負責將方法、參數等封裝成能夠進行網絡傳輸的消息體
client stub 將消息進行編碼并發送到服務端
server stub 收到消息后進行解碼
server stub 根據解碼結果調用本地的服務
本地服務執行并將結果返回給 server stub
server stub 將返回導入結果進行編碼并發送至消費方
client stub 接收到消息并進行解碼
服務消費方(client)得到結果
RPC 消息協議
RPC調用過程中需要將參數編組為消息進行發送,接收方需要解組消息為參數,過程處理結果同樣需要經編組、解組。消息由哪些部分構成及消息的表示形式就構成了消息協議。
RPC調用過程中采用的消息協議稱為RPC消息協議。
實戰篇
從上面的概念我們知道一個RPC框架大概有哪些部分組成,所以在設計一個RPC框架也需要從這些組成部分考慮。從RPC的定義中可以知道,RPC框架需要屏蔽底層細節,讓用戶感覺調用遠程服務像調用本地方法一樣簡單,所以需要考慮這些問題:
用戶使用我們的RPC框架時如何盡量少的配置
如何將服務注冊到ZK(這里注冊中心選擇ZK)上并且讓用戶無感知
如何調用透明(盡量用戶無感知)的調用服務提供者
啟用多個服務提供者如何做到動態負載均衡
框架如何做到能讓用戶自定義擴展組件(比如擴展自定義負載均衡策略)
如何定義消息協議,以及編解碼
...等等
上面這些問題在設計這個RPC框架中都會給予解決。
國內外知名的RPC框架
RPC只是描繪了 Client 與 Server 之間的點對點調用流程,包括 stub、通信、RPC 消息解析等部分,在實際應用中,還需要考慮服務的高可用、負載均衡等問題,所以產品級的 RPC 框架除了點對點的 RPC 協議的具體實現外,還應包括服務的發現與注銷、提供服務的多臺 Server 的負載均衡、服務的高可用等更多的功能。目前的 RPC 框架大致有兩種不同的側重方向,一種偏重于服務治理,另一種偏重于跨語言調用。
服務治理型的 RPC 框架有Alibab Dubbo、Motan 等,這類的 RPC 框架的特點是功能豐富,提供高性能的遠程調用以及服務發現和治理功能,適用于大型服務的微服務化拆分以及管理,對于特定語言(Java)的項目可以十分友好的透明化接入。但缺點是語言耦合度較高,跨語言支持難度較大。
跨語言調用型的 RPC 框架有 Thrift、gRPC、Hessian、Finagle 等,這一類的 RPC 框架重點關注于服務的跨語言調用,能夠支持大部分的語言進行語言無關的調用,非常適合于為不同語言提供通用遠程服務的場景。但這類框架沒有服務發現相關機制,實際使用時一般需要代理層進行請求轉發和負載均衡策略控制。
Dubbo 是阿里巴巴公司開源的一個Java高性能優秀的服務框架,使得應用可通過高性能的 RPC 實現服務的輸出和輸入功能,可以和 Spring框架無縫集成。不過,遺憾的是,據說在淘寶內部,dubbo由于跟淘寶另一個類似的框架HSF(非開源)有競爭關系,導致dubbo團隊已經解散(參見http://www.oschina.net/news/55059/druid-1-0-9 中的評論)。不過反倒是墻內開花墻外香,其它的一些知名電商如當當 (dubbox)、京東、國美維護了自己的分支或者在dubbo的基礎開發, 但是官方的實現缺乏維護,其它電商雖然維護了自己的版本,但是還是不能做大的架構的改動和提升,相關的依賴類比如Spring,Netty還是很老的版本(Spring 3.2.16.RELEASE, netty 3.2.5.Final), 而且現在看來,Dubbo的代碼結構也過于復雜了。
所以,盡管Dubbo在電商的開發圈比較流行的時候,國內一些的互聯網公司也在開發自己的RPC框架,比如Motan。Motan是新浪微博開源的一個Java 框架。它誕生的比較晚,起于2013年,2016年5月開源。Motan 在微博平臺中已經廣泛應用,每天為數百個服務完成近千億次的調用。Motan的架構相對簡單,功能也能滿足微博內部架構的要求, 雖然Motan的架構的目的主要不是跨語言,但是目前也在開發支持php client和C server特性。
gRPC是Google開發的高性能、通用的開源RPC框架,其由Google主要面向移動應用開發并基于HTTP/2協議標準而設計,基于ProtoBuf(Protocol Buffers)序列化協議開發,且支持眾多開發語言。它的目標的跨語言開發,支持多種語言, 服務治理方面需要自己去實現,所以要實現一個綜合的產品級的分布式RPC平臺還需要擴展開發。Google內部使用的也不是gRPC,而是Stubby。
thrift是Apache的一個跨語言的高性能的服務框架,也得到了廣泛的應用。它的功能類似 gRPC, 支持跨語言,不支持服務治理。
rpcx?是一個分布式的Go語言的 RPC 框架,支持Zookepper、etcd、consul多種服務發現方式,多種服務路由方式, 是目前性能最好的 RPC 框架之一。
https://doc.rpcx.io/
技術選型
注冊中心 目前成熟的注冊中心有Zookeeper,Nacos,Consul,Eureka,這里使用ZK作為注冊中心,沒有提供切換以及用戶自定義注冊中心的功能。
IO通信框架 本實現采用 Netty 作為底層通信框架,因為Netty 是一個高性能事件驅動型的非阻塞的IO(NIO)框架,沒有提供別的實現,也不支持用戶自定義通信框架
消息協議 本實現使用自定義消息協議,后面會具體說明
項目總體結構
從這個結構中可以知道,以rpc命名開頭的是rpc框架的模塊,也是本項目RPC框架的內容,而consumer是服務消費者,provider是服務提供者,provider-api是暴露的服務API。
整體依賴情況
項目實現介紹
要做到用戶使用我們的RPC框架時盡量少的配置,所以把rpc框架設計成一個starter,用戶只要依賴這個starter,基本那就可以了。
為什么要設計成兩個 starter (client-starter/server-starter) ?
這個是為了更好的體現出客戶端和服務端的概念,消費者依賴客戶端,服務提供者依賴服務端,還有就是最小化依賴。
為什么要設計成 starter ?
基于spring boot自動裝配機制,會加載starter中的 spring.factories 文件,在文件中配置以下代碼,這里我們starter的配置類就生效了,在配置類里面配置一些需要的bean。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.rrtv.rpc.client.config.RpcClientAutoConfiguration發布服務和消費服務
對于發布服務
服務提供者需要在暴露的服務上增加注解 @RpcService,這個自定義注解是基于 @service 的,是一個復合注解,具備@service注解的功能,在@RpcService注解中指明服務接口和服務版本,發布服務到ZK上,會根據這個兩個元數據注冊
發布服務原理:
服務提供者啟動之后,根據spring boot自動裝配機制,server-starter的配置類就生效了,在一個 bean 的后置處理器(RpcServerProvider)中獲取被注解 @RpcService 修飾的bean,將注解的元數據注冊到ZK上。
對于消費服務
消費服務需要使用自定義的 @RpcAutowired 注解標識,是一個復合注解,基于 @Autowired。
消費服務原理
要讓客戶端無感知的調用服務提供者,就需要使用動態代理,如上面所示, HelloWordService 沒有實現類,需要給它賦值代理類,在代理類中發起請求調用。
基于spring boot自動裝配,服務消費者啟動,bean 后置處理器 RpcClientProcessor 開始工作,它主要是遍歷所有的bean,判斷每個bean中的屬性是否有被 @RpcAutowired 注解修飾,有的話把該屬性動態賦值代理類,這個再調用時會調用代理類的 invoke 方法。
代理類 invoke 方法通過服務發現獲取服務端元數據,封裝請求,通過netty發起調用。
注冊中心
本項目注冊中心使用ZK,由于注冊中心被服務消費者和服務提供者都使用。所以把ZK放在rpc-core模塊。
rpc-core 這個模塊如上圖所示,核心功能都在這個模塊。服務注冊在 register 包下。
服務注冊接口,具體實現使用ZK實現。
負載均衡策略
負載均衡定義在rpc-core中,目前支持輪詢(FullRoundBalance)和隨機(RandomBalance),默認使用隨機策略。由rpc-client-spring-boot-starter指定。
通過ZK服務發現時會找到多個實例,然后通過負載均衡策略獲取其中一個實例
可以在消費者中配置 rpc.client.balance=fullRoundBalance 替換,也可以自定義負載均衡策略,通過實現接口 LoadBalance,并將創建的類加入IOC容器即可。由于我們配置 @ConditionalOnMissingBean,所以會優先加載用戶自定義的 bean。
自定義消息協議、編解碼
所謂協議,就是通信雙方事先商量好規則,服務端知道發送過來的數據將如何解析。
自定義消息協議
魔數:魔數是通信雙方協商的一個暗號,通常采用固定的幾個字節表示。魔數的作用是防止任何人隨便向服務器的端口上發送數據。例如 java Class 文件開頭就存儲了魔數 0xCAFEBABE,在加載 Class 文件時首先會驗證魔數的正確性
協議版本號:隨著業務需求的變化,協議可能需要對結構或字段進行改動,不同版本的協議對應的解析方法也是不同的。
序列化算法:序列化算法字段表示數據發送方應該采用何種方法將請求的對象轉化為二進制,以及如何再將二進制轉化為對象,如 JSON、Hessian、Java 自帶序列化等。
報文類型:在不同的業務場景中,報文可能存在不同的類型。RPC 框架中有請求、響應、心跳等類型的報文。
狀態:狀態字段用于標識請求是否正常(SUCCESS、FAIL)。
消息ID:請求唯一ID,通過這個請求ID將響應關聯起來,也可以通過請求ID做鏈路追蹤。
數據長度:標明數據的長度,用于判斷是否是一個完整的數據包
數據內容:請求體內容
編解碼
編解碼實現在 rpc-core 模塊,在包 com.rrtv.rpc.core.codec下。
自定義編碼器通過繼承 netty 的 MessageToByteEncoder<MessageProtocol<T>>類實現消息編碼。
自定義解碼器通過繼承 netty 的 ByteToMessageDecoder類實現消息解碼。
解碼時需要注意TCP粘包、拆包問題
什么是TCP粘包、拆包
TCP 傳輸協議是面向流的,沒有數據包界限,也就是說消息無邊界。客戶端向服務端發送數據時,可能將一個完整的報文拆分成多個小報文進行發送,也可能將多個報文合并成一個大的報文進行發送。因此就有了拆包和粘包。
在網絡通信的過程中,每次可以發送的數據包大小是受多種因素限制的,如 MTU 傳輸單元大小、滑動窗口等。
所以如果一次傳輸的網絡包數據大小超過傳輸單元大小,那么我們的數據可能會拆分為多個數據包發送出去。如果每次請求的網絡包數據都很小,比如一共請求了 10000 次,TCP 并不會分別發送 10000 次。TCP采用的 Nagle(批量發送,主要用于解決頻繁發送小數據包而帶來的網絡擁塞問題) 算法對此作出了優化。
所以,網絡傳輸會出現這樣:
tcp_package.png服務端恰巧讀到了兩個完整的數據包 A 和 B,沒有出現拆包/粘包問題;
服務端接收到 A 和 B 粘在一起的數據包,服務端需要解析出 A 和 B;
服務端收到完整的 A 和 B 的一部分數據包 B-1,服務端需要解析出完整的 A,并等待讀取完整的 B 數據包;
服務端接收到 A 的一部分數據包 A-1,此時需要等待接收到完整的 A 數據包;
數據包 A 較大,服務端需要多次才可以接收完數據包 A。
如何解決TCP粘包、拆包問題
解決問題的根本手段:找出消息的邊界:
消息長度固定
每個數據報文都需要一個固定的長度。當接收方累計讀取到固定長度的報文后,就認為已經獲得一個完整的消息。當發送方的數據小于固定長度時,則需要空位補齊。
消息定長法使用非常簡單,但是缺點也非常明顯,無法很好設定固定長度的值,如果長度太大會造成字節浪費,長度太小又會影響消息傳輸,所以在一般情況下消息定長法不會被采用。
特定分隔符
在每次發送報文的尾部加上特定分隔符,接收方就可以根據特殊分隔符進行消息拆分。分隔符的選擇一定要避免和消息體中字符相同,以免沖突。否則可能出現錯誤的消息拆分。比較推薦的做法是將消息進行編碼,例如 base64 編碼,然后可以選擇 64 個編碼字符之外的字符作為特定分隔符
消息長度 + 消息內容
消息長度 + 消息內容是項目開發中最常用的一種協議,接收方根據消息長度來讀取消息內容。
本項目就是利用 “消息長度 + 消息內容” 方式解決TCP粘包、拆包問題的。所以在解碼時要判斷數據是否夠長度讀取,沒有不夠說明數據沒有準備好,繼續讀取數據并解碼,這里這種方式可以獲取一個個完整的數據包。
序列化和反序列化
序列化和反序列化在 rpc-core 模塊 com.rrtv.rpc.core.serialization 包下,提供了 HessianSerialization 和 JsonSerialization 序列化。
默認使用 HessianSerialization 序列化。用戶不可以自定義。
序列化性能:
空間上
時間上
網絡傳輸,使用netty
netty 代碼固定的,值得注意的是 handler 的順序不能弄錯,以服務端為例,編碼是出站操作(可以放在入站后面),解碼和收到響應都是入站操作,解碼要在前面。
image.png客戶端 RPC 調用方式
成熟的 RPC 框架一般會提供四種調用方式,分別為同步 Sync、異步 Future、回調 Callback和單向 Oneway。
Sync 同步調用
客戶端線程發起 RPC 調用后,當前線程會一直阻塞,直至服務端返回結果或者處理超時異常。
sync.pngFuture 異步調用
客戶端發起調用后不會再阻塞等待,而是拿到 RPC 框架返回的 Future 對象,調用結果會被服務端緩存,客戶端自行決定后續何時獲取返回結果。當客戶端主動獲取結果時,該過程是阻塞等待的
future.pngCallback 回調調用
客戶端發起調用時,將 Callback 對象傳遞給 RPC 框架,無須同步等待返回結果,直接返回。當獲取到服務端響應結果或者超時異常后,再執行用戶注冊的 Callback 回調
callback.pngOneway 單向調用
客戶端發起請求之后直接返回,忽略返回結果
oneway.png這里使用的是第一種:客戶端同步調用,其他的沒有實現。邏輯在 RpcFuture 中,使用 CountDownLatch 實現阻塞等待(超時等待)
整體架構和流程
流程分為三塊:服務提供者啟動流程、服務消費者啟動、調用過程
服務提供者啟動
服務提供者 provider 會依賴 rpc-server-spring-boot-starter
ProviderApplication 啟動,根據springboot 自動裝配機制,RpcServerAutoConfiguration 自動配置生效
RpcServerProvider 是一個bean后置處理器,會發布服務,將服務元數據注冊到ZK上
RpcServerProvider.run 方法會開啟一個 netty 服務
服務消費者啟動
服務消費者 consumer 會依賴 rpc-client-spring-boot-starter
ConsumerApplication 啟動,根據springboot 自動裝配機制,RpcClientAutoConfiguration 自動配置生效
將服務發現、負載均衡、代理等bean加入IOC容器
后置處理器 RpcClientProcessor 會掃描 bean ,將被 @RpcAutowired 修飾的屬性動態賦值為代理對象
調用過程
服務消費者 發起請求http://localhost:9090/hello/world?name=hello
服務消費者 調用 helloWordService.sayHello() 方法,會被代理到執行 ClientStubInvocationHandler.invoke() 方法
服務消費者 通過ZK服務發現獲取服務元數據,找不到報錯404
服務消費者 自定義協議,封裝請求頭和請求體
服務消費者 通過自定義編碼器 RpcEncoder 將消息編碼
服務消費者 通過 服務發現獲取到服務提供者的ip和端口, 通過Netty網絡傳輸層發起調用
服務消費者 通過 RpcFuture 進入返回結果(超時)等待
服務提供者 收到消費者請求
服務提供者 將消息通過自定義解碼器 RpcDecoder 解碼
服務提供者 解碼之后的數據發送到 RpcRequestHandler 中進行處理,通過反射調用執行服務端本地方法并獲取結果
服務提供者 將執行的結果通過 編碼器 RpcEncoder 將消息編碼。(由于請求和響應的協議是一樣,所以編碼器和解碼器可以用一套)
服務消費者 將消息通過自定義解碼器 RpcDecoder 解碼
服務消費者 通過RpcResponseHandler將消息寫入 請求和響應 池中,并設置 RpcFuture 的響應結果
服務消費者 獲取到結果
以上流程具體可以結合代碼分析,代碼后面會給出
環境搭建
操作系統:Windows
集成開發工具:IntelliJ IDEA
項目技術棧:SpringBoot 2.5.2 + JDK 1.8 + Netty 4.1.42.Final
項目依賴管理工具:Maven 4.0.0
注冊中心:Zookeeeper 3.7.0
項目測試
啟動 Zookeeper 服務器:bin/zkServer.cmd
啟動 provider 模塊 ProviderApplication
啟動 consumer 模塊 ConsumerApplication
測試:瀏覽器輸入 http://localhost:9090/hello/world?name=hello,成功返回 您好:hello, rpc 調用成功
項目代碼地址
java:https://gitee.com/listen_w/rpc.git
go:https://gitee.com/mirrors/rpcx
grpc:https://grpc.io/
其他后端開源項目:
一些優秀的后端開源項目!
- END -
看完一鍵三連在看,轉發,點贊
是對文章最大的贊賞,極客重生感謝你
推薦閱讀
定個目標|建立自己的技術知識體系
大廠后臺開發基本功修煉路線和經典資料
后端技術趨勢指南|如何選擇自己的技術方向
你好,這里是極客重生,我是阿榮,大家都叫我榮哥,從華為->外企->到互聯網大廠,目前是大廠資深工程師,多次獲得五星員工,多年職場經驗,技術扎實,專業后端開發和后臺架構設計,熱愛底層技術,豐富的實戰經驗,分享技術的本質原理,希望幫助更多人蛻變重生,拿BAT大廠offer,培養高級工程師能力,成為技術專家,實現高薪夢想,期待你的關注!點擊藍字查看我的成長之路。
校招/社招/簡歷/面試技巧/大廠技術棧分析/后端開發進階/優秀開源項目/直播分享/技術視野/實戰高手等,?極客星球希望成為最有技術價值星球,盡最大努力為星球的同學提供技術和成長幫助!詳情查看->極客星球
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 求點贊,在看,分享三連
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的经典项目|手撸一个高质量RPC框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入理解Java内存架构
- 下一篇: 计算机基础扎实,到底是说什么?