–高性能和透明化的RPC遠程服務調用方案 –SOA服務治理方案
-Apache MINA 框架基于Reactor模型通信框架,基于tcp長連接
Dubbo缺省協(xié)議采用單一長連接和NIO異步通訊
基本原理如圖:
在 我們的系統(tǒng)中懈费,經常會有一些跨系統(tǒng)的調用葛家,如在A系統(tǒng)中要調用B系統(tǒng)的一個服務呜投,我們可能會使用RMI直接來進行,B系統(tǒng)發(fā)布一個RMI接口服務,然后A 系統(tǒng)就來通過RMI調用這個接口姻成,為了解決容災也祠,擴展昙楚,負載均衡的問題,我們可能會想很多辦法诈嘿,alibaba的這個辦法感覺不錯堪旧。
本文只說dubbo,原理如下:
ConfigServer
配置中心奖亚,和每個Server/Client之間會作一個實時的心跳檢測(因為它們都是建立的Socket長連接)淳梦,比如幾秒鐘檢測一次。收集每個Server提供的服務的信息昔字,每個Client的信息爆袍,整理出一個服務列表,如:
serviceNameserverAddressListclientAddressList
UserService192.168.0.1作郭,192.168.0.2陨囊,192.168.0.3,192.168.0.4172.16.0.1夹攒,172.16.0.2
ProductService192.168.0.3蜘醋,192.168.0.4,192.168.0.5咏尝,192.168.0.6172.16.0.2压语,172.16.0.3
OrderService192.168.0.10,192.168.0.12状土,192.168.0.5无蜂,192.168.0.6172.16.0.3,172.16.0.4
當某個Server不可用蒙谓,那么就更新受影響的服務對應的serverAddressList斥季,即把這個Server從serverAddressList中踢出去(從地址列表中刪除),同時將推送serverAddressList給這些受影響的服務的clientAddressList里面的所有Client。如:192.168.0.3掛了酣倾,那么UserService和ProductService的serverAddressList都要把192.168.0.3刪除掉舵揭,同時把新的列表告訴對應的Client172.16.0.1,172.16.0.2躁锡,172.16.0.3午绳;
當某個Client掛了,那么更新受影響的服務對應的clientAddressList
ConfigServer根據(jù)服務列表映之,就能提供一個web管理界面拦焚,來查看管理服務的提供者和使用者。
新加一個Server時杠输,由于它會主動與ConfigServer取得聯(lián)系赎败,而ConfigServer又會將這個信息主動發(fā)送給Client,所以新加一個Server時蠢甲,只需要啟動Server僵刮,然后幾秒鐘內,Client就會使用上它提供的服務
Client
調用服務的機器鹦牛,每個Client啟動時搞糕,主動與ConfigServer建立Socket長連接,并將自己的IP等相應信息發(fā)送給ConfigServer曼追。
Client在使用服務的時候根據(jù)服務名稱去ConfigServer中獲取服務提供者信息(這樣ConfigServer就知道某個服務是當前哪幾個Client在使用)窍仰,Client拿到這些服務提供者信息后,與它們都建立連接拉鹃,后面就可以直接調用服務了辈赋,當有多個服務提供者的時候,Client根據(jù)一定的規(guī)則來進行負載均衡膏燕,如輪詢钥屈,隨機,按權重等坝辫。
一旦Client使用的服務它對應的服務提供者有變化(服務提供者有新增篷就,刪除的情況),ConfigServer就會把最新的服務提供者列表推送給Client近忙,Client就會依據(jù)最新的服務提供者列表重新建立連接竭业,新增的提供者建立連接,刪除的提供者丟棄連接
Server
真正提供服務的機器及舍,每個Server啟動時未辆,主動與ConfigServer建立Scoket長連接,并將自己的IP锯玛,提供的服務名稱咐柜,端口等信息直接發(fā)送給ConfigServer兼蜈,ConfigServer就會收集到每個Server提供的服務的信息。
優(yōu)點:
1拙友,只要在Client和Server啟動的時候为狸,ConfigServer是好的,服務就可調用了遗契,如果后面ConfigServer掛了辐棒,那只影響ConfigServer掛了以后服務提供者有變化,而Client還無法感知這一變化牍蜂。
2漾根,Client每次調用服務是不經過ConfigServer的,Client只是與它建立聯(lián)系捷兰,從它那里獲取提供服務者列表而已
3立叛,調用服務-負載均衡:Client調用服務時负敏,可以根據(jù)規(guī)則在多個服務提供者之間輪流調用服務贡茅。
4,服務提供者-容災:某一個Server掛了其做,Client依然是可以正確的調用服務的顶考,當前提是這個服務有至少2個服務提供者,Client能很快的感知到服務提供者的變化妖泄,并作出相應反應驹沿。
5,服務提供者-擴展:添加一個服務提供者很容易蹈胡,而且Client會很快的感知到它的存在并使用它渊季。
Dubbo缺省協(xié)議,使用基于mina1.1.7+hessian3.2.1的tbremoting交互罚渐。
連接個數(shù):單連接
連接方式:長連接
傳輸協(xié)議:TCP
傳輸方式:NIO異步傳輸
序列化:Hessian二進制序列化
適用范圍:傳入傳出參數(shù)數(shù)據(jù)包較腥春骸(建議小于100K),消費者比提供者個數(shù)多荷并,單一消費者無法壓滿提供者合砂,盡量不要用dubbo協(xié)議傳輸大文件或超大字符串。
適用場景:常規(guī)遠程服務方法調用
通常源织,一個典型的同步遠程調用應該是這樣的:
1翩伪, 客戶端線程調用遠程接口,向服務端發(fā)送請求谈息,同時當前線程應該處于“暫驮狄伲“狀態(tài),即線程不能向后執(zhí)行了侠仇,必需要拿到服務端給自己的結果后才能向后執(zhí)行
2轻姿, 服務端接到客戶端請求后,處理請求,將結果給客戶端
3踢代, 客戶端收到結果盲憎,然后當前線程繼續(xù)往后執(zhí)行
Dubbo里使用到了Socket(采用apache mina框架做底層調用)來建立長連接,發(fā)送胳挎、接收數(shù)據(jù)饼疙,底層使用apache mina框架的IoSession進行發(fā)送消息。
查看Dubbo文檔及源代碼可知慕爬,Dubbo底層使用Socket發(fā)送消息的形式進行數(shù)據(jù)傳遞窑眯,結合了mina框架,使用IoSession.write()方法医窿,這個方法調用后對于整個遠程調用(從發(fā)出請求到接收到結果)來說是一個異步的磅甩,即對于當前線程來說,將請求發(fā)送出來姥卢,線程就可以往后執(zhí)行了卷要,至于服務端的結果,是服務端處理完成后独榴,再以消息的形式發(fā)送給客戶端的僧叉。于是這里出現(xiàn)了2個問題:
當前線程怎么讓它“暫停”棺榔,等結果回來后瓶堕,再向后執(zhí)行?
正如前面所說症歇,Socket通信是一個全雙工的方式郎笆,如果有多個線程同時進行遠程方法調用,這時建立在client server之間的socket連接上會有很多雙方發(fā)送的消息傳遞忘晤,前后順序也可能是亂七八糟的宛蚓,server處理完結果后,將結果消息發(fā)送給client德频,client收到很多消息苍息,怎么知道哪個消息結果是原先哪個線程調用的?
分析源代碼壹置,基本原理如下:
client一個線程調用遠程接口竞思,生成一個唯一的ID(比如一段隨機字符串,UUID等)钞护,Dubbo是使用AtomicLong從0開始累計數(shù)字的
將打包的方法調用信息(如調用的接口名稱盖喷,方法名稱,參數(shù)值列表等)难咕,和處理結果的回調對象callback课梳,全部封裝在一起距辆,組成一個對象object
向專門存放調用信息的全局ConcurrentHashMap里面put(ID, object)
將ID和打包的方法調用信息封裝成一對象connRequest,使用IoSession.write(connRequest)異步發(fā)送出去
當前線程再使用callback的get()方法試圖獲取遠程返回的結果暮刃,在get()內部跨算,則使用synchronized獲取回調對象callback的鎖, 再先檢測是否已經獲取到結果椭懊,如果沒有诸蚕,然后調用callback的wait()方法,釋放callback上的鎖氧猬,讓當前線程處于等待狀態(tài)背犯。
服務端接收到請求并處理后,將結果(此結果中包含了前面的ID盅抚,即回傳)發(fā)送給客戶端漠魏,客戶端socket連接上專門監(jiān)聽消息的線程收到消息,分析結果妄均,取到ID柱锹,再從前面的ConcurrentHashMap里面get(ID),從而找到callback丛晦,將方法調用結果設置到callback對象里奕纫。
監(jiān)聽線程接著使用synchronized獲取回調對象callback的鎖(因為前面調用過wait(),那個線程已釋放callback的鎖了)烫沙,再notifyAll(),喚醒前面處于等待狀態(tài)的線程繼續(xù)執(zhí)行(callback的get()方法繼續(xù)執(zhí)行就能拿到調用結果了)隙笆,至此锌蓄,整個過程結束。
當前線程怎么讓它“暫统湃幔”,等結果回來后铅忿,再向后執(zhí)行剪决?
答:先 生成一個對象obj柑潦,在一個全局map里put(ID,obj)存放起來,再用synchronized獲取obj鎖峻凫,再調用obj.wait()讓當前 線程處于等待狀態(tài),然后另一消息監(jiān)聽線程等到服務端結果來了后荧琼,再map.get(ID)找到obj差牛,再用synchronized獲取obj鎖,再調用 obj.notifyAll()喚醒前面處于等待狀態(tài)的線程偏化。
正如前面所說,Socket通信是一個全雙工的方式镐侯,如果有多個線程同時進行遠程方法調用,這時建立在client server之間的socket連接上會有很多雙方發(fā)送的消息傳遞析孽,前后順序也可能是亂七八糟的,server處理完結果后袜瞬,將結果消息發(fā)送給client,client收到很多消息邓尤,怎么知道哪個消息結果是原先哪個線程調用的拍鲤?
答:使用一個ID,讓其唯一汞扎,然后傳遞給服務端季稳,再服務端又回傳回來,這樣就知道結果是原先哪個線程的了澈魄。