rpc是什么意思?RPC涉及的相關(guān)技術(shù)詳解
什么是RPC。
RPC 全稱 Remote Procedure Call——遠(yuǎn)程過程調(diào)用。
在學(xué)校學(xué)編程,我們寫一個(gè)函數(shù)都是在本地調(diào)用就行了。
但是在互聯(lián)網(wǎng)公司,服務(wù)都是部署在不同服務(wù)器上的分布式系統(tǒng),服務(wù)之間如何調(diào)用呢? 答案就是RPC。
RPC設(shè)計(jì)的技術(shù)很多,下圖列出的是關(guān)于RPC涉及的相關(guān)技術(shù)。
添加圖片注釋,不超過 140 字(可選)。
這里給大家一個(gè)非常好用的java面試題大全:。
java面試大全:http://kdocs.cn/l/cfoqiIIJ4xmW。
RPC技術(shù)簡單說就是為了解決遠(yuǎn)程調(diào)用服務(wù)的一種技術(shù),使得調(diào)用者像調(diào)用本地服務(wù)一樣方便透明。
下圖是客戶端調(diào)用遠(yuǎn)端服務(wù)的過程:。
添加圖片注釋,不超過 140 字(可選)。
1、客戶端client發(fā)起服務(wù)調(diào)用請求。
2、client stub 可以理解成一個(gè)代理,會(huì)將調(diào)用方法、參數(shù)按照一定格式進(jìn)行封裝,通過服務(wù)提供的地址,發(fā)起網(wǎng)絡(luò)請求。
3、消息通過網(wǎng)絡(luò)傳輸?shù)椒?wù)端。
4、server stub接受來自socket的消息5、server stub將消息進(jìn)行解包、告訴服務(wù)端調(diào)用的哪個(gè)服務(wù),參數(shù)是什么6、結(jié)果返回給server stub。
7、sever stub把結(jié)果進(jìn)行打包交給socket8、socket通過網(wǎng)絡(luò)傳輸消息9、client slub 從socket拿到消息。
10、client stub解包消息將結(jié)果返回給client。
一個(gè)RPC框架就是把步驟2到9都封裝起來。
為什么需要RPC。
1、首先要明確一點(diǎn):RPC可以用HTTP協(xié)議實(shí)現(xiàn),并且用HTTP是建立在 TCP 之上最廣泛使用的 RPC,但是互聯(lián)網(wǎng)公司往往用自己的私有協(xié)議,比如鵝廠的JCE協(xié)議,私有協(xié)議不具備通用性為什么還要用呢?因?yàn)橄啾扔贖TTP協(xié)議,RPC采用二進(jìn)制字節(jié)碼傳輸,更加高效也更加安全。
2、現(xiàn)在業(yè)界提倡“微服務(wù)“的概念,而服務(wù)之間通信目前有兩種方式,RPC就是其中一種。
RPC可以保證不同服務(wù)之間的互相調(diào)用。
即使是跨語言跨平臺(tái)也不是問題,讓構(gòu)建分布式系統(tǒng)更加容易。
3、RPC框架都會(huì)有服務(wù)降級、流量控制的功能,保證服務(wù)的高可用。
一個(gè)簡單例子。
下面就舉一個(gè)。
1+1=2。
的遠(yuǎn)程調(diào)用的例子。
客戶端發(fā)送兩個(gè)參數(shù),服務(wù)端返回兩個(gè)數(shù)字的相加結(jié)果。
RpcConsumer。
類調(diào)用。
CalculateService。
中的。
Calculate。
方法, 首先通過。
RpcFramework。
中的。
call。
方法,注冊自己想要調(diào)用那個(gè)服務(wù),返回代理,然后就像本地調(diào)用一樣去調(diào)用。
Calculate。
方法,計(jì)算。
People。
,““People“`有兩個(gè)屬性都被賦值成1,返回這兩個(gè)屬性相加后的結(jié)果。
public class RpcConsumer {public static void main(String args[]) { CalculateService service=RpcFramework.call(CalculateService.class,”127.0.0.1″,8888); People people=new People(1,1); String hello=service.Calculate(people); System.out.println(hello); }}生成動(dòng)態(tài)代理的代碼如下。
客戶端在調(diào)用方法前會(huì)先執(zhí)行。
invoke。
方法,建立socket連接,把方法名和參數(shù)傳遞給服務(wù)端,然后獲取返回結(jié)果。
//動(dòng)態(tài)代理機(jī)制。
public static <T> T call(final Class<?> interfaceClass,String host,int port){ if(interfaceClass==null){ throw new IllegalArgumentException(“調(diào)用服務(wù)為空”); } if(host==null||host.length()==0){ throw new IllegalArgumentException(“主機(jī)不能為null”); } if(port<=0||port>65535){ throw new IllegalArgumentException(“端口不合法”+port); } return (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(),new Class<?>[]{interfaceClass},new CallerHandler(host,port)); } static class CallerHandler implements InvocationHandler { private String host; private int port; public CallerHandler(String host, int port) { this.host = host; this.port = port; SERVER = new InetSocketAddress(host, port); } public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable { Socket socket = new Socket(host, port); try { ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream()); try { output.writeUTF(method.getName()); output.writeObject(method.getParameterTypes()); output.writeObject(arguments); ObjectInputStream input = new ObjectInputStream(socket.getInputStream()); try { Object result = input.readObject(); if (result instanceof Throwable) { throw (Throwable) result; } return result; } finally { input.close(); } } finally { output.close(); } } finally { socket.close(); } } }。
RpcProvider。
類實(shí)現(xiàn)具體的。
Calculate。
方法。
通過。
RpcFramework。
中的。
publish。
方法,發(fā)布自己的服務(wù)。
public class RpcProvider{ public static void main(String[] args) throws Exception { CalculateService service =new CalculateServiceImpl(); RpcFramework.publish(service,8888); }}interface CalculateService{ String Calculate(People p);}class CalculateServiceImpl implements CalculateService{ public String Calculate(People people){ int res=people.getA()+people.getB(); return “計(jì)算結(jié)果 “+res; }}發(fā)布服務(wù)的代碼如下。
服務(wù)端循環(huán)監(jiān)聽某個(gè)端口,采用java原生的序列化方法,讀取客戶端需要調(diào)用的方法和參數(shù),執(zhí)行該方法并將結(jié)果返回。
public static void publish(final Object service,int port) throws IOException { if(service==null) throw new IllegalArgumentException(“發(fā)布服務(wù)不能是空”); if(port<=0 || port >65535) throw new IllegalArgumentException(“端口不合法”+port); ServerSocket server=new ServerSocket(port); while (true) { try{ final Socket socket=server.accept(); new Thread(new Runnable() { @Override public void run() { try { try { ObjectInputStream input = new ObjectInputStream(socket.getInputStream()); try { String methodName = input.readUTF(); Class<?>[] parameterTypes = (Class<?>[]) input.readObject(); Object[] arguments = (Object[]) input.readObject(); ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream()); try { Method method = service.getClass().getMethod(methodName, parameterTypes); Object result = method.invoke(service, arguments); output.writeObject(result); } catch (Throwable t) { output.writeObject(t); } finally { output.close(); } } finally { input.close(); } } finally { socket.close(); } } catch (Exception e) { e.printStackTrace(); } } }).start(); }catch(Exception e){ e.printStackTrace(); } } }可以看到正確返回計(jì)算結(jié)果2。
添加圖片注釋,不超過 140 字(可選)。
以上就是一個(gè)簡單的RPC例子,下面我們看一下如何優(yōu)化這個(gè)例子。
序列化和I/O模型的優(yōu)化。
數(shù)據(jù)序列化:什么是序列化?序列化就是編碼的過程,把對象或者數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化成二進(jìn)制字節(jié)碼的過程。
而反序列化就是把二進(jìn)制字節(jié)碼轉(zhuǎn)化成數(shù)據(jù)結(jié)構(gòu)或者對象。
只有經(jīng)過序列化后的數(shù)據(jù)才能在網(wǎng)絡(luò)中傳輸。
I/O模型:客戶端和服務(wù)端的通信依賴Socket I/O。
I/O 模型又可以分為:傳統(tǒng)的阻塞 I/O(Blocking I/O)、非阻塞 I/O(Non-blocking I/O)、I/O 多路復(fù)用(I/O multiplexing)、異步 I/O(Asynchronous I/O)下面針對上面的兩個(gè)技術(shù)點(diǎn)進(jìn)行優(yōu)化。
用Protobuf優(yōu)化數(shù)據(jù)序列化。
數(shù)據(jù)序列化方法有很多種方法,常見的有Avro,Thrift,,XML,JSON,Protocol Buffer等工具。
本文主要介紹的是Protobuf。
Protobuf 全稱““Protocol Buffer”” 是google 推出的高性能序列化工具。
現(xiàn)已經(jīng)在Github上開源。
Protobuf采用tag來標(biāo)識(shí)字段序號(hào),用varint 和zigzag兩種編碼方式對整形做特殊的處理,Protobuf序列化后的數(shù)據(jù)緊湊,而且序列化時(shí)間短。
下面兩張圖分別是Uber對不同的序列化框架做的比較結(jié)果。
添加圖片注釋,不超過 140 字(可選)。
從上面兩張看出從數(shù)據(jù)壓縮和時(shí)間維度上看,Protobuf 和 Thrift的性能都是非常優(yōu)秀的。
至于如何做到這么好的性能,本文不在這邊細(xì)究,有興趣的同學(xué)可以參考圖解Protobuf編碼以及Protocol Buffer 序列化原理大揭秘。
用Netty優(yōu)化I/O模型。
網(wǎng)絡(luò)通信中I/O模型可以大致分為以下四種(準(zhǔn)確說是5種,這里不討論信號(hào)驅(qū)動(dòng)I/O,因?yàn)樵谡嬲木幊讨?我們很少使用這種模型): 1、阻塞I/O 2、非阻塞I/O 3、I/O多路復(fù)用 4、異步I/O 我們知道I/O處理是非常耗時(shí)的,CPU的處理速度非常快,如何最大化的利用CPU的性能,就是要避免線程阻塞在I/O處理上。
業(yè)界目前比較多的采用I/O多路復(fù)用和異步I/O提高性能。
如何理解這四種I/O模型,大家可以參照銀行業(yè)務(wù)辦理例子。
Netty 正是采用了第三種 I/O多路復(fù)用的方法,I/O多路復(fù)用對應(yīng)Reactor模式。
Reactor把耗時(shí)的網(wǎng)絡(luò)操作編碼交給專門的線程或者線程池去處理。
比如下面這張圖是Reactor模式示意圖。
圖中mainReactor線程、subReactor線程、work線程這些不同的線程,分別干不同專業(yè)的事情,吞吐量自然上去了。
添加圖片注釋,不超過 140 字(可選)。
這里要再多說一句異步I/O。
前面提到的三種I/O模型都?xì)w屬于同步I/O,用戶發(fā)起I/O請求后需要等待或者輪詢內(nèi)核I/O的完成。
我現(xiàn)在用的是PHP中的swoole框架, 一款異步網(wǎng)絡(luò)通信框架。
當(dāng)時(shí)我第一次聽到異步I/O的感到很奇怪,因?yàn)橹翱吹接行┪恼吕锒加姓f到異步I/O往往對應(yīng)的是Proactor模式,而Proactor在Linix中沒有很好的實(shí)現(xiàn)。
那么Swoole是如何實(shí)現(xiàn)異步I/O。
這里就要提。
協(xié)程。
的概念了。
協(xié)程可以理解為用戶態(tài)的線程,他有兩個(gè)特點(diǎn): 1、占用的資源更少。
2、所有切換和調(diào)度都發(fā)生在用戶態(tài)。
Swoole底層就是借鑒了Go語言的協(xié)程,而Go語言之所有能受到關(guān)注和部分青睞,也是因?yàn)樗肓藚f(xié)程。
這里特別要推薦知乎專欄里的協(xié)程,高并發(fā)IO的終級殺器的文章,通過簡單的例子幫你理解協(xié)程。
那為什么協(xié)程可以提升IO效率?傳統(tǒng)的阻塞IO模型中,一個(gè)線程在處理IO請求時(shí)就被阻塞了,不能再去處理其他IO請求,而服務(wù)器創(chuàng)建線程的數(shù)量是有限的(線程消耗比較高的內(nèi)存資源),一個(gè)服務(wù)器能處理多少個(gè)客戶端的連接又取決于可以創(chuàng)建多少個(gè)線程,這也是造成傳統(tǒng)阻塞IO模型不能支持高并發(fā)的原因。
協(xié)程提供了另一種角度去解決高并發(fā)問題:把線程占用的資源降下來。
所以協(xié)程是十分輕量的,只有線程的幾十分之一,通過創(chuàng)建更多的協(xié)程實(shí)現(xiàn)同步的寫法。
這里多說一句,目前Java對協(xié)程的支持可以通過開源的協(xié)程庫Quasar實(shí)現(xiàn),不過看到消息說Oracle已經(jīng)在準(zhǔn)備把協(xié)程引入到新的Java版本中。
優(yōu)化結(jié)果的比較。
下面給出的是壓測代碼,。
parallel。
變量控制并發(fā)的請求數(shù)。
對阻塞I/O模型+java原生序列化方法壓測。
public static void main(String[] args) throws Exception {。
//并行度10。
int parallel = 10;。
//開始計(jì)時(shí)。
StopWatch sw = new StopWatch(); sw.start(); CountDownLatch signal = new CountDownLatch(1); CountDownLatch finish = new CountDownLatch(parallel); for (int index = 0; index < parallel; index++) { CalcParallelRequestThread client = new CalcParallelRequestThread(signal, finish, index); new Thread(client).start(); }。
//10個(gè)并發(fā)線程瞬間發(fā)起請求操作。
signal.countDown(); finish.await(); sw.stop(); String tip = String.format(“RPC調(diào)用總共耗時(shí): [%s] 毫秒”, sw.getTime()); System.out.println(tip); }下圖是并發(fā)請求數(shù)是10的時(shí)候,返回了10個(gè)結(jié)果,QPS大致在181。
添加圖片注釋,不超過 140 字(可選)。
下圖是將。
parallel。
調(diào)整到100。
可以看到部分請求開始報(bào)錯(cuò)了。
不能拿到正確結(jié)果。
添加圖片注釋,不超過 140 字(可選)。
優(yōu)化后并發(fā)10000個(gè)請求,QPS高達(dá)10214。
添加圖片注釋,不超過 140 字(可選)。
我們用tcpdump抓包工具看一下具體的TCP包。
添加圖片注釋,不超過 140 字(可選)。
可以看到client的端口是51332,Server端口8090。
發(fā)送了“beidou”字符串給Server, 對應(yīng)到代碼中。
Provider。
屬性。
代碼中的temp1 和temp2都是字符串“1”。
Server返回相加結(jié)果1。
添加圖片注釋,不超過 140 字(可選)。
讓我們看一下Server的返回結(jié)果。
添加圖片注釋,不超過 140 字(可選)。
0200 0000 一共32位表示body 長度為2個(gè)字節(jié)。
08 表示tag為采用Protobuf Variant 編碼, 02表示值為2。
總結(jié)。
本文只是通過一個(gè)簡單例子介紹了RPC中的I/O模型以及序列化,其實(shí)RPC本身是一個(gè)很大的話題,比如如何保證在不可靠的網(wǎng)絡(luò)中保證RPC的可靠性?如何實(shí)現(xiàn)客戶端的重試調(diào)用、超時(shí)控制?如何優(yōu)雅的起停服務(wù)、發(fā)現(xiàn)與注冊服務(wù)?還有很多問題值得大家研究學(xué)習(xí),這里不做過多探討了。
雖然國內(nèi)的阿里巴巴、騰訊、新浪,國外的Google、Facebook都有自己的RPC框架,但是他們都繞不開之前需要考慮的那些技術(shù)點(diǎn)。
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。