RPC(遠端過程呼叫)簡介
RPC(Remote Procedure Call Protocol)——遠端過程呼叫協議,它是一種通過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。
之前聽過這個名詞,但是也只是大概記住了“遠端呼叫”之類的關鍵詞,而其他並沒有太多瞭解。
來到TX實習,確實如別人所說的那樣,公司內部有自己的開發框架。我所在部門使用的是一個叫做TAF(Tencent Application Framwork)的後臺框架,它本質上是一個分散式系統,提供了良好的RPC封裝。
那麼問題來了——RPC到底是什麼?
在校期間,大家多多少少也都寫過一些程式,比如寫個hello world服務類,然後本地呼叫下,如下所示。這些程式的特點是服務消費方和服務提供方是本地呼叫關係。
//服務介面
public interface HelloWorldService {
String sayHello(String msg);
}
1
2
3
4
//服務實現
public class HelloWorldServiceImpl implements HelloWorldService {
@Override
public String sayHello(String msg) {
String result = "hello world " + msg;
System.out.println(result);
return result;
}
}
1
2
3
4
5
6
7
8
9
//本地服務呼叫
public class Test {
public static void main(String[] args) {
HelloWorldService helloWorldService = new HelloWorldServiceImpl();
helloWorldService.sayHello("test");
}
}
1
2
3
4
5
6
7
而在大型網際網路公司,公司的系統都由成千上萬大大小小的服務組成,各個服務部署在不同的機器上,由不同的團隊負責。
這時就會遇到兩個問題:
要搭建一個新服務,免不了需要依賴他人的服務,而現在他人的服務都在遠端(而不是在本地),怎麼呼叫?
其它團隊要使用我們的服務,我們的服務該怎麼釋出以便他人呼叫?
RPC可以解決上面兩個問題。簡單來說,RPC就是說,假設有兩臺伺服器A和B,一個應用部署在A伺服器上,想要呼叫B伺服器上應用提供的函式/方法,由於不在一個記憶體空間,不能直接呼叫,需要通過網路來表達呼叫的語義和傳達呼叫的資料。
1 如何呼叫他人的遠端服務?
由於各服務部署在不同機器,服務間的呼叫免不了網路通訊過程,服務消費方每呼叫一個服務都要寫一大堆網路通訊相關的程式碼,不僅複雜而且極易出錯。
如果有一種方式能讓我們像呼叫本地服務一樣呼叫遠端服務,而讓呼叫者對網路通訊這些細節透明,那麼將大大提高生產力,比如服務消費方在執行helloWorldService.sayHello(“test”)時,實質上呼叫的是遠端的服務
RPC的例子有:阿里巴巴的hsf、dubbo(開源)、Facebook的thrift(開源)、Google grpc(開源)、Twitter的finagle等。
首先我們先看下一個RPC呼叫的流程:
1)服務消費方(client)呼叫以本地呼叫方式呼叫服務;
2)client stub接收到呼叫後負責將方法、引數等組裝成能夠進行網路傳輸的訊息體(編碼);
3)client stub找到服務地址,並將訊息傳送到服務端;
4)server stub收到訊息後進行解碼;
5)server stub根據解碼結果呼叫本地的服務;
6)本地服務執行並將結果返回給server stub;
7)server stub將返回結果打包成訊息(編碼)併發送至消費方;
8)client stub接收到訊息,並進行解碼;
9)服務消費方得到最終結果。
RPC的目標就是要2~8這些步驟都封裝起來,讓使用者對這些細節透明。
Q:怎麼做到透明化遠端服務呼叫?
答:使用代理!
Q:為什麼使用代理呢?
舉個例子:假設你有一套房子要賣,一種方法是你直接去網上釋出出售資訊,然後直接帶要買房子的人來看房子等等,另一種方法是去找中介,中介實際上就是你的代理——本來是你要做的事情,現在中介幫助你一一處理。對於買方來說跟你直接交易跟同中介直接交易沒有任何差異,買方甚至可能覺察不到你的存在,這就是代理最大的好處。(當然,還有另外一個好處就是:在你很忙的時候,你可以把事情交給代理去做!)
下面簡單介紹下動態代理怎麼實現我們的需求。我們需要實現RPCProxyClient代理類,代理類的invoke方法中封裝了與遠端服務通訊的細節,消費方首先從RPCProxyClient獲得服務提供方的介面,當執行helloWorldService.sayHello(“test”)方法時就會呼叫invoke方法。
Q:怎麼對訊息進行編碼和解碼?
首先,要確定訊息資料結構。通訊的第一步就是要確定客戶端和服務端相互通訊的訊息結構。客戶端的請求訊息結構一般需要包括以下內容:
1)介面名稱
比如“HelloWorldService”,如果不傳,服務端就不知道呼叫哪個介面了;
2)方法名
一個介面內可能有很多方法,如果不傳方法名服務端也就不知道呼叫哪個方法;
3)引數型別&引數值
引數型別有很多,比如有bool、int、long、double、string、map、list,甚至如struct(class);
4)超時時間
5)requestID,標識唯一請求id。
同理服務端返回的訊息結構一般包括以下內容:
1)返回值
2)狀態code
3)requestID
第二步,就是序列化與反序列化。
序列化:將資料結構或物件轉換成二進位制串的過程,也就是編碼的過程。
反序列化:將在序列化過程中所生成的二進位制串轉換成資料結構或者物件的過程。
為什麼需要序列化:轉換為二進位制串後便於網路傳輸。
序列化方案在選擇的時候,主要看三點:
1)通用性,比如是否能支援Map等複雜的資料結構;
2)效能,包括時間複雜度和空間複雜度,由於RPC框架將會被公司幾乎所有服務使用,如果序列化上能節約一點時間,對整個公司的收益都將非常可觀,同理如果序列化上能節約一點記憶體,網路頻寬也能省下不少;
3)可擴充套件性,對網際網路公司而言,業務變化快,如果序列化協議具有良好的可擴充套件性,支援自動增加新的業務欄位,刪除老的欄位,而不影響老的服務,這將大大提供系統的健壯性。
目前國內各大網際網路公司廣泛使用hessian、protobuf、thrift、avro等成熟的序列化解決方案來搭建RPC框架。
2 如何釋出自己的服務?
如何讓別人使用我們的服務呢?
最基本的,我們需要告訴使用者服務的IP以及埠。但是問題在於,如果是直接通過告知IP+port的方式,將會有一系列問題:如果你發現你的服務一臺機器不夠,要再新增一臺,這個時候就要告訴呼叫者我現在有兩個ip了,你們要輪詢呼叫來實現負載均衡;呼叫者咬咬牙改了,結果某天一臺機器掛了,呼叫者發現服務有一半不可用,他又只能手動修改程式碼來刪除掛掉那臺機器的ip——這是非常不可取的。
機智的做法是:呼叫者不寫死服務提供方地址,並做到機器的增添、剔除對呼叫方透明。比如zookeeper被廣泛用於實現服務自動註冊與發現功能。
簡單來講,zookeeper可以充當一個服務登錄檔(Service Registry),讓多個服務提供者形成一個叢集,讓服務消費者通過服務登錄檔獲取具體的服務訪問地址(ip+埠)去訪問具體的服務提供者。
具體來說,zookeeper就是個分散式檔案系統,每當一個服務提供者部署後都要將自己的服務註冊到zookeeper的某一路徑上: /{service}/{version}/{ip:port}, 比如我們的HelloWorldService部署到兩臺機器,那麼zookeeper上就會建立兩條目錄:分別為/HelloWorldService/1.0.0/100.19.20.01:16888 /HelloWorldService/1.0.0/100.19.20.02:16888。
zookeeper提供了“心跳檢測”功能,它會定時向各個服務提供者傳送一個請求(實際上建立的是一個 socket 長連線),如果長期沒有響應,服務中心就認為該服務提供者已經“掛了”,並將其剔除,比如100.19.20.02這臺機器如果宕機了,那麼zookeeper上的路徑就會只剩/HelloWorldService/1.0.0/100.19.20.01:16888。
服務消費者會去監聽相應路徑(/HelloWorldService/1.0.0),一旦路徑上的資料有任務變化(增加或減少),zookeeper都會通知服務消費方服務提供者地址列表已經發生改變,從而進行更新。
更為重要的是zookeeper 與生俱來的容錯容災能力,可以確保服務登錄檔的高可用性。