RPC全稱是Remote Procedure Call,即遠程過程調(diào)用。
RPC的作用:
- 屏蔽遠程調(diào)用跟本地調(diào)用的區(qū)別,讓我們感受就是調(diào)用項目內(nèi)的方法;
- 隱藏底層網(wǎng)絡通信的復雜性,專注于業(yè)務邏輯。
網(wǎng)絡傳輸?shù)臄?shù)據(jù)必須是二進制數(shù)據(jù)。
整體協(xié)議就變成三部分內(nèi)容:固定部分、協(xié)議頭部分、協(xié)議體內(nèi)容。在協(xié)議頭部中有一個固定的部分,指定頭部長度,然后在指定協(xié)議體的部分。
序列化的就是將對象轉換成二進制數(shù)據(jù)的過程,反序列化就是反過來二進制轉換為對象的過程。
常用的序列化
JDK原生序列化
序列化具體的實現(xiàn)是有ObjectOutputStream完成的,反序列化的具體實現(xiàn)是有ObjectInputStream完成的。
JSON
JSON序列化需要注意的事項:
- JSON序列化時額外開銷比較大,對于大數(shù)據(jù)量服務這意味著需要巨大的內(nèi)容和磁盤開銷;
- JSON沒有類型,性能不會太好。
Hessian
Hessian是動態(tài)類型、二進制、緊湊的,并且可跨語言一直的一種序列化框架。
Hessian要比JDK、JSON更加緊湊,性能上要好,生成的字節(jié)數(shù)也要小。
Hessian本身也有問題,Java常見對象的類型不支持,比如:
- Linked系列,LinkedHashMap、LinkedHashSet等,可以通過擴展CollectionDeserializer類修復。
- Local類,可以通過ContextSerializerFactory類修復;
- Byte/Short反序列化的時候變成Integer。
ProtoBuf
ProtoBuf是一種輕便、搞笑的結構化數(shù)據(jù)存儲格式,可以用于結構化數(shù)據(jù)序列化,支持Java、Python、C++、Go等語言。
ProtoBuf使用的時候需要定義IDL, 然后使用不同語言的IDL編譯器,生成序列化工具類,優(yōu)點是:
- 序列化后體積相比JSON、Hessian小很多;
- IDL能清晰地描述語義,足以幫助并保證應用程序之間的類型不會丟失,無需類似XML解析器;
- 序列化反序列化速度很快,不需要通過反射獲取類型;
- 消息格式升級和兼容性不錯,可以做到向后兼容。
ProtoBuf不支持的類型:
- 不支持null
- 不支持單純的Map、List集合對象,需要包在對象里面。
RPC框架中網(wǎng)絡IO模型
阻塞IO
應用進程發(fā)起IO系統(tǒng)調(diào)用后,應用進程被阻塞,賺到內(nèi)核空間處理。內(nèi)核開始等待數(shù)據(jù),等待到數(shù)據(jù)之后,在將內(nèi)核中的數(shù)據(jù)拷貝到用戶內(nèi)存中,
整個IO處理完畢后返回進程。最后應用的進程接觸阻塞狀態(tài),運行業(yè)務邏輯。等待數(shù)據(jù)和拷貝數(shù)據(jù)操作時線程會一直處于阻塞狀態(tài)。
IO多路服用
多路就是指多個通道,也就是多個網(wǎng)絡連接的IO,復用是指多個通道復用在一個復用器上。
IO多路復用更適合高并發(fā)的場景。
零拷貝
所謂零拷貝,就是取消用戶空間與內(nèi)核空間之間的數(shù)據(jù)拷貝操作,應用進程每一次的讀寫操作,都可以通過一種方式,讓應用進程向用戶空間寫入或者讀取數(shù)據(jù),
就如同直接向內(nèi)核空間寫入或者讀取數(shù)據(jù)一樣,在通過dma將內(nèi)核中的數(shù)據(jù)拷貝到網(wǎng)卡,或者將網(wǎng)卡中的數(shù)據(jù)copy到內(nèi)核。
零拷貝的兩種實現(xiàn)方式:mmap+write方式,sendFile方式。
mmap+write
mmap是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。
mmap 通過內(nèi)存映射,將文件映射到內(nèi)核緩沖區(qū),同時,用戶空間可以共享內(nèi)核空間的數(shù)據(jù)。這樣,在進行網(wǎng)絡傳輸時,就可以減少內(nèi)核空間到用戶空間的拷貝次數(shù)。
這樣就可以省掉原來內(nèi)核read緩沖區(qū)copy數(shù)據(jù)到用戶緩沖區(qū),但是還是需要內(nèi)核read緩沖區(qū)將數(shù)據(jù)copy到內(nèi)核socket緩沖區(qū)。
sendFile
數(shù)據(jù)被 DMA 引擎從文件復制到內(nèi)核緩沖區(qū),然后調(diào)用 write 方法時,從內(nèi)核緩沖區(qū)進入到 Socket,這時,是沒有上下文切換的,因為都在內(nèi)核空間。
mmap 和 sendFile 的區(qū)別
- mmap 適合小數(shù)據(jù)量讀寫,sendFile 適合大文件傳輸。
- mmap 需要 4 次上下文切換,3 次數(shù)據(jù)拷貝;sendFile 需要 3 次上下文切換,最少 2 次數(shù)據(jù)拷貝。
- sendFile 可以利用 DMA 方式,減少 CPU 拷貝,mmap 則不能(必須從內(nèi)核拷貝到 Socket 緩沖區(qū))。
在這個選擇上:rocketMQ 在消費消息時,使用了 mmap。kafka 使用了 sendFile。
Netty的零拷貝是完全站在用戶空間上,也就是JVM上,它的零拷貝主要是偏向于數(shù)據(jù)操作的優(yōu)化上。
Netty對數(shù)據(jù)操作進行的優(yōu)化
- Netty 提供了CompositeByteBuf類,可以將多個ByteBuf合并為一個邏輯上的ByteBuf,避免了各個ByteBuf之間的拷貝。
- ByteBuffer支持slice操作,因此可以將ByteBuf分解為多個共享同一個存儲區(qū)域的ByteBuf,避免了內(nèi)存的拷貝。
- 通過wrap操作,可以將byte[] 數(shù)組、ByteBuf、ByteBuffer等包裝盛一個Netty ByteBuf對象,進而避免拷貝操作。
- Netty 的ByteBuf可以采用Direct Buffers,使用堆外直接內(nèi)存進行socket的讀寫操作。
- Netty還提供了FileRegion中保證NIO的FileChannel.transferTo方法實現(xiàn)了零拷貝。
面向接口編程,屏蔽RPC處理流程
使用反射來進行處理。
可擴展的框架:
會使用spi方式,在springboot中使用condition接口也可以實現(xiàn)。
服務發(fā)現(xiàn):到底是要CP還是AP
選擇AP。
在大規(guī)模的服務集群下,注冊中心所面臨的挑戰(zhàn)就是超大批量服務節(jié)點同時上下線,注冊中心集群接受到大量服務變更請求,集群件各節(jié)點間需要同步大量服務節(jié)點數(shù)據(jù),
到最如下問題:
- 注冊中心負載過高;
- 各節(jié)點數(shù)據(jù)不一致;
- 服務下發(fā)不及時或者下發(fā)錯誤的服務節(jié)點列表。
RPC框架依賴的注冊中心的服務數(shù)據(jù)的一致性其實并不滿足CP,只要滿足AP即可。可以使用消息總線的通知機制,來保證注冊中心數(shù)據(jù)的最終一致性,來解決這些問題。
當節(jié)點達到一定數(shù)量時,集中進行注冊操作,會導致cpu持續(xù)升高,最終宕機。
健康監(jiān)測:這個節(jié)點都掛了,為啥還要瘋狂發(fā)請求
要保證業(yè)務正常:不同區(qū)域、不同機架去檢測,保證業(yè)務正常。
負載均衡:節(jié)點收到的流量不一樣
dubbo的負載均衡方法:
- 基于權重隨機算法:將請求按照權重進行分配,權重越大,分配的越多。
- 基于最小活躍調(diào)用數(shù)算法:活躍少越少,表明該服務提供者效率越高,單位時間內(nèi)可處理更多的請求。
- 基于hash一致性:適用于服務有狀態(tài)的場景。
- 基于加權輪詢算法:權重越大,節(jié)點選中的更多。
異常重試:在約定時間內(nèi)安全重試
異常重試需要關注的點:
- 保證被重試的業(yè)務服務是具有冪等性的;
- 超時重試前重置計時;
- 針對業(yè)務返回的異常,設置重試是異常名單;
- 重試時負載均衡選取節(jié)點時要剔除前一次訪問的節(jié)點
優(yōu)雅關閉:如何避免服務停機帶來的業(yè)務損失
服務對象在關閉過程中,會拒絕的新的請求,同時根據(jù)引用計數(shù)器等待正在處理的請求全部結束之后才會真正關閉。
Runtime.getRuntime().addShutdownHook(this);,注冊一個JVM關閉的鉤子,這個鉤子可以在以下幾種場景被調(diào)用:
- 程序正常退出
- 使用System.exit()
- 終端使用Ctrl+C觸發(fā)的中斷
- 系統(tǒng)關閉
- 使用Kill pid命令干掉進程(不要帶-9)
優(yōu)雅啟動
在服務啟動之后,要對系統(tǒng)進行預熱,可以執(zhí)行一些測試數(shù)據(jù),在整個系統(tǒng)啟動并且預熱之后,然后在進行服務注冊。
熔斷限流
可以使用Guava RateLimiter、Lua+Redis、Sentinel進行限流,hystrix、resilience4j進行熔斷。
異步RPC
使用CompletableFuture、Future、ThreadPoolExecutor來進行請求。