轉載地址:http://www.cnblogs.com/LBSer/p/4853234.html
在學校期間大家都寫過不少程序刀森,比如寫個hello world服務類沽翔,然后本地調用下,如下所示。這些程序的特點是服務消費方和服務提供方是本地調用關系。
而一旦踏入公司尤其是大型互聯(lián)網(wǎng)公司就會發(fā)現(xiàn),公司的系統(tǒng)都由成千上萬大大小小的服務組成睛榄,各服務部署在不同的機器上,由不同的團隊負責想帅。這時就會遇到兩個問題:1)要搭建一個新服務场靴,免不了需要依賴他人的服務,而現(xiàn)在他人的服務都在遠端港准,怎么調用旨剥?2)其它團隊要使用我們的新服務,我們的服務該怎么發(fā)布以便他人調用浅缸?下文將對這兩個問題展開探討轨帜。
1publicinterfaceHelloWorldService {2String sayHello(String msg);3}
1publicclassHelloWorldServiceImplimplementsHelloWorldService {2@Override3publicString sayHello(String msg) {4String result = "hello world " +msg;5System.out.println(result);6returnresult;7}8}
1publicclassTest {2publicstaticvoidmain(String[] args) {3HelloWorldService helloWorldService =newHelloWorldServiceImpl();4helloWorldService.sayHello("test");5}6}
1 如何調用他人的遠程服務?
由于各服務部署在不同機器衩椒,服務間的調用免不了網(wǎng)絡通信過程蚌父,服務消費方每調用一個服務都要寫一坨網(wǎng)絡通信相關的代碼哮兰,不僅復雜而且極易出錯。
如果有一種方式能讓我們像調用本地服務一樣調用遠程服務苟弛,而讓調用者對網(wǎng)絡通信這些細節(jié)透明喝滞,那么將大大提高生產力,比如服務消費方在執(zhí)行helloWorldService.sayHello("test")時膏秫,實質上調用的是遠端的服務右遭。這種方式其實就是RPC(Remote Procedure Call Protocol),在各大互聯(lián)網(wǎng)公司中被廣泛使用缤削,如阿里巴巴的hsf窘哈、dubbo(開源)、Facebook的thrift(開源)亭敢、Google grpc(開源)滚婉、Twitter的finagle(開源)等。
要讓網(wǎng)絡通信細節(jié)對使用者透明吨拗,我們需要對通信細節(jié)進行封裝满哪,我們先看下一個RPC調用的流程涉及到哪些通信細節(jié):
1)服務消費方(client)調用以本地調用方式調用服務婿斥;
2)client stub接收到調用后負責將方法劝篷、參數(shù)等組裝成能夠進行網(wǎng)絡傳輸?shù)南Ⅲw;
3)client stub找到服務地址民宿,并將消息發(fā)送到服務端娇妓;
4)server stub收到消息后進行解碼;
5)server stub根據(jù)解碼結果調用本地的服務活鹰;
6)本地服務執(zhí)行并將結果返回給server stub哈恰;
7)server stub將返回結果打包成消息并發(fā)送至消費方;
8)client stub接收到消息志群,并進行解碼着绷;
9)服務消費方得到最終結果。
RPC的目標就是要2~8這些步驟都封裝起來锌云,讓用戶對這些細節(jié)透明荠医。
1.1 怎么做到透明化遠程服務調用?
怎么封裝通信細節(jié)才能讓用戶像以本地調用方式調用遠程服務呢桑涎?對java來說就是使用代理彬向!java代理有兩種方式:1) jdk 動態(tài)代理;2)字節(jié)碼生成攻冷。盡管字節(jié)碼生成方式實現(xiàn)的代理更為強大和高效娃胆,但代碼維護不易,大部分公司實現(xiàn)RPC框架時還是選擇動態(tài)代理方式等曼。
下面簡單介紹下動態(tài)代理怎么實現(xiàn)我們的需求里烦。我們需要實現(xiàn)RPCProxyClient代理類凿蒜,代理類的invoke方法中封裝了與遠端服務通信的細節(jié),消費方首先從RPCProxyClient獲得服務提供方的接口胁黑,當執(zhí)行helloWorldService.sayHello("test")方法時就會調用invoke方法篙程。
1publicclassRPCProxyClientimplementsjava.lang.reflect.InvocationHandler{2privateObject obj;34publicRPCProxyClient(Object obj){5this.obj=obj;6}78/**9* 得到被代理對象;10*/11publicstaticObject getProxy(Object obj){12returnjava.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(),13obj.getClass().getInterfaces(),newRPCProxyClient(obj));14}1516/**17* 調用此方法執(zhí)行18*/19publicObject invoke(Object proxy, Method method, Object[] args)20throwsThrowable {21//結果參數(shù);22Object result =newObject();23//...執(zhí)行通信相關邏輯24//...25returnresult;26}27}
1publicclassTest {2publicstaticvoidmain(String[] args) {3HelloWorldService helloWorldService = (HelloWorldService)RPCProxyClient.getProxy(HelloWorldService.class);4helloWorldService.sayHello("test");5}6}
1.2 ?怎么對消息進行編碼和解碼?
1.2.1 確定消息數(shù)據(jù)結構
上節(jié)講了invoke里需要封裝通信細節(jié)别厘,而通信的第一步就是要確定客戶端和服務端相互通信的消息結構虱饿。客戶端的請求消息結構一般需要包括以下內容:
1)接口名稱
在我們的例子里接口名是“HelloWorldService”触趴,如果不傳氮发,服務端就不知道調用哪個接口了;
2)方法名
一個接口內可能有很多方法冗懦,如果不傳方法名服務端也就不知道調用哪個方法爽冕;
3)參數(shù)類型&參數(shù)值
參數(shù)類型有很多,比如有bool披蕉、int颈畸、long、double没讲、string眯娱、map、list爬凑,甚至如struct(class)徙缴;
以及相應的參數(shù)值;
4)超時時間
5)requestID嘁信,標識唯一請求id于样,在下面一節(jié)會詳細描述requestID的用處。
同理服務端返回的消息結構一般包括以下內容潘靖。
1)返回值
2)狀態(tài)code
3)requestID
1.2.2 序列化
一旦確定了消息的數(shù)據(jù)結構后穿剖,下一步就是要考慮序列化與反序列化了。
什么是序列化卦溢?序列化就是將數(shù)據(jù)結構或對象轉換成二進制串的過程糊余,也就是編碼的過程。
什么是反序列化既绕?將在序列化過程中所生成的二進制串轉換成數(shù)據(jù)結構或者對象的過程啄刹。
為什么需要序列化?轉換為二進制串后才好進行網(wǎng)絡傳輸嘛凄贩!
為什么需要反序列化誓军?將二進制轉換為對象才好進行后續(xù)處理!
現(xiàn)如今序列化的方案越來越多疲扎,每種序列化方案都有優(yōu)點和缺點昵时,它們在設計之初有自己獨特的應用場景捷雕,那到底選擇哪種呢?從RPC的角度上看壹甥,主要看三點:1)通用性救巷,比如是否能支持Map等復雜的數(shù)據(jù)結構;2)性能句柠,包括時間復雜度和空間復雜度浦译,由于RPC框架將會被公司幾乎所有服務使用,如果序列化上能節(jié)約一點時間溯职,對整個公司的收益都將非尘眩可觀,同理如果序列化上能節(jié)約一點內存谜酒,網(wǎng)絡帶寬也能省下不少叹俏;3)可擴展性,對互聯(lián)網(wǎng)公司而言僻族,業(yè)務變化飛快粘驰,如果序列化協(xié)議具有良好的可擴展性,支持自動增加新的業(yè)務字段述么,而不影響老的服務蝌数,這將大大提供系統(tǒng)的靈活度。
目前互聯(lián)網(wǎng)公司廣泛使用Protobuf碉输、Thrift籽前、Avro等成熟的序列化解決方案來搭建RPC框架,這些都是久經(jīng)考驗的解決方案敷钾。
1.3 ?通信
消息數(shù)據(jù)結構被序列化為二進制串后,下一步就要進行網(wǎng)絡通信了肄梨。目前有兩種常用IO通信模型:1)BIO阻荒;2)NIO。一般RPC框架需要支持這兩種IO模型众羡,原理可參考:一個故事講清楚NIO侨赡。
如何實現(xiàn)RPC的IO通信框架呢?1)使用java nio方式自研粱侣,這種方式較為復雜羊壹,而且很有可能出現(xiàn)隱藏bug,但也見過一些互聯(lián)網(wǎng)公司使用這種方式齐婴;2)基于mina油猫,mina在早幾年比較火熱,不過這些年版本更新緩慢柠偶;3)基于netty情妖,現(xiàn)在很多RPC框架都直接基于netty這一IO通信框架睬关,省力又省心,比如阿里巴巴的HSF毡证、dubbo电爹,Twitter的finagle等。
1.4消息里為什么要有requestID料睛?
如果使用netty的話丐箩,一般會用channel.writeAndFlush()方法來發(fā)送消息二進制串,這個方法調用后對于整個遠程調用(從發(fā)出請求到接收到結果)來說是一個異步的恤煞,即對于當前線程來說雏蛮,將請求發(fā)送出來后,線程就可以往后執(zhí)行了阱州,至于服務端的結果挑秉,是服務端處理完成后,再以消息的形式發(fā)送給客戶端的苔货。于是這里出現(xiàn)以下兩個問題:
1)怎么讓當前線程“暫拖牛”,等結果回來后夜惭,再向后執(zhí)行姻灶?
2)如果有多個線程同時進行遠程方法調用,這時建立在client server之間的socket連接上會有很多雙方發(fā)送的消息傳遞诈茧,前后順序也可能是隨機的产喉,server處理完結果后,將結果消息發(fā)送給client敢会,client收到很多消息曾沈,怎么知道哪個消息結果是原先哪個線程調用的?
如下圖所示鸥昏,線程A和線程B同時向client socket發(fā)送請求requestA和requestB塞俱,socket先后將requestB和requestA發(fā)送至server,而server可能將responseA先返回吏垮,盡管requestA請求到達時間更晚障涯。我們需要一種機制保證responseA丟給ThreadA,responseB丟給ThreadB膳汪。
怎么解決呢唯蝶?
1)client線程每次通過socket調用一次遠程接口前翅溺,生成一個唯一的ID颅围,即requestID(requestID必需保證在一個Socket連接里面是唯一的),一般常常使用AtomicLong從0開始累計數(shù)字生成唯一ID蛤虐;
2)將處理結果的回調對象callback媳谁,存放到全局ConcurrentHashMap里面put(requestID, callback)涂滴;
3)當線程調用channel.writeAndFlush()發(fā)送消息后友酱,緊接著執(zhí)行callback的get()方法試圖獲取遠程返回的結果。在get()內部柔纵,則使用synchronized獲取回調對象callback的鎖缔杉,再先檢測是否已經(jīng)獲取到結果,如果沒有搁料,然后調用callback的wait()方法或详,釋放callback上的鎖,讓當前線程處于等待狀態(tài)郭计。
4)服務端接收到請求并處理后霸琴,將response結果(此結果中包含了前面的requestID)發(fā)送給客戶端,客戶端socket連接上專門監(jiān)聽消息的線程收到消息昭伸,分析結果梧乘,取到requestID,再從前面的ConcurrentHashMap里面get(requestID)庐杨,從而找到callback對象选调,再用synchronized獲取callback上的鎖,將方法調用結果設置到callback對象里灵份,再調用callback.notifyAll()喚醒前面處于等待狀態(tài)的線程仁堪。
1publicObject get() {2synchronized(this) {//旋鎖3while(!isDone) {//是否有結果了4wait();//沒結果是釋放鎖,讓當前線程處于等待狀態(tài)5}6}7}
1privatevoidsetDone(Response res) {2this.res =res;3isDone =true;4synchronized(this) {//獲取鎖填渠,因為前面wait()已經(jīng)釋放了callback的鎖了5notifyAll();//喚醒處于等待的線程6}7}
2 如何發(fā)布自己的服務弦聂?
如何讓別人使用我們的服務呢?有同學說很簡單嘛氛什,告訴使用者服務的IP以及端口就可以了啊莺葫。確實是這樣,這里問題的關鍵在于是自動告知還是人肉告知屉更。
人肉告知的方式:如果你發(fā)現(xiàn)你的服務一臺機器不夠徙融,要再添加一臺,這個時候就要告訴調用者我現(xiàn)在有兩個ip了瑰谜,你們要輪詢調用來實現(xiàn)負載均衡;調用者咬咬牙改了树绩,結果某天一臺機器掛了萨脑,調用者發(fā)現(xiàn)服務有一半不可用,他又只能手動修改代碼來刪除掛掉那臺機器的ip〗确梗現(xiàn)實生產環(huán)境當然不會使用人肉方式渤早。
有沒有一種方法能實現(xiàn)自動告知,即機器的增添瘫俊、剔除對調用方透明鹊杖,調用者不再需要寫死服務提供方地址悴灵?當然可以,現(xiàn)如今zookeeper被廣泛用于實現(xiàn)服務自動注冊與發(fā)現(xiàn)功能骂蓖!
簡單來講积瞒,zookeeper可以充當一個服務注冊表(Service Registry),讓多個服務提供者形成一個集群登下,讓服務消費者通過服務注冊表獲取具體的服務訪問地址(ip+端口)去訪問具體的服務提供者茫孔。如下圖所示:
具體來說,zookeeper就是個分布式文件系統(tǒng)被芳,每當一個服務提供者部署后都要將自己的服務注冊到zookeeper的某一路徑上:?/{service}/{version}/{ip:port}, 比如我們的HelloWorldService部署到兩臺機器缰贝,那么zookeeper上就會創(chuàng)建兩條目錄:分別為/HelloWorldService/1.0.0/100.19.20.01:16888 ?/HelloWorldService/1.0.0/100.19.20.02:16888。
zookeeper提供了“心跳檢測”功能畔濒,它會定時向各個服務提供者發(fā)送一個請求(實際上建立的是一個 Socket 長連接)剩晴,如果長期沒有響應,服務中心就認為該服務提供者已經(jīng)“掛了”侵状,并將其剔除赞弥,比如100.19.20.02這臺機器如果宕機了,那么zookeeper上的路徑就會只剩/HelloWorldService/1.0.0/100.19.20.01:16888壹将。
服務消費者會去監(jiān)聽相應路徑(/HelloWorldService/1.0.0)嗤攻,一旦路徑上的數(shù)據(jù)有任務變化(增加或減少),zookeeper都會通知服務消費方服務提供者地址列表已經(jīng)發(fā)生改變诽俯,從而進行更新妇菱。
更為重要的是zookeeper與生俱來的容錯容災能力(比如leader選舉),可以確保服務注冊表的高可用性暴区。
3 小結
RPC幾乎是每一個從學校進入互聯(lián)網(wǎng)公司的同學都要首先學習的框架闯团,之前面試過一個在大型互聯(lián)網(wǎng)公司工作過兩年的同學,對RPC還是停留在使用層面仙粱,這是不應該的房交,希望大家不僅要會用而且要知道內部的原理。本文也僅是對RPC的一個比較粗糙的描述伐割,希望對大家有所幫助候味,錯誤之處也請指出修正。
4 一些開源的RPC框架
https://github.com/alibaba/dubbo
http://thrift.apache.org/?cm_mc_uid=87762817217214314008006&cm_mc_sid_50200000=1444181090