alibaba有好幾個分布式框架,主要有:進行遠程調用(類似於RMI的這種遠程調用)的(dubbo、hsf),jms消息服務(napoli、notify),KV數據庫(tair)等。
這個框架/工具/產品在實現的時候,都考慮到了容災,擴展,負載均衡,於是出現一個配置中心(ConfigServer)的東西來解決這些問題。
基本原理如圖:
在 我們的系統中,經常會有一些跨系統的調用,如在A系統中要調用B系統的一個服務,我們可能會使用RMI直接來進行,B系統發布一個RMI接口服務,然后A 系統就來通過RMI調用這個接口,為了解決容災,擴展,負載均衡的問題,我們可能會想很多辦法,alibaba的這個辦法感覺不錯。
本文只說dubbo,原理如下:
配置中心,
和每個Server/Client之間會作一個實時的心跳檢測(
因為它們都是
建立的Socket長連接),比如幾秒鍾檢測一次。收集每個Server提供的服務的信息,每個Client的信息,整理出一個服務列表,如:
serviceName |
serverAddressList |
clientAddressList |
UserService |
192.168.0.1,192.168.0.2,192.168.0.3,192.168.0.4 |
172.16.0.1,172.16.0.2 |
ProductService |
192.168.0.3,192.168.0.4,192.168.0.5,192.168.0.6 |
172.16.0.2,172.16.0.3 |
OrderService |
192.168.0.10,192.168.0.12,192.168.0.5,192.168.0.6 |
172.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刪除掉,同時把新的列表告訴對應的Client 172.16.0.1,172.16.0.2,172.16.0.3;
當某個Client掛了,那么更新受影響的服務對應的clientAddressList
ConfigServer根據服務列表,就能提供一個web管理界面,來查看管理服務的提供者和使用者。
新加一個Server時,由於它會主動與ConfigServer取得聯系,而ConfigServer又會將這個信息主動發送給Client,所以
新加一個Server時,只需要啟動Server,然后幾秒鍾內,Client就會使用上它提供的服務
調用服務的機器,每個Client啟動時,主動與ConfigServer建立Socket長連接,並將自己的IP等相應信息發送給ConfigServer。
Client在使用服務的時候根據服務名稱去ConfigServer中獲取服務提供者信息(這樣ConfigServer就知道某個服務是
當前哪幾個Client在使用),Client拿到這些服務提供者信息后,與它們都建立連接,后面就可以直接調用服務了,當有多個服務提供者的時候,Client根據一定的規則來進行負載均衡,如輪詢,隨機,按權重等。
一旦Client使用的服務它對應的服務提供者有變化(服務提供者有新增,刪除的情況),ConfigServer就會把最新的服務提供者列表推送給Client,Client就會依據最新的服務提供者列表重新建立連接,新增的提供者建立連接,刪除的提供者丟棄連接
真正提供服務的機器,每個Server啟動
時,主動與ConfigServer建立Scoket長連接,並將自己的IP,提供的服務名稱,端口等信息直接發送給ConfigServer,ConfigServer就會收集到每個Server提供的服務的信息。
優點:
1,只要在Client和Server啟動的時候,ConfigServer是好的,服務就可調用了,如果后面
ConfigServer掛了,那只影響ConfigServer掛了以后服務提供者有變化,而Client還無法感知這一變化。
2,Client每次調用服務是不經過ConfigServer的,Client只是與它建立聯系,從它那里獲取提供服務者列表而已
3,調用服務-負載均衡:Client調用服務時,可以根據規則在多個服務提供者之間輪流調用服務。
4,服務提供者-容災:某一個Server掛了,Client依然是可以正確的調用服務的,當前提是這個服務有至少2個服務提供者,Client能很快的感知到服務提供者的變化,並作出相應反應。
5,服務提供者-擴展:添加一個服務提供者很容易,而且Client會很快的感知到它的存在並使用它。
順便說一下,hadoop里面的中心節點跟這里的configServer作用類似,在維護節點列表方面,不過它的相關計算都需要通過中心節節點,讓它來分配任務。
由於Dubbo底層采用Socket進行通信,自己對通信理理論也不是很清楚,所以順便把通信的知識也學習一下。
n 通信理論
計算機與外界的信息交換稱為通信。基本的通信方法有並行通信和串行通信兩種。
1.一組信息(通常是字節)的各位數據被同時傳送的通信方法稱為並行通信。並行通信依靠並行I/O接口實現。並行通信速度快,但傳輸線根數多,只適用於近距離(相距數公尺)的通信。
2.一組信息的各位數據被逐位順序傳送的通信方式稱為串行通信。串行通信可通過串行接口來實現。串行通信速度慢,但傳輸線少,適宜長距離通信。
串行通信按信息傳送方向分為以下3種:
1) 單工
只能一個方向傳輸數據
2) 半雙工
信息能雙向傳輸,但不能同時雙向傳輸
3) 全雙工
能雙向傳輸並且可以同時雙向傳輸
n Socket
Socket 是一種應用接口, TCP/IP 是網絡傳輸協議,雖然接口相同, 但是不同的協議會有不同的服務性質。創建Socket 連接時,可以指定使用的傳輸層協議,Socket 可以支持不同的傳輸層協議(TCP 或UDP ),當使用TCP 協議進行連接時,該Socket 連接就是一個TCP 連接。Soket 跟TCP/IP 並沒有必然的聯系。Socket 編程接口在設計的時候,就希望也能適應其他的網絡協議。所以,socket 的出現只是可以更方便的使用TCP/IP 協議棧而已。
引自:http://hi.baidu.com/lewutian/blog/item/b28e27fd446d641d09244d08.html
上一個通信理論其實是想說Socket(TCP)通信是全雙工的方式
n Dubbo遠程同步調用原理分析
從Dubbo開源文檔上了解到一個調用過程如下圖
http://code.alibabatech.com/wiki/display/dubbo/User+Guide#UserGuide-APIReference
另外文檔里有說明:Dubbo缺省協議采用單一長連接和NIO異步通訊,適合於小數據量大並發的服務調用,以及服務消費者機器數遠大於服務提供者機器數的情況。
Dubbo缺省協議,使用基於mina1.1.7+hessian3.2.1的tbremoting交互。
- 連接個數:單連接
- 連接方式:長連接
- 傳輸協議:TCP
- 傳輸方式:NIO異步傳輸
- 序列化:Hessian二進制序列化
- 適用范圍:傳入傳出參數數據包較小(建議小於100K),消費者比提供者個數多,單一消費者無法壓滿提供者,盡量不要用dubbo協議傳輸大文件或超大字符串。
- 適用場景:常規遠程服務方法調用
通常,一個典型的同步遠程調用應該是這樣的:
1, 客戶端線程調用遠程接口,向服務端發送請求,同時當前線程應該處於“暫停“狀態,即線程不能向后執行了,必需要拿到服務端給自己的結果后才能向后執行
2, 服務端接到客戶端請求后,處理請求,將結果給客戶端
3, 客戶端收到結果,然后當前線程繼續往后執行
Dubbo里使用到了Socket(采用apache mina框架做底層調用)來建立長連接,發送、接收數據,底層使用apache mina框架的IoSession進行發送消息。
查看Dubbo文檔及源代碼可知,Dubbo底層使用Socket發送消息的形式進行數據傳遞,結合了mina框架,使用IoSession.write()方法,這個方法調用后對於整個遠程調用(從發出請求到接收到結果)來說是一個異步的,即對於當前線程來說,將請求發送出來,線程就可以往后執行了,至於服務端的結果,是服務端處理完成后,再以消息的形式發送給客戶端的。於是這里出現了2個問題:
- 當前線程怎么讓它“暫停”,等結果回來后,再向后執行?
- 正如前面所說,Socket通信是一個全雙工的方式,如果有多個線程同時進行遠程方法調用,這時建立在client server之間的socket連接上會有很多雙方發送的消息傳遞,前后順序也可能是亂七八糟的,server處理完結果后,將結果消息發送給client,client收到很多消息,怎么知道哪個消息結果是原先哪個線程調用的?
分析源代碼,基本原理如下:
- client一個線程調用遠程接口,生成一個唯一的ID(比如一段隨機字符串,UUID等),Dubbo是使用AtomicLong從0開始累計數字的
- 將打包的方法調用信息(如調用的接口名稱,方法名稱,參數值列表等),和處理結果的回調對象callback,全部封裝在一起,組成一個對象object
- 向專門存放調用信息的全局ConcurrentHashMap里面put(ID, object)
- 將ID和打包的方法調用信息封裝成一對象connRequest,使用IoSession.write(connRequest)異步發送出去
- 當前線程再使用callback的get()方法試圖獲取遠程返回的結果,在get()內部,則使用synchronized獲取回調對象callback的鎖, 再先檢測是否已經獲取到結果,如果沒有,然后調用callback的wait()方法,釋放callback上的鎖,讓當前線程處於等待狀態。
- 服務端接收到請求並處理后,將結果(此結果中包含了前面的ID,即回傳)發送給客戶端,客戶端socket連接上專門監聽消息的線程收到消息,分析結果,取到ID,再從前面的ConcurrentHashMap里面get(ID),從而找到callback,將方法調用結果設置到callback對象里。
- 監聽線程接着使用synchronized獲取回調對象callback的鎖(因為前面調用過wait(),那個線程已釋放callback的鎖了),再notifyAll(),喚醒前面處於等待狀態的線程繼續執行(callback的get()方法繼續執行就能拿到調用結果了),至此,整個過程結束。
這里還需要畫一個大圖來描述,后面再補了
需要注意的是,這里的callback對象是每次調用產生一個新的,不能共享,否則會有問題;另外ID必需至少保證在一個Socket連接里面是唯一的。
現在,前面兩個問題已經有答案了,
- 當前線程怎么讓它“暫停”,等結果回來后,再向后執行?
答:先 生成一個對象obj,在一個全局map里put(ID,obj)存放起來,再用synchronized獲取obj鎖,再調用obj.wait()讓當前 線程處於等待狀態,然后另一消息監聽線程等到服務端結果來了后,再map.get(ID)找到obj,再用synchronized獲取obj鎖,再調用 obj.notifyAll()喚醒前面處於等待狀態的線程。
- 正如前面所說,Socket通信是一個全雙工的方式,如果有多個線程同時進行遠程方法調用,這時建立在client server之間的socket連接上會有很多雙方發送的消息傳遞,前后順序也可能是亂七八糟的,server處理完結果后,將結果消息發送給client,client收到很多消息,怎么知道哪個消息結果是原先哪個線程調用的?
答:使用一個ID,讓其唯一,然后傳遞給服務端,再服務端又回傳回來,這樣就知道結果是原先哪個線程的了。
這種做法不是第一次見了,10年在上一公司里,也是遠程接口調用,不過走的消息中間件rabbitmq,同步調用的原理跟這類似,詳見:rabbitmq 學習-9- RpcClient發送消息和同步接收消息原理