為什么要學(xué)習(xí)RPC
如下是Http請求案例:
請求過程會有3次握手4次揮手:
1:瀏覽器請求服務(wù)器(訂單服務(wù)),請求建立鏈接 1次握手
2:服務(wù)器(訂單服務(wù))響應(yīng)瀏覽器锅尘,可以建立鏈接继薛,并詢問瀏覽器是否可以建立鏈接 2次握手
3:瀏覽器響應(yīng)服務(wù)器(訂單服務(wù)),可以建立鏈接 3次握手
------開始傳輸數(shù)據(jù)------
1:瀏覽器向服務(wù)端(訂單服務(wù))發(fā)起請求钉凌,要求斷開鏈接 1次揮手
2:服務(wù)器(訂單服務(wù))回應(yīng)瀏覽器,數(shù)據(jù)還在傳輸中 2次揮手
3:服務(wù)器(訂單服務(wù))接收完數(shù)據(jù)后,向?yàn)g覽器發(fā)消息要求斷開鏈接 3次揮手
4:瀏覽器收到服務(wù)器消息后,回復(fù)服務(wù)器(訂單服務(wù))同意斷開鏈接 4次揮手
1.1 PRC概述
RPC 的主要功能目標(biāo)是讓構(gòu)建分布式計(jì)算(應(yīng)用)更容易,在提供強(qiáng)大的遠(yuǎn)程調(diào)用能力時(shí)不損失本地調(diào)用的語義簡潔性捂人。為實(shí)現(xiàn)該目標(biāo)御雕,RPC 框架需提供一種透明調(diào)用機(jī)制,讓使用者不必顯式的區(qū)分本地調(diào)用和遠(yuǎn)程調(diào)用滥搭。
RPC的優(yōu)點(diǎn):
分布式設(shè)計(jì)
部署靈活
解耦服務(wù)
擴(kuò)展性強(qiáng)
RPC框架優(yōu)勢:
- RPC框架一般使用長鏈接酸纲,不必每次通信都要3次握手,減少網(wǎng)絡(luò)開銷瑟匆。
- RPC框架一般都有注冊中心闽坡,有豐富的監(jiān)控管理、發(fā)布脓诡、下線接口无午、動態(tài)擴(kuò)展等,對調(diào)用方來說是無感知祝谚、統(tǒng)一化的操作、協(xié)議私密酣衷,安全性較高
- RPC 協(xié)議更簡單內(nèi)容更小交惯,效率更高,服務(wù)化架構(gòu)穿仪、服務(wù)化治理席爽,RPC框架是一個(gè)強(qiáng)力的支撐。
- RPC基于TCP實(shí)現(xiàn)啊片,也可以基于Http2實(shí)現(xiàn)
1.2 RPC框架
主流RPC框架:
- Dubbo:國內(nèi)最早開源的 RPC 框架只锻,由阿里巴巴公司開發(fā)并于 2011 年末對外開源,僅支持 Java 語言紫谷。
- Motan:新浪微博內(nèi)部使用的 RPC 框架齐饮,于 2016 年對外開源捐寥,僅支持 Java 語言。
- Tars:騰訊內(nèi)部使用的 RPC 框架祖驱,于 2017 年對外開源握恳,僅支持 C++ 語言。
- Spring Cloud:國外 Pivotal 公司 2014 年對外開源的 RPC 框架捺僻,提供了豐富的生態(tài)組件乡洼。
- gRPC:Google 于 2015 年對外開源的跨語言 RPC 框架,支持多種語言匕坯。
- Thrift:最初是由 Facebook 開發(fā)的內(nèi)部系統(tǒng)跨語言的 RPC 框架束昵,2007 年貢獻(xiàn)給了 Apache 基金,成為 Apache 開源項(xiàng)目之一葛峻,支持多種語言妻怎。
1.3 應(yīng)用場景
應(yīng)用例舉:
- 分布式操作系統(tǒng)的進(jìn)程間通訊
進(jìn)程間通訊是操作系統(tǒng)必須提供的基本設(shè)施之一,分布式操作系統(tǒng)必須提供分布于異構(gòu)的結(jié)點(diǎn)機(jī)上進(jìn)程間的通訊機(jī)制,RPC是實(shí)現(xiàn)消息傳送模式的分布式進(jìn)程間通訊方式之一泞歉。 - 構(gòu)造分布式設(shè)計(jì)的軟件環(huán)境
由于分布式軟件設(shè)計(jì)逼侦,服務(wù)與環(huán)境的分布性, 它的各個(gè)組成成份之間存在大量的交互和通訊, RPC是其基本的實(shí)現(xiàn)方法之一。Dubbo分布式服務(wù)框架基于RPC實(shí)現(xiàn)腰耙,Hadoop也采用了RPC方式實(shí)現(xiàn)客戶端與服務(wù)端的交互榛丢。 - 遠(yuǎn)程數(shù)據(jù)庫服務(wù)
在分布式數(shù)據(jù)庫系統(tǒng)中,數(shù)據(jù)庫一般駐存在服務(wù)器上挺庞,客戶機(jī)通過遠(yuǎn)程數(shù)據(jù)庫服務(wù)功能訪問數(shù)據(jù)庫服務(wù)器晰赞,現(xiàn)有的遠(yuǎn)程數(shù)據(jù)庫服務(wù)是使用RPC模式的。例如选侨,Sybase和Oracle都提供了存儲過程機(jī)制掖鱼,系統(tǒng)與用戶定義的存儲過程存儲在數(shù)據(jù)庫服務(wù)器上,用戶在客戶端使用RPC模式調(diào)用存儲過程援制。 - 分布式應(yīng)用程序設(shè)計(jì)
RPC機(jī)制與RPC工具為分布式應(yīng)用程序設(shè)計(jì)提供了手段和方便, 用戶可以無需知道網(wǎng)絡(luò)結(jié)構(gòu)和協(xié)議細(xì)節(jié)而直接使用RPC工具設(shè)計(jì)分布式應(yīng)用程序戏挡。 - 分布式程序的調(diào)試
RPC可用于分布式程序的調(diào)試。使用反向RPC使服務(wù)器成為客戶并向它的客戶進(jìn)程發(fā)出RPC晨仑,可以調(diào)試分布式程序褐墅。例如,在服務(wù)器上運(yùn)行一個(gè)遠(yuǎn)端調(diào)試程序洪己,它不斷接收客戶端的RPC妥凳,當(dāng)遇到一個(gè)調(diào)試程序斷點(diǎn)時(shí),它向客戶機(jī)發(fā)回一個(gè)RPC答捕,通知斷點(diǎn)已經(jīng)到達(dá)逝钥,這也是RPC用于進(jìn)程通訊的例子。
2. 深入RPC原理
2.1 設(shè)計(jì)與調(diào)用流程
具體調(diào)用過程:
- 服務(wù)消費(fèi)者(client客戶端)通過本地調(diào)用的方式調(diào)用服務(wù)拱镐。
- 客戶端存根(client stub)接收到請求后負(fù)責(zé)將方法艘款、入?yún)⒌刃畔⑿蛄谢ńM裝)成能夠進(jìn)行網(wǎng)絡(luò)傳輸?shù)南Ⅲw持际。
- 客戶端存根(client stub)找到遠(yuǎn)程的服務(wù)地址,并且將消息通過網(wǎng)絡(luò)發(fā)送給服務(wù)端磷箕。
- 服務(wù)端存根(server stub)收到消息后進(jìn)行解碼(反序列化操作)选酗。
- 服務(wù)端存根(server stub)根據(jù)解碼結(jié)果調(diào)用本地的服務(wù)進(jìn)行相關(guān)處理。
- 本地服務(wù)執(zhí)行具體業(yè)務(wù)邏輯并將處理結(jié)果返回給服務(wù)端存根(server stub)岳枷。
- 服務(wù)端存根(server stub)將返回結(jié)果重新打包成消息(序列化)并通過網(wǎng)絡(luò)發(fā)送至消費(fèi)方芒填。
- 客戶端存根(client stub)接收到消息,并進(jìn)行解碼(反序列化)空繁。
- 服務(wù)消費(fèi)方得到最終結(jié)果殿衰。
所涉及的技術(shù):
-
動態(tài)代理
生成Client Stub(客戶端存根)和Server Stub(服務(wù)端存根)的時(shí)候需要用到j(luò)ava動態(tài)代理技術(shù)。
-
序列化
在網(wǎng)絡(luò)中盛泡,所有的數(shù)據(jù)都將會被轉(zhuǎn)化為字節(jié)進(jìn)行傳送闷祥,需要對這些參數(shù)進(jìn)行序列化和反序列化操作。目前主流高效的開源序列化框架有Kryo傲诵、fastjson凯砍、Hessian、Protobuf等拴竹。
-
NIO通信
Java 提供了 NIO 的解決方案悟衩,Java 7 也提供了更優(yōu)秀的 NIO.2 支持∷ò荩可以采用Netty或者mina框架來解決NIO數(shù)據(jù)傳輸?shù)膯栴}座泳。開源的RPC框架Dubbo就是采用NIO通信,集成支持netty幕与、mina挑势、grizzly。
-
服務(wù)注冊中心
通過注冊中心啦鸣,讓客戶端連接調(diào)用服務(wù)端所發(fā)布的服務(wù)潮饱。主流的注冊中心組件:Redis、Nacos赏陵、Zookeeper饼齿、Consul 、Etcd蝙搔。Dubbo采用的是ZooKeeper提供服務(wù)注冊與發(fā)現(xiàn)功能。
-
負(fù)載均衡
在高并發(fā)的場景下考传,需要多個(gè)節(jié)點(diǎn)或集群來提升整體吞吐能力吃型。
-
健康檢查
健康檢查包括,客戶端心跳和服務(wù)端主動探測兩種方式僚楞。
2.2 RPC深入解析
2.2.1 序列化技術(shù)
-
序列化的作用
在網(wǎng)絡(luò)傳輸中勤晚,數(shù)據(jù)必須采用二進(jìn)制形式枉层, 所以在RPC調(diào)用過程中, 需要采用序列化技術(shù)赐写,對入?yún)ο蠛头祷刂祵ο筮M(jìn)行序列化與反序列化鸟蜡。
-
序列化原理
自定義的二進(jìn)制協(xié)議來實(shí)現(xiàn)序列化:
file
一個(gè)對象是如何進(jìn)行序列化? 下面以User對象例舉講解:
User對象:
package com.itcast; public class User { /** * 用戶編號 */ private String userNo = "0001"; /** * 用戶名稱 */ private String name = "zhangsan"; }
包體的數(shù)據(jù)組成:
業(yè)務(wù)指令為0x00000001占1個(gè)字節(jié),類的包名com.itcast占10個(gè)字節(jié), 類名User占4個(gè)字節(jié);
屬性UserNo名稱占6個(gè)字節(jié)挺邀,屬性類型string占2個(gè)字節(jié)表示揉忘,屬性值為0001占4個(gè)字節(jié);
屬性name名稱占4個(gè)字節(jié)端铛,屬性類型string占2個(gè)字節(jié)表示泣矛,屬性值為zhangsan占8個(gè)字節(jié);
包體共計(jì)占有1+10+4+6+2+4+4+2+8 = 41字節(jié)禾蚕。
包頭的數(shù)據(jù)組成:
版本號v1.0占4個(gè)字節(jié)您朽,消息包體實(shí)際長度為41占4個(gè)字節(jié)表示,序列號0001占4個(gè)字節(jié)换淆,校驗(yàn)碼32位表示占4個(gè)字節(jié)哗总。
包頭共計(jì)占有4+4+4+4 = 16字節(jié)。
包尾的數(shù)據(jù)組成:
通過回車符標(biāo)記結(jié)束\r\n倍试,占用1個(gè)字節(jié)讯屈。
整個(gè)包的序列化二進(jìn)制字節(jié)流共41+16+1 = 58字節(jié)。這里講解的是整個(gè)序列化的處理思路易猫, 在實(shí)際的序列化處理中還要考慮更多細(xì)節(jié)耻煤,比如說方法和屬性的區(qū)分,方法權(quán)限的標(biāo)記准颓,嵌套類型的處理等等哈蝇。
-
序列化的處理要素
- 解析效率:序列化協(xié)議應(yīng)該首要考慮的因素,像xml/json解析起來比較耗時(shí)攘已,需要解析dom樹炮赦,二進(jìn)制自定義協(xié)議解析起來效率要快很多软能。
- 壓縮率:同樣一個(gè)對象心俗,xml/json傳輸起來有大量的標(biāo)簽冗余信息,信息有效性低母谎,二進(jìn)制自定義協(xié)議占用的空間相對來說會小很多峡眶。
- 擴(kuò)展性與兼容性:是否能夠利于信息的擴(kuò)展,并且增加字段后舊版客戶端是否需要強(qiáng)制升級峭拘,這都是需要考慮的問題鸡挠,在自定義二進(jìn)制協(xié)議時(shí)候,要做好充分考慮設(shè)計(jì)彭沼。
- 可讀性與可調(diào)試性:xml/json的可讀性會比二進(jìn)制協(xié)議好很多备埃,并且通過網(wǎng)絡(luò)抓包是可以直接讀取姓惑,二進(jìn)制則需要反序列化才能查看其內(nèi)容。
- 跨語言:有些序列化協(xié)議是與開發(fā)語言緊密相關(guān)的瓜喇,例如dubbo的Hessian序列化協(xié)議就只能支持Java的RPC調(diào)用挺益。
- 通用性:xml/json非常通用,都有很好的第三方解析庫乘寒,各個(gè)語言解析起來都十分方便望众,二進(jìn)制數(shù)據(jù)的處理方面也有Protobuf和Hessian等插件,在做設(shè)計(jì)的時(shí)候盡量做到較好的通用性伞辛。
-
常用的序列化技術(shù)
-
JDK原生序列化
代碼:
... public static void main(String[] args) throws IOException, ClassNotFoundException { String basePath = "D:/TestCode"; FileOutputStream fos = new FileOutputStream(basePath + "tradeUser.clazz"); TradeUser tradeUser = new TradeUser(); tradeUser.setName("Mirson"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(tradeUser); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream(basePath + "tradeUser.clazz"); ObjectInputStream ois = new ObjectInputStream(fis); TradeUser deStudent = (TradeUser) ois.readObject(); ois.close(); System.out.println(deStudent); } ...
(1) 在Java中烂翰,序列化必須要實(shí)現(xiàn)java.io.Serializable接口。
(2) 通過ObjectOutputStream和ObjectInputStream對象進(jìn)行序列化及反序列化操作蚤氏。
(3) 虛擬機(jī)是否允許反序列化甘耿,不僅取決于類路徑和功能代碼是否一致佳恬,一個(gè)非常重要的一點(diǎn)是兩個(gè)類的序列化 ID 是否一致
(也就是在代碼中定義的序列ID private static final long serialVersionUID)(4) 序列化并不會保存靜態(tài)變量贰剥。
(5) 要想將父類對象也序列化,就需要讓父類也實(shí)現(xiàn)Serializable 接口。
(6) Transient 關(guān)鍵字的作用是控制變量的序列化乖菱,在變量聲明前加上該關(guān)鍵字帆锋,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設(shè)為初始值摄乒,如基本類型 int為 0梨水,封裝對象型Integer則為null。
(7) 服務(wù)器端給客戶端發(fā)送序列化對象數(shù)據(jù)并非加密的,如果對象中有一些敏感數(shù)據(jù)比如密碼等,那么在對密碼字段序列化之前奕短,最好做加密處理, 這樣可以一定程度保證序列化對象的數(shù)據(jù)安全日杈。
-
JSON序列化
一般在HTTP協(xié)議的RPC框架通信中,會選擇JSON方式填硕。
優(yōu)勢:JSON具有較好的擴(kuò)展性姻檀、可讀性和通用性狭莱。
缺陷:JSON序列化占用空間開銷較大,沒有JAVA的強(qiáng)類型區(qū)分,需要通過反射解決,解析效率和壓縮率都較差痕檬。
如果對并發(fā)和性能要求較高唁桩,或者是傳輸數(shù)據(jù)量較大的場景,不建議采用JSON序列化方式碍现。
-
Hessian2序列化
Hessian 是一個(gè)動態(tài)類型躏升,二進(jìn)制序列化佃却,并且支持跨語言特性的序列化框架瘤泪。
Hessian 性能上要比 JDK、JSON 序列化高效很多惶洲,并且生成的字節(jié)數(shù)也更小。有非常好的兼容性和穩(wěn)定性,所以 Hessian 更加適合作為 RPC 框架遠(yuǎn)程通信的序列化協(xié)議。
代碼示例:
... TradeUser tradeUser = new TradeUser(); tradeUser.setName("Mirson"); //tradeUser對象序列化處理 ByteArrayOutputStream bos = new ByteArrayOutputStream(); Hessian2Output output = new Hessian2Output(bos); output.writeObject(tradeUser); output.flushBuffer(); byte[] data = bos.toByteArray(); bos.close(); //tradeUser對象反序列化處理 ByteArrayInputStream bis = new ByteArrayInputStream(data); Hessian2Input input = new Hessian2Input(bis); TradeUser deTradeUser = (TradeUser) input.readObject(); input.close(); System.out.println(deTradeUser); ...
Dubbo Hessian Lite序列化流程:
fileDubbo Hessian Lite反序列化流程:
fileHessian自身也存在一些缺陷耘分,大家在使用過程中要注意:
對Linked系列對象不支持计盒,比如LinkedHashMap拔第、LinkedHashSet 等,但可以通過CollectionSerializer類修復(fù)。
Locale 類不支持,可以通過擴(kuò)展 ContextSerializerFactory 類修復(fù)拷呆。
Byte/Short 在反序列化的時(shí)候會轉(zhuǎn)成 Integer啥供。
-
Dubbo2.7.3通訊序列化源碼實(shí)現(xiàn)分析:
- 序列化實(shí)現(xiàn)流程:
ExchangeCodec的encode方法:
```java
@Override
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
if (msg instanceof Request) {
encodeRequest(channel, buffer, (Request) msg);
} else if (msg instanceof Response) {
encodeResponse(channel, buffer, (Response) msg);
} else {
super.encode(channel, buffer, msg);
}
}
```
- 反序列化實(shí)現(xiàn)流程:
源碼:
ExchangeCodec的decode方法:
```java
@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
int readable = buffer.readableBytes();
byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
buffer.readBytes(header);
return decode(channel, buffer, readable, header);
}
```
ExchangeCodec的decodeBody方法:
```java
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
...
} else {
// decode request.
Request req = new Request(id);
req.setVersion(Version.getProtocolVersion());
req.setTwoWay((flag & FLAG_TWOWAY) != 0);
if ((flag & FLAG_EVENT) != 0) {
req.setEvent(true);
}
try {
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
}
...
}
```
-
Protobuf序列化
Protobuf 是 Google 推出的開源序列庫,它是一種輕便、高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式属划,可以用于結(jié)構(gòu)化數(shù)據(jù)序列化,支持 Java、Python菱农、C++、Go 等多種語言舅巷。
Protobuf 使用的時(shí)候需要定義 IDL(Interface description language)飒房,然后使用不同語言的 IDL 編譯器褥芒,生成序列化工具類,它具備以下優(yōu)點(diǎn):
- 壓縮比高,體積小颜及,序列化后體積相比 JSON、Hessian 小很多爱葵;
- IDL 能清晰地描述語義辆雾,可以幫助并保證應(yīng)用程序之間的類型不會丟失,無需類似 XML 解析器惭墓;
- 序列化反序列化速度很快钧萍,不需要通過反射獲取類型;
- 消息格式的擴(kuò)展、升級和兼容性都不錯(cuò),可以做到向后兼容挖炬。
代碼示例:
Protobuf腳本定義:
// 定義Proto版本 syntax = "proto3"; // 是否允許生成多個(gè)JAVA文件 option java_multiple_files = false; // 生成的包路徑 option java_package = "com.itcast.bulls.stock.struct.netty.trade"; // 生成的JAVA類名 option java_outer_classname = "TradeUserProto"; // 預(yù)警通知消息體 message TradeUser { /** * 用戶ID */ int64 userId = 1 ; /** * 用戶名稱 */ string userName = 2 ; }
代碼操作:
// 創(chuàng)建TradeUser的Protobuf對象 TradeUserProto.TradeUser.Builder builder = TradeUserProto.TradeUser.newBuilder(); builder.setUserId(101); builder.setUserName("Mirson"); //將TradeUser做序列化處理 TradeUserProto.TradeUser msg = builder.build(); byte[] data = msg.toByteArray(); //反序列化處理, 將剛才序列化的byte數(shù)組轉(zhuǎn)化為TradeUser對象 TradeUserProto.TradeUser deTradeUser = TradeUserProto.TradeUser.parseFrom(data); System.out.println(deTradeUser);
2.2.2 動態(tài)代理
-
內(nèi)部接口如何調(diào)用實(shí)現(xiàn)?
RPC的調(diào)用對用戶來講是透明的钓猬,那內(nèi)部是如何實(shí)現(xiàn)呢?內(nèi)部核心技術(shù)采用的就是動態(tài)代理,RPC 會自動給接口生成一個(gè)代理類,當(dāng)我們在項(xiàng)目中注入接口的時(shí)候,運(yùn)行過程中實(shí)際綁定的是這個(gè)接口生成的代理類面褐。在接口方法被調(diào)用的時(shí)候闻蛀,它實(shí)際上是被生成代理類攔截到了薪棒,這樣就可以在生成的代理類里面,加入其他調(diào)用處理邏輯,比如連接負(fù)載管理吨述,日志記錄等等冰啃。
JDK動態(tài)代理:
被代理對象必須實(shí)現(xiàn)1個(gè)接口
-
JDK動態(tài)代理的如何實(shí)現(xiàn)?
實(shí)例代碼:
public class JdkProxyTest { /** * 定義用戶的接口 */ public interface User { String job(); } /** * 實(shí)際的調(diào)用對象 */ public static class Teacher { public String invoke(){ return "i'm Teacher"; } } /** * 創(chuàng)建JDK動態(tài)代理類 */ public static class JDKProxy implements InvocationHandler { private Object target; JDKProxy(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] paramValues) { return ((Teacher)target).invoke(); } } public static void main(String[] args){ // 構(gòu)建代理器 JDKProxy proxy = new JDKProxy(new Teacher()); ClassLoader classLoader = ClassLoaderUtils.getClassLoader(); // 生成代理類 User user = (User) Proxy.newProxyInstance(classLoader, new Class[]{User.class}, proxy); // 接口調(diào)用 System.out.println(user.job()); } }
JDK動態(tài)代理的實(shí)現(xiàn)原理:
JDK內(nèi)部如何處理?
反編譯生成的代理類可以知道汪榔,代理類 $Proxy里面會定義相同簽名的接口(也就是上面代碼User的job接口)士聪,然后內(nèi)部會定義一個(gè)變量綁定JDKProxy代理對象曼库,當(dāng)調(diào)用User.job接口方法,實(shí)質(zhì)上調(diào)用的是JDKProxy.invoke()方法,從而實(shí)現(xiàn)了接口的動態(tài)代理。
-
為什么要加入動態(tài)代理?
第一, 如果沒有動態(tài)代理, 服務(wù)端大量的接口將不便于管理,需要大量的if判斷讥电,如果擴(kuò)展了新的接口纠炮,需要更改調(diào)用邏輯耕肩, 不利于擴(kuò)展維護(hù)梳虽。
第二, 是可以攔截竖螃,添加其他額外功能画拾, 比如連接負(fù)載管理,日志記錄等等。
-
動態(tài)代理開源技術(shù)
(1) Cglib 動態(tài)代理
Cglib是一個(gè)強(qiáng)大的、高性能的代碼生成包汽畴,它廣泛被許多AOP框架使用旧巾,支持方法級別的攔截。它是高級的字節(jié)碼生成庫忍些,位于ASM之上鲁猩,ASM是低級的字節(jié)碼生成工具是尔,ASM的使用對開發(fā)人員要求較高,相比較來講, ASM性能更好。
file(2) Javassist 動態(tài)代理
一個(gè)開源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫。javassist是jboss的一個(gè)子項(xiàng)目漂佩,它直接使用java編碼的形式熟尉,不需要了解虛擬機(jī)指令,可以動態(tài)改變類的結(jié)構(gòu),或者動態(tài)生成類。Javassist 的定位是能夠操縱底層字節(jié)碼典鸡,所以使用起來并不簡單搂蜓,Dubbo 框架的設(shè)計(jì)者為了追求性能花費(fèi)了不少精力去適配javassist伪煤。
(3) Byte Buddy 字節(jié)碼增強(qiáng)庫
Byte Buddy是致力于解決字節(jié)碼操作和 簡化操作復(fù)雜性的開源框架。Byte Buddy 目標(biāo)是將顯式的字節(jié)碼操作隱藏在一個(gè)類型安全的領(lǐng)域特定語言背后。它屬于后起之秀,在很多優(yōu)秀的項(xiàng)目中碘饼,像 Spring隘冲、Jackson 都用到了 Byte Buddy 來完成底層代理辐马。相比 Javassist努释,Byte Buddy 提供了更容易操作的 API炬搭,編寫的代碼可讀性更高丘薛。
幾種動態(tài)代理性能比較:
單位是納秒。大括號內(nèi)代表的是樣本標(biāo)準(zhǔn)差,綜合結(jié)果:
Byte Buddy > CGLIB > Javassist> JDK拦坠。
源碼剖析:
核心源碼:
2.2.3 服務(wù)注冊發(fā)現(xiàn)
-
服務(wù)注冊發(fā)現(xiàn)的作用
在高可用的生產(chǎn)環(huán)境中,一般都以集群方式提供服務(wù)栏赴,集群里面的IP可能隨時(shí)變化竖瘾,也可能會隨著維護(hù)擴(kuò)充或減少節(jié)點(diǎn)沟突,客戶端需要能夠及時(shí)感知服務(wù)端的變化,獲取集群最新服務(wù)節(jié)點(diǎn)的連接信息准浴。
服務(wù)注冊發(fā)現(xiàn)功能
服務(wù)注冊:在服務(wù)提供方啟動的時(shí)候事扭,將對外暴露的接口注冊到注冊中心內(nèi),注冊中心將這個(gè)服務(wù)節(jié)點(diǎn)的 IP 和接口等連接信息保存下來乐横。為了檢測服務(wù)的服務(wù)端的有效狀態(tài)求橄,一般會建立雙向心跳機(jī)制。
服務(wù)訂閱:在服務(wù)調(diào)用方啟動的時(shí)候葡公,客戶端去注冊中心查找并訂閱服務(wù)提供方的 IP罐农,然后緩存到本地,并用于后續(xù)的遠(yuǎn)程調(diào)用催什。如果注冊中心信息發(fā)生變化涵亏, 一般會采用推送的方式做更新。
-
服務(wù)注冊發(fā)現(xiàn)的具體流程
主流服務(wù)注冊工具有Nacos蒲凶、Consul气筋、Zookeeper等,
基于 ZooKeeper 的服務(wù)發(fā)現(xiàn):
ZooKeeper 集群作為注冊中心集群旋圆,服務(wù)注冊的時(shí)候只需要服務(wù)節(jié)點(diǎn)向 ZooKeeper 節(jié)點(diǎn)寫入注冊信息即可宠默,利用 ZooKeeper 的 Watcher 機(jī)制完成服務(wù)訂閱與服務(wù)下發(fā)功能。
A. 先在 ZooKeeper 中創(chuàng)建一個(gè)服務(wù)根路徑灵巧,可以根據(jù)接口名命名(例如:/dubbo/com.itcast.xxService)搀矫,在這個(gè)路徑再創(chuàng)建服務(wù)提供方與調(diào)用方目錄(providers、consumers)刻肄,分別用來存儲服務(wù)提供方和調(diào)用方的節(jié)點(diǎn)信息瓤球。
B. 服務(wù)端發(fā)起注冊時(shí),會在服務(wù)提供方目錄中創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn)敏弃,節(jié)點(diǎn)中存儲注冊信 息卦羡,比如IP,端口麦到,服務(wù)名稱等等虹茶。
C. 客戶端發(fā)起訂閱時(shí),會在服務(wù)調(diào)用方目錄中創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn)隅要,節(jié)點(diǎn)中存儲調(diào)用方的信息蝴罪,同時(shí)watch 服務(wù)提供方的目錄(/dubbo/com.itcast.xxService/providers)中所有的服務(wù)節(jié)點(diǎn)數(shù)據(jù)。當(dāng)服務(wù)端產(chǎn)生變化時(shí)步清,比如下線或宕機(jī)等要门,ZooKeeper 就會通知給訂閱的客戶端虏肾。
ZooKeeper方案的特點(diǎn):
ZooKeeper 的一大特點(diǎn)就是強(qiáng)一致性,ZooKeeper 集群的每個(gè)節(jié)點(diǎn)的數(shù)據(jù)每次發(fā)生更新操作欢搜,都會通知其它 ZooKeeper 節(jié)點(diǎn)同時(shí)執(zhí)行更新封豪。它要求保證每個(gè)節(jié)點(diǎn)的數(shù)據(jù)能夠?qū)崟r(shí)的完全一致,這樣也就會導(dǎo)致ZooKeeper 集群性能上的下降炒瘟,ZK是采用CP模式(保證強(qiáng)一致性)吹埠,如果要注重性能, 可以考慮采用AP模式(保證最終一致)的注冊中心組件疮装, 比如Nacos等缘琅。
-
源碼剖析
Dubbo Spring Cloud 訂閱的源碼(客戶端):
file核心源碼:
RegistryProtocol的doRefer方法:
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) { RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url); directory.setRegistry(registry); directory.setProtocol(protocol); // all attributes of REFER_KEY Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters()); URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters); if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) { directory.setRegisteredConsumerUrl(getRegisteredConsumerUrl(subscribeUrl, url)); registry.register(directory.getRegisteredConsumerUrl()); } directory.buildRouterChain(subscribeUrl); directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY, PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY)); Invoker invoker = cluster.join(directory); ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory); return invoker; }
Dubbo Spring Cloud 注冊發(fā)現(xiàn)的源碼(服務(wù)端):
file核心源碼:
RegistryProtocol的export方法:
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException { // 獲取注冊信息 URL registryUrl = getRegistryUrl(originInvoker); // 獲取服務(wù)提供方信息 URL providerUrl = getProviderUrl(originInvoker); // Subscribe the override data // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call // the same service. Because the subscribed is cached key with the name of the service, it causes the // subscription information to cover. final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl); final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker); overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener); providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener); //export invoker final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl); // 獲取訂閱注冊器 final Registry registry = getRegistry(originInvoker); final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl); ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl); //to judge if we need to delay publish boolean register = registeredProviderUrl.getParameter("register", true); if (register) { // 進(jìn)入服務(wù)端信息注冊處理 register(registryUrl, registeredProviderUrl); providerInvokerWrapper.setReg(true); } // Deprecated! Subscribe to override rules in 2.6.x or before. // 服務(wù)端信息訂閱處理 registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); exporter.setRegisterUrl(registeredProviderUrl); exporter.setSubscribeUrl(overrideSubscribeUrl); //Ensure that a new exporter instance is returned every time export return new DestroyableExporter<>(exporter); }
2.2.4 網(wǎng)絡(luò)IO模型
-
有哪些網(wǎng)絡(luò)IO模型
分為五種:
- 同步阻塞 IO(BIO)
- 同步非阻塞 IO(NIO)
- IO 多路復(fù)用
- 信號驅(qū)動IO
- 異步非阻塞 IO(AIO)
常用的是同步阻塞 IO 和 IO 多路復(fù)用模型。
什么是阻塞IO模型
通常由一個(gè)獨(dú)立的 Acceptor 線程負(fù)責(zé)監(jiān)聽客戶端的連接廓推。一般通過在while(true)
循環(huán)中服務(wù)端會調(diào)用 accept()
方法等待接收客戶端的連接的方式監(jiān)聽請求刷袍,請求一旦接收到一個(gè)連接請求,就可以建立通信套接字,在這個(gè)通信套接字上進(jìn)行讀寫操作樊展,此時(shí)不能再接收其他客戶端連接請求呻纹,直到客戶端的操作執(zhí)行完成。
系統(tǒng)內(nèi)核處理 IO 操作分為兩個(gè)階段——等待數(shù)據(jù)和拷貝數(shù)據(jù)专缠。而在這兩個(gè)階段中雷酪,應(yīng)用進(jìn)程中 IO 操作的線程會一直都處于阻塞狀態(tài),如果是基于 Java 多線程開發(fā)涝婉,那么每一個(gè) IO 操作都要占用線程哥力,直至 IO 操作結(jié)束。
-
IO多路復(fù)用
概念: 服務(wù)端采用單線程過select/epoll機(jī)制嘁圈,獲取fd列表, 遍歷fd中的所有事件, 可以關(guān)注多個(gè)文件描述符蟀淮,使其能夠支持更多的并發(fā)連接最住。
IO多路復(fù)用的實(shí)現(xiàn)主要有select,poll和epoll模式怠惶。
文件描述符:
在Linux系統(tǒng)中一切皆可以看成是文件涨缚,文件又可分為:普通文件、目錄文件策治、鏈接文件和設(shè)備文件脓魏。
文件描述符(file descriptor)是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引,用來指向被打開的文件通惫。文件描述符的值是一個(gè)非負(fù)整數(shù)茂翔。
下圖說明(左邊是進(jìn)程、中間是內(nèi)核履腋、右邊是文件系統(tǒng)):
1) A的文件描述符1和30都指向了同一個(gè)打開的文件句柄珊燎, 代表進(jìn)程多次執(zhí)行打開操作惭嚣。
2) A的文件描述符2和B的文件描述符2都指向文件句柄(#73),代表A和程B可能是父子進(jìn)程或者A和進(jìn)程B打開了同一個(gè)文件(低概率)悔政。
3) (時(shí)間緊張可不講)A的描述符0和B的描述符3分別指向不同的打開文件句柄晚吞,但這些句柄均指向i-node表的相同條目(#1936),這種情況是因?yàn)槊總€(gè)進(jìn)程各自對同一個(gè)文件發(fā)起了打開請求谋国。
程序剛剛啟動的時(shí)候槽地,0是標(biāo)準(zhǔn)輸入,1是標(biāo)準(zhǔn)輸出芦瘾,2是標(biāo)準(zhǔn)錯(cuò)誤捌蚊。如果此時(shí)去打開一個(gè)新的文件,它的文件描述符會是3旅急。
三者的區(qū)別:
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍歷 | 遍歷 | 回調(diào) |
底層實(shí)現(xiàn) | bitmap | 數(shù)組 | 紅黑樹 |
IO效率 | 每次調(diào)用都進(jìn)行線性遍歷逢勾,時(shí)間復(fù)雜度為O(n) | 每次調(diào)用都進(jìn)行線性遍歷,時(shí)間復(fù)雜度為O(n) | 事件通知方式藐吮,每當(dāng)fd就緒溺拱,系統(tǒng)注冊的回調(diào)函數(shù)就會被調(diào)用,將就緒fd放到readyList里面谣辞,時(shí)間復(fù)雜度O(1) |
最大連接數(shù) | 1024(x86)或2048(x64) | 無上限 | 無上限 |
fd拷貝 | 每次調(diào)用select迫摔,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài) | 每次調(diào)用poll,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài) | 調(diào)用epoll_ctl時(shí)拷貝進(jìn)內(nèi)核并保存泥从,之后每次epoll_wait不拷貝 |
select/poll處理流程:
此處是動圖
epoll的處理流程:
此處是動圖
當(dāng)連接有I/O流事件產(chǎn)生的時(shí)候句占,epoll就會去告訴進(jìn)程哪個(gè)連接有I/O流事件產(chǎn)生,然后進(jìn)程就去處理這個(gè)進(jìn)程躯嫉。這樣性能相比要高效很多纱烘!
epoll 可以說是I/O 多路復(fù)用最新的一個(gè)實(shí)現(xiàn),epoll 修復(fù)了poll 和select絕大部分問題, 比如
epoll 是線程安全的祈餐。
epoll 不僅告訴你sock組里面的數(shù)據(jù)擂啥,還會告訴你具體哪個(gè)sock連接有數(shù)據(jù),不用進(jìn)程獨(dú)自輪詢查找帆阳。
-
select 模型
使用示例:
while (1) { // 阻塞獲取 // 每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài) nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout); // 每次需要遍歷所有fd哺壶,判斷有無讀寫事件發(fā)生 for (int i = 0; i <= max && nfds; ++i) { if (i == listenfd) { --nfds; // 這里處理accept事件 FD_SET(i, &read_fd);//將客戶端socket加入到集合中 } if (FD_ISSET(i, &read_fd)) { --nfds; // 這里處理read事件 } if (FD_ISSET(i, &write_fd)) { --nfds; // 這里處理write事件 } } }
缺點(diǎn):
- 單個(gè)進(jìn)程所打開的FD最大數(shù)限制為1024。
- 每次調(diào)用select蜒谤,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài)尚粘,fd數(shù)據(jù)較大時(shí)影響性能嫉称。
- 對socket掃描時(shí)是線性掃描搔预,效率較低(高并發(fā)場景)
-
POLL模型
int max = 0; // 隊(duì)列的實(shí)際長度 while (1) { // 阻塞獲取 // 每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài) nfds = poll(fds, max+1, timeout); if (fds[0].revents & POLLRDNORM) { // 這里處理accept事件 connfd = accept(listenfd); //將新的描述符添加到讀描述符集合中 } // 每次需要遍歷所有fd首昔,判斷有無讀寫事件發(fā)生 for (int i = 1; i < max; ++i) { if (fds[i].revents & POLLRDNORM) { sockfd = fds[i].fd if ((n = read(sockfd, buf, MAXLINE)) <= 0) { // 這里處理read事件 if (n == 0) { close(sockfd); fds[i].fd = -1; } } else { // 這里處理write事件 } if (--nfds <= 0) { break; } } } }
缺點(diǎn):
- poll與select相比,只是沒有fd的限制阶祭,都存在相同的缺陷台妆。
-
EPOLL模型
使用示例:
// 需要監(jiān)聽的socket放到ep中 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); while(1) { // 阻塞獲取 nfds = epoll_wait(epfd,events,20,0); for(i=0;i<nfds;++i) { if(events[i].data.fd==listenfd) { // 這里處理accept事件 connfd = accept(listenfd); // 接收新連接寫到內(nèi)核對象中 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); } else if (events[i].events&EPOLLIN) { // 這里處理read事件 read(sockfd, BUF, MAXLINE); //讀完后準(zhǔn)備寫 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); } else if(events[i].events&EPOLLOUT) { // 這里處理write事件 write(sockfd, BUF, n); //寫完后準(zhǔn)備讀 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); } } }
缺點(diǎn):
- 目前只能工作在linux環(huán)境下
- 數(shù)據(jù)量很小的時(shí)候沒有性能優(yōu)勢
epoll下的兩種模式(拓展了解):
EPOLLLT和EPOLLET兩種觸發(fā)模式翎猛,LT是默認(rèn)的模式,ET是“高速”模式接剩。
LT(水平觸發(fā))模式下切厘,只要這個(gè)fd還有數(shù)據(jù)可讀,每次 epoll_wait都會返回它的事件懊缺,提醒用戶程序去操作
ET(邊緣觸發(fā))模式下疫稿,它只會提示一次,直到下次再有數(shù)據(jù)流入之前都不會再提示了鹃两,無論fd中是否還有數(shù)據(jù)可讀遗座。所以在ET模式下,read一個(gè)fd的時(shí)候一定要把它的buffer讀完俊扳,或者遇到EAGAIN錯(cuò)誤
-
三種模式對比
對比項(xiàng) select poll epoll 數(shù)據(jù)結(jié)構(gòu) bitmap 數(shù)組 紅黑樹 最大連接數(shù) 1024 無上限 無上限 fd拷貝 每次調(diào)用select拷貝 每次調(diào)用poll拷貝 fd首次調(diào)用epoll_ctl拷貝途蒋,每次調(diào)用epoll_wait不拷貝 工作效率 輪詢:O(n) 輪詢:O(n) 回調(diào):O(1) -
為什么阻塞 IO 和 IO 多路復(fù)用最為常用?
在實(shí)際的網(wǎng)絡(luò) IO 的應(yīng)用中馋记,需要的是系統(tǒng)內(nèi)核的支持以及編程語言的支持『牌拢現(xiàn)在大多數(shù)系統(tǒng)內(nèi)核都會支持阻塞 IO、非阻塞 IO 和 IO 多路復(fù)用梯醒,但像信號驅(qū)動 IO宽堆、異步 IO,只有高版本的 Linux 系統(tǒng)內(nèi)核才會支持茸习。
同步阻塞IO畜隶、同步非阻塞IO、同步IO多路復(fù)用與異步IO區(qū)別:
- 同步阻塞IO(實(shí)質(zhì)上号胚, 每個(gè)請求不管成功或失敗籽慢, 都會阻塞)
file- 同步非阻塞IO(相比第一種的完全阻塞,如果數(shù)據(jù)沒準(zhǔn)備好猫胁,會返回EWOULDBLOCK箱亿, 這樣就不會造成阻塞)
file-
同步IO多路復(fù)用
(kernel會根據(jù)select/epoll等機(jī)制, 監(jiān)聽所有select接入的socket杜漠,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了极景,select就會返回察净, 使得一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符驾茴。)
- 異步IO
-
RPC 框架采用哪種網(wǎng)絡(luò) IO 模型?
- IO 多路復(fù)用應(yīng)用特點(diǎn):
IO 多路復(fù)用更適合高并發(fā)的場景氢卡,可以用較少的進(jìn)程(線程)處理較多的 socket 的 IO 請求锈至,但使用難度比較高。
- 阻塞 IO應(yīng)用特點(diǎn):
與 IO 多路復(fù)用相比译秦,阻塞 IO 每處理一個(gè) socket 的 IO 請求都會阻塞進(jìn)程(線程)峡捡,但使用難度較低击碗。在并發(fā)量較低、業(yè)務(wù)邏輯只需要同步進(jìn)行 IO 操作的場景下们拙,阻塞 IO 已經(jīng)滿足了需求稍途,并且不需要發(fā)起 大量的select 調(diào)用,開銷上要比 IO 多路復(fù)用低砚婆。
- RPC框架應(yīng)用:
RPC 調(diào)用在大多數(shù)的情況下械拍,是一個(gè)高并發(fā)調(diào)用的場景, 在 RPC 框架的實(shí)現(xiàn)中装盯,一般會選擇 IO 多路復(fù)用的方式坷虑。在開發(fā)語言的網(wǎng)絡(luò)通信框架的選型上,我們最優(yōu)的選擇是基于 Reactor 模式實(shí)現(xiàn)的框架埂奈,如 Java 語言迄损,首選的框架便是 Netty 框架(目前 Netty 是應(yīng)用最為廣泛的框架),并且在 Linux 環(huán)境下账磺,也要開啟 epoll 來提升系統(tǒng)性能(Windows 環(huán)境下是無法開啟 epoll 的芹敌,因?yàn)橄到y(tǒng)內(nèi)核不支持)。
2.2.5 時(shí)間輪
-
為什么需要時(shí)間輪绑谣?
在Dubbo中党窜,為增強(qiáng)系統(tǒng)的容錯(cuò)能力,會有相應(yīng)的監(jiān)聽判斷處理機(jī)制借宵。比如RPC調(diào)用的超時(shí)機(jī)制的實(shí)現(xiàn)幌衣,消費(fèi)者判斷RPC調(diào)用是否超時(shí),如果超時(shí)會將超時(shí)結(jié)果返回給應(yīng)用層壤玫。在Dubbo最開始的實(shí)現(xiàn)中豁护,是將所有的返回結(jié)果(DefaultFuture)都放入一個(gè)集合中,并且通過一個(gè)定時(shí)任務(wù)欲间,每隔一定時(shí)間間隔就掃描所有的future楚里,逐個(gè)判斷是否超時(shí)。
這樣的實(shí)現(xiàn)方式雖然比較簡單猎贴,但是存在一個(gè)問題就是會有很多無意義的遍歷操作開銷班缎。比如一個(gè)RPC調(diào)用的超時(shí)時(shí)間是10秒,而設(shè)置的超時(shí)判定的定時(shí)任務(wù)是2秒執(zhí)行一次她渴,那么可能會有4次左右無意義的循環(huán)檢測判斷操作达址。
為了解決上述場景中的類似問題,Dubbo借鑒Netty趁耗,引入了時(shí)間輪算法沉唠,減少無意義的輪詢判斷操作。
-
時(shí)間輪原理
對于以上問題苛败, 目的是要減少額外的掃描操作就可以了满葛。比如說一個(gè)定時(shí)任務(wù)是在5 秒之后執(zhí)行径簿,那么在 4.9 秒之后才掃描這個(gè)定時(shí)任務(wù),這樣就可以極大減少 CPU開銷嘀韧。這時(shí)我們就可以利用時(shí)鐘輪的機(jī)制了篇亭。
時(shí)鐘輪的實(shí)質(zhì)上是參考了生活中的時(shí)鐘跳動的原理,那么具體是如何實(shí)現(xiàn)呢锄贷?
在時(shí)鐘輪機(jī)制中暗赶,有時(shí)間槽和時(shí)鐘輪的概念,時(shí)間槽就相當(dāng)于時(shí)鐘的刻度肃叶;而時(shí)鐘輪就相當(dāng)于指針跳動的一個(gè)周期蹂随,我們可以將每個(gè)任務(wù)放到對應(yīng)的時(shí)間槽位上。
如果時(shí)鐘輪有 10 個(gè)槽位因惭,而時(shí)鐘輪一輪的周期是 10 秒岳锁,那么我們每個(gè)槽位的單位時(shí)間就是 1 秒,而下一層時(shí)間輪的周期就是 100 秒蹦魔,每個(gè)槽位的單位時(shí)間也就是 10 秒激率,這就好比秒針與分針, 在秒針周期下勿决, 刻度單位為秒乒躺, 在分針周期下, 刻度為分低缩。
假設(shè)現(xiàn)在我們有 3 個(gè)任務(wù)嘉冒,分別是任務(wù) A(0.9秒之后執(zhí)行)、任務(wù) B(2.1秒后執(zhí)行)與任務(wù) C(12.1秒之后執(zhí)行)咆繁,我們將這 3 個(gè)任務(wù)添加到時(shí)鐘輪中讳推,任務(wù) A 被放到第 0 槽位,任務(wù) B 被放到第 2槽位玩般,任務(wù) C 被放到下一層時(shí)間輪的第2個(gè)槽位银觅,如下圖所示:
通過這個(gè)場景我們可以了解到,時(shí)鐘輪的掃描周期仍是最小單位1秒坏为,但是放置其中的任務(wù)并沒有反復(fù)掃描究驴,每個(gè)任務(wù)會按要求只掃描執(zhí)行一次, 這樣就能夠很好的解決CPU 浪費(fèi)的問題匀伏。
疊加時(shí)鐘輪洒忧, 無限增長, 效率會不斷下降帘撰,該如何解決跑慕?設(shè)定三個(gè)時(shí)鐘輪万皿, 小時(shí)輪摧找, 分鐘輪核行, 秒級輪
Dubbo中的時(shí)間輪原理是如何實(shí)現(xiàn)?
主要是通過Timer蹬耘,Timeout芝雪,TimerTask幾個(gè)接口定義了一個(gè)定時(shí)器的模型,再通過HashedWheelTimer這個(gè)類實(shí)現(xiàn)了一個(gè)時(shí)間輪定時(shí)器(默認(rèn)的時(shí)間槽的數(shù)量是512综苔,可以自定義這個(gè)值)惩系。它對外提供了簡單易用的接口,只需要調(diào)用newTimeout接口如筛,就可以實(shí)現(xiàn)對只需執(zhí)行一次任務(wù)的調(diào)度堡牡。通過該定時(shí)器,Dubbo在響應(yīng)的場景中實(shí)現(xiàn)了高效的任務(wù)調(diào)度杨刨。
-
Dubbo源碼剖析
時(shí)間輪核心類HashedWheelTimer結(jié)構(gòu):
-
時(shí)間輪在RPC的應(yīng)用
-
調(diào)用超時(shí)與重試處理: 上面所講的客戶端調(diào)用超時(shí)的處理晤柄,就可以應(yīng)用到時(shí)鐘輪,我們每發(fā)一次請求妖胀,都創(chuàng)建一個(gè)處理請求超時(shí)的定時(shí)任務(wù)放到時(shí)鐘輪里芥颈,在高并發(fā)、高訪問量的情況下赚抡,時(shí)鐘輪每次只輪詢一個(gè)時(shí)間槽位中的任務(wù)爬坑,這樣會節(jié)省大量的 CPU。
源碼:
FailbackRegistry涂臣, 代碼片段:
// 構(gòu)造方法 public FailbackRegistry(URL url) { super(url); this.retryPeriod = url.getParameter(REGISTRY_RETRY_PERIOD_KEY, DEFAULT_REGISTRY_RETRY_PERIOD); // since the retry task will not be very much. 128 ticks is enough. // 重試器的時(shí)間槽數(shù)量盾计, 設(shè)定為128 retryTimer = new HashedWheelTimer(new NamedThreadFactory("DubboRegistryRetryTimer", true), retryPeriod, TimeUnit.MILLISECONDS, 128); } // 失敗時(shí)間任務(wù)注冊器 private void addFailedRegistered(URL url) { FailedRegisteredTask oldOne = failedRegistered.get(url); if (oldOne != null) { return; } FailedRegisteredTask newTask = new FailedRegisteredTask(url, this); oldOne = failedRegistered.putIfAbsent(url, newTask); if (oldOne == null) { // never has a retry task. then start a new task for retry. // 舊任務(wù)不存在, 則放置時(shí)間輪赁遗,開啟新一個(gè)任務(wù) retryTimer.newTimeout(newTask, retryPeriod, TimeUnit.MILLISECONDS); } }
-
-
定時(shí)心跳檢測: RPC 框架調(diào)用端定時(shí)向服務(wù)端發(fā)送的心跳檢測闯估,來維護(hù)連接狀態(tài),我們可以將心跳的邏輯封裝為一個(gè)心跳任務(wù)吼和,放到時(shí)鐘輪里涨薪。心跳是要定時(shí)重復(fù)執(zhí)行的,而時(shí)鐘輪中的任務(wù)執(zhí)行一遍就被移除了炫乓,對于這種需要重復(fù)執(zhí)行的定時(shí)任務(wù)我們該如何處理呢刚夺?我們在定時(shí)任務(wù)邏輯結(jié)束的最后,再加上一段邏輯末捣, 重設(shè)這個(gè)任務(wù)的執(zhí)行時(shí)間侠姑,把它重新丟回到時(shí)鐘輪里。這樣就可以實(shí)現(xiàn)循環(huán)執(zhí)行箩做。
源碼:
HeaderExchangeServer代碼片段:
... // 建立心跳時(shí)間輪莽红, 槽位數(shù)默認(rèn)為128 private static final HashedWheelTimer IDLE_CHECK_TIMER = new HashedWheelTimer(new NamedThreadFactory("dubbo-server-idleCheck", true), 1, TimeUnit.SECONDS, TICKS_PER_WHEEL); ... // 啟動心跳任務(wù)檢測 private void startIdleCheckTask(URL url) { if (!server.canHandleIdle()) { AbstractTimerTask.ChannelProvider cp = () -> unmodifiableCollection(HeaderExchangeServer.this.getChannels()); int idleTimeout = getIdleTimeout(url); long idleTimeoutTick = calculateLeastDuration(idleTimeout); CloseTimerTask closeTimerTask = new CloseTimerTask(cp, idleTimeoutTick, idleTimeout); this.closeTimerTask = closeTimerTask; // init task and start timer. // 開啟心跳檢測任務(wù) IDLE_CHECK_TIMER.newTimeout(closeTimerTask, idleTimeoutTick, TimeUnit.MILLISECONDS); } } ...
連接檢測, 會不斷執(zhí)行, 加入時(shí)間輪中安吁。
AbstractTimerTask源碼:
@Override public void run(Timeout timeout) throws Exception { Collection<Channel> c = channelProvider.getChannels(); for (Channel channel : c) { if (channel.isClosed()) { continue; } // 調(diào)用心跳檢測任務(wù) doTask(channel); } // 重新放入時(shí)間輪中 reput(timeout, tick); }
還可以參考HeartbeatTimerTask醉蚁、ReconnectTimerTask源碼實(shí)現(xiàn)。
3. RPC的高級機(jī)制
3.1 異步處理機(jī)制
-
為什么要采用異步鬼店?
如果采用同步調(diào)用网棍, CPU 大部分的時(shí)間都在等待而沒有去計(jì)算,從而導(dǎo)致 CPU 的利用率不夠妇智。
RPC 請求比較耗時(shí)的原因主要是在哪里滥玷?
在大多數(shù)情況下,RPC 本身處理請求的效率是在毫秒級的巍棱。RPC 請求的耗時(shí)大部分都是業(yè)務(wù)耗時(shí)惑畴,比如業(yè)務(wù)邏輯中有訪問數(shù)據(jù)庫執(zhí)行慢 SQL 的操作,核心是在I/O瓶頸航徙。所以說桨菜,在大多數(shù)情況下,影響到 RPC 調(diào)用的吞吐量的原因也就是業(yè)務(wù)邏輯處理慢了捉偏,CPU 大部分時(shí)間都在等待資源倒得。
-
調(diào)用端如何實(shí)現(xiàn)異步?
常用的方式就是Future 方式夭禽,它是返回 Future 對象霞掺,通過GET方式獲取結(jié)果;或者采用入?yún)?Callback 對象的回調(diào)方式讹躯,處理結(jié)果菩彬。
從DUBBO框架, 來看具體是如何實(shí)現(xiàn)異步調(diào)用潮梯?
-
服務(wù)端如何實(shí)現(xiàn)異步骗灶?
為了提升性能,連接請求與業(yè)務(wù)處理不會放在一個(gè)線程處理秉馏, 這個(gè)就是服務(wù)端的異步化耙旦。服務(wù)端業(yè)務(wù)處理邏輯加入異步處理機(jī)制。
在RPC 框架提供一種回調(diào)方式萝究,讓業(yè)務(wù)邏輯可以異步處理免都,處理完之后調(diào)用 RPC 框架的回調(diào)接口。
RPC 框架的異步策略主要是調(diào)用端異步與服務(wù)端異步帆竹。調(diào)用端的異步就是通過 Future 方式绕娘。
服務(wù)端異步則需要一種回調(diào)方式,讓業(yè)務(wù)邏輯可以異步處理栽连。這樣就實(shí)現(xiàn)了RPC調(diào)用的全異步化险领。
-
RPC框架的異步實(shí)現(xiàn)
RPC 框架的異步策略主要是調(diào)用端異步與服務(wù)端異步。調(diào)用端的異步就是通過 Future 方式實(shí)現(xiàn)異步,調(diào)用端發(fā)起一次異步請求并且從請求上下文中拿到一個(gè) Future绢陌,之后通過 Future 的 get 方法獲取結(jié)果挨下,如果業(yè)務(wù)邏輯中同時(shí)調(diào)用多個(gè)其它的服務(wù),則可以通過 Future 的方式減少業(yè)務(wù)邏輯的耗時(shí)下面,提升吞吐量。
服務(wù)端異步則需要一種回調(diào)方式绩聘,讓業(yè)務(wù)邏輯可以異步處理沥割,之后調(diào)用 RPC 框架提供的回調(diào)接口,將最終結(jié)果異步通知給調(diào)用端凿菩。這樣就實(shí)現(xiàn)了RPC調(diào)用的全異步机杜。
Dubbo源碼:
異步調(diào)用: AsyncToSyncInvoker.invoke方法
獲取結(jié)果:ChannelWrappedInvoker.doInvoke方法
3.2 路由與負(fù)載均衡(了解)
我們后面會講解灰度發(fā)布機(jī)制,基于Nginx+Lua衅谷、擴(kuò)展SpringCloud Gateway源碼灰度發(fā)布和負(fù)載均衡椒拗,只要項(xiàng)目集群、分布式應(yīng)用就會涉及到路由與負(fù)載均衡获黔。
-
為什么要采用路由蚀苛?
真實(shí)的環(huán)境中一般是以集群的方式提供服務(wù),對于服務(wù)調(diào)用方來說玷氏,一個(gè)接口會有多個(gè)服務(wù)提供方同時(shí)提供服務(wù)堵未,所以 RPC 在每次發(fā)起請求的時(shí)候,都需要從多個(gè)服務(wù)節(jié)點(diǎn)里面選取一個(gè)用于處理請求的服務(wù)節(jié)點(diǎn)盏触。
這就需要在RPC應(yīng)用中增加路由功能渗蟹。
-
如何實(shí)現(xiàn)路由?
服務(wù)注冊發(fā)現(xiàn)方式:
通過服務(wù)發(fā)現(xiàn)的方式從邏輯上看是可行赞辩,但注冊中心是用來保證數(shù)據(jù)的一致性雌芽。通過服務(wù)發(fā)現(xiàn)方式來實(shí)現(xiàn)請求隔離并不理想。
RPC路由策略:
從服務(wù)提供方節(jié)點(diǎn)集合里面選擇一個(gè)合適的節(jié)點(diǎn)(負(fù)載均衡)辨嗽,把符合我們要求的節(jié)點(diǎn)篩選出來世落。這個(gè)就是路由策略:
接收請求-->請求校驗(yàn)-->路由策略-->負(fù)載均衡-->
使用了 IP 路由策略后,整個(gè)集群的調(diào)用拓?fù)淙缦聢D所示:
有些場景下糟需,可能還需要更細(xì)粒度的路由方式岛心,比如說根據(jù)SESSIONID要落到相同的服務(wù)節(jié)點(diǎn)上以保持會話的有效性;
可以考慮采用參數(shù)化路由:
-
RPC框架中的負(fù)載均衡
RPC 的負(fù)載均衡是由 RPC 框架自身提供實(shí)現(xiàn),自主選擇一個(gè)最佳的服務(wù)節(jié)點(diǎn)篮灼,發(fā)起 RPC 調(diào)用請求忘古。
RPC 負(fù)載均衡策略一般包括輪詢、隨機(jī)诅诱、權(quán)重髓堪、最少連接等。Dubbo默認(rèn)就是使用隨機(jī)負(fù)載均衡策略。
-
自適應(yīng)的負(fù)載均衡策略
RPC 的負(fù)載均衡完全由 RPC 框架自身實(shí)現(xiàn)干旁,服務(wù)調(diào)用方發(fā)起請求時(shí)驶沼,會通過所配置的負(fù)載均衡組件,自主地選擇合適服務(wù)節(jié)點(diǎn)争群。調(diào)用方如果能知道每個(gè)服務(wù)節(jié)點(diǎn)處理請求的能力回怜,再根據(jù)服務(wù)節(jié)點(diǎn)處理請求的能力來判斷分配相應(yīng)的流量,集群資源就能夠得到充分的利用换薄, 當(dāng)一個(gè)服務(wù)節(jié)點(diǎn)負(fù)載過高或響應(yīng)過慢時(shí)玉雾,就少給它發(fā)送請求,反之則多給它發(fā)送請求轻要。這個(gè)就是自適應(yīng)的負(fù)載均衡策略复旬。
具體如何實(shí)現(xiàn)?
這就需要判定服務(wù)節(jié)點(diǎn)的處理能力冲泥。
file主要步驟:
(1)添加計(jì)分器和指標(biāo)采集器驹碍。
(2)指標(biāo)采集器收集服務(wù)節(jié)點(diǎn) CPU 核數(shù)、CPU 負(fù)載以及內(nèi)存占用率等指標(biāo)凡恍。
(3)可以配置開啟哪些指標(biāo)采集器志秃,并設(shè)置這些參考指標(biāo)的具體權(quán)重。
(4)通過對服務(wù)節(jié)點(diǎn)的綜合打分嚼酝,最終計(jì)算出服務(wù)節(jié)點(diǎn)的實(shí)際權(quán)重洽损,選擇合適的服務(wù)節(jié)點(diǎn)。
3.3 熔斷限流(了解)
我們后面課程會詳細(xì)講解熔斷限流組件Sentinel高級用法革半、源碼剖析碑定、策略機(jī)制,但是RPC需要考慮熔斷限流機(jī)制又官,我們一起來了解一下延刘。
-
為什么要進(jìn)行限流?
在實(shí)際生產(chǎn)環(huán)境中六敬,每個(gè)服務(wù)節(jié)點(diǎn)都可能由于訪問量過大而引起一系列問題碘赖,就需要業(yè)務(wù)提供方能夠進(jìn)行自我保護(hù),從而保證在高訪問量外构、高并發(fā)的場景下普泡,系統(tǒng)依然能夠穩(wěn)定,高效運(yùn)行审编。
-
服務(wù)端的自我保護(hù)實(shí)現(xiàn)
file
在Dubbo框架中撼班, 可以通過Sentinel來實(shí)現(xiàn)更為完善的熔斷限流功能,服務(wù)端是具體如何實(shí)現(xiàn)限流邏輯的垒酬?
方法有很多種砰嘁, 最簡單的是計(jì)數(shù)器件炉,還有平滑限流的滑動窗口、漏斗算法以及令牌桶算法等等矮湘。Sentinel采用的是滑動窗口來實(shí)現(xiàn)的限流斟冕。
windowStart: 時(shí)間窗口的開始時(shí)間,單位是毫秒
windowLength: 時(shí)間窗口的長度缅阳,單位是毫秒
value: 時(shí)間窗口的內(nèi)容
初始的時(shí)候arrays數(shù)組中只有一個(gè)窗口磕蛇,每個(gè)時(shí)間窗口的長度是500ms,這就意味著只要當(dāng)前時(shí)間與時(shí)間窗口的差值在500ms之內(nèi)十办,時(shí)間窗口就不會向前滑動秀撇。
時(shí)間繼續(xù)往前走,當(dāng)超過500ms時(shí)橘洞,時(shí)間窗口就會向前滑動到下一個(gè)捌袜,這時(shí)就會更新當(dāng)前窗口的開始時(shí)間:
在當(dāng)前時(shí)間點(diǎn)中進(jìn)入的請求说搅,會被統(tǒng)計(jì)到當(dāng)前時(shí)間所對應(yīng)的時(shí)間窗口中炸枣。
-
調(diào)用方的自我保護(hù)
一個(gè)服務(wù) A 調(diào)用服務(wù) B 時(shí),服務(wù) B 的業(yè)務(wù)邏輯又調(diào)用了服務(wù) C弄唧,這時(shí)服務(wù) C 響應(yīng)超時(shí)适肠,服務(wù) B 就可能會因?yàn)槎逊e大量請求而導(dǎo)致服務(wù)宕機(jī)映企,由此產(chǎn)生服務(wù)雪崩的問題计呈。
熔斷處理流程:
熔斷機(jī)制:
熔斷器的工作機(jī)制主要是關(guān)閉渴语、打開和半打開這三個(gè)狀態(tài)之間的切換锭碳。
Sentinel 熔斷降級組件它可以支持以下降級策略:
-
平均響應(yīng)時(shí)間 (
DEGRADE_GRADE_RT
):當(dāng) 1s 內(nèi)持續(xù)進(jìn)入 N 個(gè)請求灶挟,對應(yīng)時(shí)刻的平均響應(yīng)時(shí)間(秒級)均超過閾值(count
熊尉,以 ms 為單位)季稳,那么在接下的時(shí)間窗口(DegradeRule
中的timeWindow
馋嗜,以 s 為單位)之內(nèi)麸俘,對這個(gè)方法的調(diào)用都會自動地熔斷(拋出DegradeException
)辩稽。注意 Sentinel 默認(rèn)統(tǒng)計(jì)的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms从媚,若需要變更此上限可以通過啟動配置項(xiàng)-Dcsp.sentinel.statistic.max.rt=xxx
來配置逞泄。 -
異常比例 (
DEGRADE_GRADE_EXCEPTION_RATIO
):當(dāng)資源的每秒請求量 >= N(可配置),并且每秒異嘲菪В總數(shù)占通過量的比值超過閾值(DegradeRule
中的count
)之后喷众,資源進(jìn)入降級狀態(tài),即在接下的時(shí)間窗口(DegradeRule
中的timeWindow
紧憾,以 s 為單位)之內(nèi)到千,對這個(gè)方法的調(diào)用都會自動地返回。異常比率的閾值范圍是[0.0, 1.0]
赴穗,代表 0% - 100%父阻。 -
異常數(shù) (
DEGRADE_GRADE_EXCEPTION_COUNT
):當(dāng)資源近 1 分鐘的異常數(shù)目超過閾值之后會進(jìn)行熔斷愈涩。注意由于統(tǒng)計(jì)時(shí)間窗口是分鐘級別的,若timeWindow
小于 60s加矛,則結(jié)束熔斷狀態(tài)后仍可能再進(jìn)入熔斷狀態(tài)履婉。
更多資料,參考Sentinel官方文檔斟览。
本文由育博學(xué)谷狂野架構(gòu)師發(fā)布
如果本文對您有幫助毁腿,歡迎關(guān)注和點(diǎn)贊;如果您有任何建議也可留言評論或私信苛茂,您的支持是我堅(jiān)持創(chuàng)作的動力
轉(zhuǎn)載請注明出處已烤!