体系化认识RPC--转
原文地址:http://www.infoq.com/cn/articles/get-to-know-rpc?utm_source=infoq&utm_medium=popular_widget&utm_campaign=popular_content_list&utm_content=homepage
RPC(Remote Procedure Call),即遠程過程調(diào)用,是一個分布式系統(tǒng)間通信的必備技術(shù),本文體系性地介紹了 RPC 包含的核心概念和技術(shù),希望讀者讀完文章,一提到 RPC,腦中不是零碎的知識,而是具體的一個腦圖般的體系。本文并不會深入到每一個主題剖析,只做提綱挈領(lǐng)的介紹。
RPC 最核心要解決的問題就是在分布式系統(tǒng)間,如何執(zhí)行另外一個地址空間上的函數(shù)、方法,就仿佛在本地調(diào)用一樣,個人總結(jié)的 RPC 最核心的概念和技術(shù)包括如下,如圖所示:
(點擊放大圖像)
下面依次展開每個部分。
傳輸(Transport)
TCP 協(xié)議是 RPC 的?基石,一般來說通信是建立在 TCP 協(xié)議之上的,而且 RPC 往往需要可靠的通信,因此不采用 UDP。
這里重申下 TCP 的關(guān)鍵詞:面向連接的,全雙工,可靠傳輸(按序、不重、不丟、容錯),流量控制(滑動窗口)。
另外,要理解 RPC 中的嵌套 header+body,協(xié)議棧每一層都包含了下一層協(xié)議的全部數(shù)據(jù),只不過包了一個頭而已,如下圖所示的 TCP segment 包含了應(yīng)用層的數(shù)據(jù),套了一個頭而已。
(點擊放大圖像)
那么 RPC 傳輸?shù)?message 也就是 TCP body 中的數(shù)據(jù),這個 message 也同樣可以包含 header+body。body 也經(jīng)常叫做 payload。
TCP 就是可靠地把數(shù)據(jù)在不同的地址空間上搬運,例如在傳統(tǒng)的阻塞 I/O 模型中,當有數(shù)據(jù)過來的時候,操作系統(tǒng)內(nèi)核把數(shù)據(jù)從 I/O 中讀出來存放在 kernal space,然后內(nèi)核就通知 user space 可以拷貝走數(shù)據(jù),用以騰出空間,讓 TCP 滑動窗口向前移動,接收更多的數(shù)據(jù)。
TCP 協(xié)議棧存在端口的概念,端口是進程獲取數(shù)據(jù)的渠道。
I/O 模型(I/O Model)
做一個高性能 /scalable 的 RPC,需要能夠滿足:
- 第一,服務(wù)端盡可能多的處理并發(fā)請求
- 第二,同時盡可能短的處理完畢。
CPU 和 I/O 之間天然存在著差異,網(wǎng)絡(luò)傳輸?shù)难訒r不可控,最簡單的模型下,如果有線程或者進程在調(diào)用 I/O,I/O 沒響應(yīng)時,CPU 只能選擇掛起,線程或者進程也被 I/O 阻塞住。
而 CPU 資源寶貴,要讓 CPU 在該忙碌的時候盡量忙碌起來,而不需要頻繁地掛起、喚醒做切換,同時很多寶貴的線程和進程占用系統(tǒng)資源也在做無用功。
Socket I/O 可以看做是二者之間的橋梁,如何更好地協(xié)調(diào)二者,去滿足前面說的兩點要求,有一些模式(pattern)是可以應(yīng)用的。
RPC 框架可選擇的 I/O 模型嚴格意義上有 5 種,這里不討論基于?信號驅(qū)動?的 I/O(Signal Driven I/O)。這幾種模型在《UNIX 網(wǎng)絡(luò)編程》中就有提到了,它們分別是:
這里不細說每種 I/O 模型。這里舉一個形象的例子,讀者就可以領(lǐng)會這四種 I/O 的區(qū)別,就用?銀行辦業(yè)務(wù)?這個生活的場景描述。
下圖是使用?傳統(tǒng)的阻塞 I/O 模型。一個柜員服務(wù)所有客戶,可見當客戶填寫單據(jù)的時候也就是發(fā)生網(wǎng)絡(luò) I/O 的時候,柜員,也就是寶貴的線程或者進程就會被阻塞,白白浪費了 CPU 資源,無法服務(wù)后面的請求。
下圖是上一個的進化版,如果一個柜員不夠,那么就?并發(fā)處理,對應(yīng)采用線程池或者多進程方案,一個客戶對應(yīng)一個柜員,這明顯加大了并發(fā)度,在并發(fā)不高的情況下性能夠用,但是仍然存在柜員被 I/O 阻塞的可能。
下圖是?I/O 多路復用,存在一個大堂經(jīng)理,相當于代理,它來負責所有的客戶,只有當客戶寫好單據(jù)后,才把客戶分配一個柜員處理,可以想象柜員不用阻塞在 I/O 讀寫上,這樣柜員效率會非常高,這也就是 I/O 多路復用的精髓。
下圖是?異步 I/O,完全不存在大堂經(jīng)理,銀行有一個天然的“高級的分配機器”,柜員注冊自己負責的業(yè)務(wù)類型,例如 I/O 可讀,那么由這個“高級的機器”負責 I/O 讀,當可讀時候,通過?回調(diào)機制,把客戶已經(jīng)填寫完畢的單據(jù)主動交給柜員,回調(diào)其函數(shù)完成操作。
重點說下高性能,并且工業(yè)界普遍使用的方案,也就是后兩種。
I/O 多路復用
基于內(nèi)核,建立在 epoll 或者 kqueue 上實現(xiàn),I/O 多路復用最大的優(yōu)勢是用戶可以在一個線程內(nèi)同時處理多個 Socket 的 I/O 請求。用戶可以訂閱事件,包括文件描述符或者 I/O 可讀、可寫、可連接事件等。
通過一個線程監(jiān)聽全部的 TCP 連接,有任何事件發(fā)生就通知用戶態(tài)處理即可,這么做的目的就是?假設(shè) I/O 是慢的,CPU 是快的,那么要讓用戶態(tài)盡可能的忙碌起來去,也就是最大化 CPU 利用率,避免傳統(tǒng)的 I/O 阻塞。
異步 I/O
這里重點說下同步 I/O 和異步 I/O,理論上前三種模型都叫做同步 I/O,同步是指用戶線程發(fā)起 I/O 請求后需要等待或者輪詢內(nèi)核 I/O 完成后再繼續(xù),而異步是指用戶線程發(fā)起 I/O 請求直接退出,當內(nèi)核 I/O 操作完成后會通知用戶線程來調(diào)用其回調(diào)函數(shù)。
進程 / 線程模型(Thread/Process Model)
進程 / 線程模型往往和 I/O 模型有聯(lián)系,當 Socket I/O 可以很高效的工作時候,真正的業(yè)務(wù)邏輯如何利用 CPU 更快地處理請求,也是有 pattern 可尋的。這里主要說 Scalable I/O 一般是如何做的,它的 I/O 需要經(jīng)歷 5 個環(huán)節(jié):
Read -> Decode -> Compute -> Encode -> Send使用傳統(tǒng)的阻塞 I/O + 線程池的方案(Multitasks)會遇?C10k問題。
https://en.wikipedia.org/wiki/C10k_problem
但是業(yè)界有很多實現(xiàn)都是這個方式,比如 Java web 容器 Tomcat/Jetty 的默認配置就采用這個方案,可以工作得很好。
但是從 I/O 模型可以看出 I/O Blocking is killer to performance,它會讓工作線程卡在 I/O 上,而一個系統(tǒng)內(nèi)部可使用的線程數(shù)量是有限的(本文暫時不談協(xié)程、纖程的概念),所以才有了 I/O 多路復用和異步 I/O。
I/O 多路復用往往對應(yīng) Reactor 模式,異步 I/O 往往對應(yīng) Proactor。
Reactor 一般使用?epoll+ 事件驅(qū)動?的經(jīng)典模式,通過?分治?的手段,把耗時的網(wǎng)絡(luò)連接、安全認證、編碼等工作交給專門的線程池或者進程去完成,然后再去調(diào)用真正的核心業(yè)務(wù)邏輯層,這在 *nix 系統(tǒng)中被廣泛使用。
著名的 Redis、Nginx、Node.js 的 Socket I/O 都用的這個,而 Java 的 NIO 框架 Netty 也是,Spark 2.0 RPC 所依賴的同樣采用了 Reactor 模式。
Proactor 在 *nix 中沒有很好的實現(xiàn),但是在 Windows 上大放異彩(例如 IOCP 模型)。
關(guān)于 Reactor 可以參考?Doug Lea 的 PPT
http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
以及?這篇 paper
http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf
關(guān)于 Proactor 可以參考?這篇 paper
http://www.cs.wustl.edu/~schmidt/PDF/proactor.pdf
說個具體的例子,Thrift 作為一個融合了?序列化 +RPC?的框架,提供了很多種 Server 的構(gòu)建選項,從名稱中就可以看出他們使用哪種 I/O 和線程模型。
(點擊放大圖像)
Schema 和序列化(Schema & Data Serialization)
當 I/O 完成后,數(shù)據(jù)可以由程序處理,那么如何識別這些二進制的數(shù)據(jù),是下一步要做的。序列化和反序列化,是做對象到二進制數(shù)據(jù)的轉(zhuǎn)換,程序是可以理解對象的,對象一般含有 schema 或者結(jié)構(gòu),基于這些語義來做特定的業(yè)務(wù)邏輯處理。
考察一個序列化框架一般會關(guān)注以下幾點:
- Encoding format。是 human readable 還是 binary。
- Schema declaration。也叫作契約聲明,基于 IDL,比如 Protocol Buffers/Thrift,還是自描述的,比如 JSON、XML。另外還需要看是否是強類型的。
- 語言平臺的中立性。比如 Java 的 Native Serialization 就只能自己玩,而 Protocol Buffers 可以跨各種語言和平臺。
- 新老契約的兼容性。比如 IDL 加了一個字段,老數(shù)據(jù)是否還可以反序列化成功。
- 和壓縮算法的契合度。跑 benchmark 和實際應(yīng)用都會結(jié)合各種壓縮算法,例如 gzip、snappy。
- 性能。這是最重要的,序列化、反序列化的時間,序列化后數(shù)據(jù)的字節(jié)大小是考察重點。
序列化方式非常多,常見的有?Protocol Buffers, Avro,Thrift,XML,JSON,MessagePack,Kyro,Hessian,Protostuff,Java Native Serialize,FST。
下面詳細展開 Protocol Buffers(簡稱 PB),看看為什么作為工業(yè)界用得最多的高性能序列化類庫,好在哪里。
首先去官網(wǎng)查看它的?Encoding format
https://developers.google.com/protocol-buffers/docs/encoding
緊湊高效?是 PB 的特點,使用字段的序號作為標識,而不是包名類名(Java 的 Native Serialization 序列化后數(shù)據(jù)大就在于什么都一股腦放進去),使用 varint 和 zigzag 對整型做特殊處理。
PB 可以跨各種語言,但是前提是使用 IDL 編寫描述文件,然后 codegen 工具生成各種語言的代碼。
舉個例子,有個 Person 對象,包含內(nèi)容如下圖所示,經(jīng)過 PB 序列化后只有 33 個字節(jié),可以對比 XML、JSON 或者 Java 的 Native Serialization 都會大非常多,而且序列化、反序列化的速度也不會很好。記住這個數(shù)據(jù),后面 demo 的時候會有用。
(點擊放大圖像)
圖片來源
https://www.slideshare.net/SergeyPodolsky/google-protocol-buffers-56085699
再舉個例子,使用 Thrift 做同樣的序列化,采用 Binary Protocol 和 Compact Protocol 的大小是不一樣的,但是 Compact Protocol 和 PB 雖然序列化的編碼不一樣,但是同樣是非常高效的。
(點擊放大圖像)
圖片來源
https://www.slideshare.net/SergeyPodolsky/google-protocol-buffers-56085699
這里給一個?Uber 做的序列化框架比較
https://eng.uber.com/trip-data-squeeze/
可以看出 Protocol Buffers 和 Thrift 都是名列前茅的,但是這些 benchmark 看看就好,知道個大概,沒必要細究,因為樣本數(shù)據(jù)、測試環(huán)境、版本等都可能會影響結(jié)果。
協(xié)議結(jié)構(gòu)(Wire Protocol)
Socket 范疇里討論的包叫做 Frame、Packet、Segment 都沒錯,但是一般把這些分別映射為數(shù)據(jù)鏈路層、IP 層和 TCP 層的數(shù)據(jù)包,應(yīng)用層的暫時沒有,所以下文不必計較包怎么翻譯。
協(xié)議結(jié)構(gòu),英文叫做 wire protocol 或者 wire format。TCP 只是 binary stream 通道,是 binary 數(shù)據(jù)的可靠搬用工,它不懂 RPC 里面包裝的是什么。而在一個通道上傳輸 message,勢必涉及 message 的識別。
舉個例子,正如下圖中的例子,ABC+DEF+GHI 分 3 個 message,也就是分 3 個 Frame 發(fā)送出去,而接收端分四次收到 4 個 Frame。
Socket I/O 的工作完成得很好,可靠地傳輸過去,這是 TCP 協(xié)議保證的,但是接收到的是 4 個 Frame,不是原本發(fā)送的 3 個 message 對應(yīng)的 3 個 Frame。
這種情況叫做發(fā)生了?TCP 粘包和半包?現(xiàn)象,AB、H、I 的情況叫做半包,CDEFG 的情況叫做粘包。雖然順序是對的,但是分組完全和之前對應(yīng)不上。
這時候應(yīng)用層如何做語義級別的 message 識別是個問題,只有做好了協(xié)議的結(jié)構(gòu),才能把一整個數(shù)據(jù)片段做序列化或者反序列化處理。
一般采用的方式有三種:
方式 1:分隔符。
方式 2:換行符。比如 memcache 由客戶端發(fā)送的命令使用的是文本行\(zhòng)r\n 做為 mesage 的分隔符,組織成一個有意義的 message。
圖片來源
https://www.kancloud.cn/kancloud/essential-netty-in-action/52643
圖中的說明:
方式 3:固定長度。RPC 經(jīng)常采用這種方式,使用 header+payload 的方式。
比如 HTTP 協(xié)議,建立在 TCP 之上最廣泛使用的 RPC,HTTP 頭中肯定有一個 body length 告知應(yīng)用層如何去讀懂一個 message,做 HTTP 包的識別。
在 HTTP/2 協(xié)議中,詳細見?Hypertext Transfer Protocol Version 2 (HTTP/2)
https://tools.ietf.org/html/rfc7540
雖然精簡了很多,加入了流的概念,但是 header+payload 的方式是絕對不能變的。
圖片來源
https://tools.ietf.org/html/rfc7540
下面展示的是作者自研的一個 RPC 框架,可以在 github 上找到這個工程?
neoremind/navi-pbrpc:
https://github.com/neoremind/navi-pbrpc
可以看出它的協(xié)議棧 header+payload 方式的,header 固定 36 個字節(jié)長度,最后 4 個字節(jié)是 body length,也就是 payload length,可以使用大尾端或者小尾端編碼。
可靠性(Reliability)
RPC 框架不光要處理 Network I/O、序列化、協(xié)議棧。還有很多不確定性問題要處理,這里的不確定性就是由?網(wǎng)絡(luò)的不可靠?帶來的麻煩。
例如如何保持長連接心跳?網(wǎng)絡(luò)閃斷怎么辦?重連、重傳?連接超時?這些都非常的細碎和麻煩,所以說開發(fā)好一個穩(wěn)定的 RPC 類庫是一個非常系統(tǒng)和細心的工程。
但是好在工業(yè)界有一群人就致力于提供平臺似的解決方案,例如 Java 中的 Netty,它是一個強大的異步、事件驅(qū)動的網(wǎng)絡(luò) I/O 庫,使用 I/O 多路復用的模型,做好了上述的麻煩處理。
它是面向?qū)ο笤O(shè)計模式的集大成者,使用方只需要會使用 Netty 的各種類,進行擴展、組合、插拔,就可以完成一個高性能、可靠的 RPC 框架。
著名的 gRPC Java 版本、Twitter 的 Finagle 框架、阿里巴巴的 Dubbo、新浪微博的 Motan、Spark 2.0 RPC 的網(wǎng)絡(luò)層(可以參考?kraps-rpc:https://github.com/neoremind/kraps-rpc)都采用了這個類庫。
易用性(Ease of use)
RPC 是需要讓上層寫業(yè)務(wù)邏輯來實現(xiàn)功能的,如何優(yōu)雅地啟停一個 server,注入 endpoint,客戶端怎么連,重試調(diào)用,超時控制,同步異步調(diào)用,SDK 是否需要交換等等,都決定了基于 RPC 構(gòu)建服務(wù),甚至 SOA 的工程效率與生產(chǎn)力高低。這里不做展開,看各種 RPC 的文檔就知道他們的易用性如何了。
工業(yè)界的 RPC 框架一覽
國內(nèi)
- Dubbo。來自阿里巴巴?http://dubbo.I/O/
- Motan。新浪微博自用?https://github.com/weibocom/motan
- Dubbox。當當基于 dubbo 的?https://github.com/dangdangdotcom/dubbox
- rpcx。基于 Golang 的?https://github.com/smallnest/rpcx
- Navi & Navi-pbrpc。作者開源的?https://github.com/neoremind/navihttps://github.com/neoremind/navi-pbrpc
國外
- Thrift from facebook?https://thrift.apache.org
- Avro from hadoop?https://avro.apache.org
- Finagle by twitter?https://twitter.github.I/O/finagle
- gRPC by Google?http://www.grpc.I/O?(Google inside use Stuppy)
- Hessian from cuacho?http://hessian.caucho.com
- Coral Service inside amazon?(not open sourced)
上述列出來的都是現(xiàn)在互聯(lián)網(wǎng)企業(yè)常用的解決方案,暫時不考慮傳統(tǒng)的 SOAP,XML-RPC 等。這些是有網(wǎng)絡(luò)資料的,實際上很多公司內(nèi)部都會針對自己的業(yè)務(wù)場景,以及和公司內(nèi)的平臺相融合(比如監(jiān)控平臺等),自研一套框架,但是殊途同歸,都逃不掉剛剛上面所列舉的 RPC 的要考慮的各個部分。
Demo 展示
為了使讀者更好地理解上面所述的各個章節(jié),下面做一個簡單例子分析。使用?neoremind/navi-pbrpc:https://github.com/neoremind/navi-pbrpc?來做 demo,使用 Java 語言來開發(fā)。
假設(shè)要開發(fā)一個服務(wù)端和客戶端,服務(wù)端提供一個請求響應(yīng)接口,請求是?user_id,響應(yīng)是一個 user 的數(shù)據(jù)結(jié)構(gòu)對象。
首先定義一個 IDL,使用 PB 來做 Schema 聲明,IDL 描述如下,第一個 Request 是請求,第二個 Person 是響應(yīng)的對象結(jié)構(gòu)。
然后使用 codegen 生成對應(yīng)的代碼,例如生成了 PersonPB.Request 和 PersonPB.Person 兩個 class。
server 端需要開發(fā)請求響應(yīng)接口,API 是?PersonPB.Person doSmth(PersonPB.Request req),實現(xiàn)如下,包含一個 Interface 和一個實現(xiàn) class。
server 返回的是一個 Person 對象,里面的內(nèi)容主要就是上面講到的 PB 例子里面的。
啟動 server。在 8098 端口開啟服務(wù),客戶端需要靠 id=100 這個標識來路由到這個服務(wù)。
至此,服務(wù)端開發(fā)完畢,可以看出使用一個完善的 RPC 框架,只需要定義好 Schema 和業(yè)務(wù)邏輯就可以發(fā)布一個 RPC,而 I/O model、線程模型、序列化 / 反序列化、協(xié)議結(jié)構(gòu)均由框架服務(wù)。
navi-pbrpc 底層使用 Netty,在 Linux 下會使用 epoll 做 I/O 多路復用,線程模型默認采用 Reactor 模式,序列化和反序列化使用 PB,協(xié)議結(jié)構(gòu)見上文部分介紹的,是一個標準的 header+payload 結(jié)構(gòu)。
下面開發(fā)一個 client,調(diào)用剛剛開發(fā)的 RPC。
client 端代碼實現(xiàn)如下。首先構(gòu)造 PbrpcClient,然后構(gòu)造 PersonPB.Request,也就是請求,設(shè)置好 user_id,構(gòu)造 PbrpcMsg 作為 TCP 層傳輸?shù)臄?shù)據(jù) payload,這就是協(xié)議結(jié)構(gòu)中的 body 部分。
通過 asyncTransport 進行通信,返回一個 Future 句柄,通過 Future.get 阻塞獲取結(jié)果并且打印。
至此,可以看出作為一個 RPC client 易用性是很簡單的,同時可靠性,例如重試等會由 navi-pbrpc 框架負責完成,用戶只需要聚焦到真正的業(yè)務(wù)邏輯即可。
下面繼續(xù)深入到 binary stream 級別觀察,使用嗅探工具來看看 TCP 包。一般使用?wireshark?或者?tcpdump。
客戶端的一次請求調(diào)用如下圖所示,第一個包就是 TCP 三次握手的 SYN 包。
(點擊放大圖像)
根據(jù) TCP 頭協(xié)議,可看出來。
- ff 15 = 65301 是客戶端的端口
- 1f a2 = 8098 是服務(wù)端的端口
- header 的長度 44 字節(jié)是 20 字節(jié)頭 +20 字節(jié) option+padding 構(gòu)成的。
三次握手成功后,下面客戶端發(fā)起了 RPC 請求,如下圖所示。
(點擊放大圖像)
可以看出 TCP 包含了一個 message,由 navi-pbrpc 的協(xié)議棧規(guī)定的 header+payload 構(gòu)成,
繼續(xù)深入分析 message 中的內(nèi)容,如下圖所示:
(點擊放大圖像)
其中
- 61 70 = ap 是頭中的的 provider 標識
- body length 是 2,注意 navi-pbrpc 采用了小尾端。
- payload 是 08 7f,08 在 PB 中理解為第一個屬性,是 varint 整型,7f 表示傳輸?shù)氖?127 這個整型。
服務(wù)端響應(yīng) RPC 請求,還是由 navi-pbrpc 的協(xié)議棧規(guī)定的 header+payload 構(gòu)成,可以看出 body 就是 PB 例子里面的二進制數(shù)據(jù)。
(點擊放大圖像)
最后,客戶端退出,四次分手結(jié)束。
總結(jié)
本文系統(tǒng)性地介紹了 RPC 包含的核心概念和技術(shù),帶著讀者從一個實際的例子去映射理解。很多東西都是蜻蜓點水,每一個關(guān)鍵字都能成為一個很大的話題,希望這個提綱挈領(lǐng)的介紹可以讓讀者在大腦里面有一個系統(tǒng)的體系去看待 RPC。
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/7744534.html
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的体系化认识RPC--转的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jsoup HTML parser he
- 下一篇: 当我说要做大数据工程师时他们都笑我,直到