異步機制(Asynchronous) -- (二)異步消息機制兼談Hadoop RPC

上篇說了半天,卻迴避了一個重要的問題:爲什麼要用異步呢,它有什麼樣的好處?坦率的說,我對這點的認識不是太深刻(套句俗語,只可意會,不可言傳)。還是舉個例子吧:
比如Client向Server發送一個request,Server收到後需要100ms的處理時間,爲了方便起見,我們忽略掉網絡的延遲,並且,我們認爲Server端的處理能力是無窮大的。在這個use case下,如果採用同步機制,即Client發送request -> 等待結果 -> 繼續發送,那麼,一個線程一秒鐘之內只能夠發送10個request,如果希望達到10000 request/s的發送壓力,那麼Client端就需要創建1000個線程,而這麼多線程的context switch就成爲client的負擔了。而採用異步機制,就不存在這個問題了。Client將request發送出去後,立即發送下一個request,理論上,它能夠達到網卡發送數據的極限。當然,同時需要有機制不斷的接收來自Server端的response。

以上的例子其實就是這篇的主題,異步的消息機制,基本的流程是這樣的:

如果仔細琢磨的話,會發現這個流程中有兩個很重要的問題需要解決:
1. 當client接收到response後,怎樣確認它到底是之前哪個request的response呢?
2. 如果發送一個request後,這個request對應的response由於種種原因(比如server端出問題了)一直沒有返回。client怎麼能夠發現類似這樣長時間沒有收到response的request呢?

對於第一個問題,一般會嘗試給每個request分配一個獨一無二的ID,返回的Response會同時攜帶這個ID,這樣就能夠將request和response對應上了。
對於第二個問題,需要有一個timeout機制,對於每一個request都有一個定時器,如果到指定時間仍然沒有返回結果,那麼會觸發timeout操作。多說一句,timeout機制其實對於涉及網絡的同步機制也是非常有必要的,因爲有可能client與server之間的鏈接壞了,在極端情況下,client會被一直阻塞住。

紙上談兵了這麼久,還是看一個實際的例子。我在這裏用Hadoop的RPC代碼舉例。這裏需要事先說明的是,Hadoop的RPC對外的接口其實是同步的,但是,RPC的內部實現其實是異步消息機制。多說無益,直接看代碼吧(討論的所有代碼都在org.apache.hadoop.ipc.Client.java 裏):

這就是Client.java對外提供的接口。一共有兩個參數,param是希望發送的request,remoteId是指遠程server對應的Id。函數的返回就是response(也是繼承自writable)。所以說,這是一個同步調用,一旦call函數返回,那麼response也就拿到了。

call函數的具體實現一會再看,先介紹Client中兩個重要的內部類:

call這個類對應的就是一次異步請求。它的幾個成員變量:
id: 這個就是之前提過的,對於每一個request都需要分配一個唯一標示符,這樣接收到response後才能知道到底對應哪個request;
param: 需要發送到server的request;
value: 從server發送過來的response;
error: 可能發生的異常(比如網絡讀寫錯誤,server掛了,等等);
done:  表示這個call是否成功完成了,即是否接收到了response;

 

Connection這個類要比之前的Call複雜得多,所以我省略了很多這裏不會被討論的代碼。
Connection對應於一個連接,即一個socket。但同時,它又繼承自Thread,所有它本身又對應於一個線程。可以看出,在Hadoop的RPC中,一個連接對應於一個線程。先看他的成員變量:
server: 這是遠程server的地址;
socket: 對應的socket;
in / out: socket的輸入流和輸出流;
calls: 重要的成員變量。它是一個hash表, 維護了這個connection正在進行的所有call和它們對應的id之間的關係。當讀取到一個response後,就通過id在這張表中找到對應的call;
再看看它的run()函數。這是Connection這個線程的啓動函數,我貼的代碼中這個函數沒做任何的刪減,你可以發現,刨除一些冗餘代碼,這個函數其實就只做了一件事:receiveResponse,即等待接收response。

 

OK。回到call()這個函數,看看它到底做了什麼:

首先,它創建了一個新的call(這個call是Call類的實體,注意和call()函數的區分),然後根據remoteId找到對應的connection(Client類中維護了一個connection pool),然後調用connection.sendParam()。從前面找到這個函數,你會發現它就是將request寫入到socket,發送出去。
但值得一提的是,它使用的write是最普通的blocking IO,也是同步IO(後面會看到,它讀取response也是用的blcoking IO,所以,hadoop RPC雖然是異步機制,但是採用的是同步blocking IO,所以,異步消息機制還採用什麼樣的IO機制是沒有關係的)。
接下來,調用了call.wait(),將線程阻塞在這裏。直到在某個地方調用了call.notify(),它才重新運行起來,然後一通判斷後返回call.value,即接收到的response。

所以,剩下的問題是,到底是哪調用了call.notify()?
回到connection的receiveResponse函數:
首先,它從socket的輸入流中讀到一個id,然後根據這個id找到對應的call,調用call.setValue將從socket中讀取的response放入到call的value中,然後調用calls.remove(id)將這個call從隊列中移除。這裏要注意的是call.setValue,這個函數將value設置好之後,調用了call.notify()!

好了,讓我們再重頭將流程捋一遍:
這裏其實有兩個線程,一個線程是調用Client.call(),希望向遠程server發送請求的線程,另外一個線程就是connection對應的那個線程。當然,雖然有兩個線程,但server對應的只有一個socket。第一個線程創建call,然後調用call.sendParam將request通過這個socket發送出去;而第二個線程不斷的從socket中讀取response。因此,request的發送和response的接收被分隔到不同的線程中執行,而且這兩個線程之間關於socket的讀寫並沒有任何的同步機制,因此我認爲這個RPC是異步消息機制實現的,只不過通過call.wait()/call.notify()使得對外的接口看上去像是同步。

好了,Hadoop的RPC介紹完了(雖然我略掉了很多內容,比如timeout機制我這裏就沒寫),說說我個人的評價吧。我認爲,Hadoop的這個設計還是挺巧妙的,底層採用的是異步機制,但對外的接口提供的又是一般人比較習慣的同步方式。但是,我覺着缺點不是沒有,一個問題是一個鏈接就要產生一個線程,這個如果是在幾千臺的cluster中,仍然會帶來巨大的線程context switch的開銷;另一個問題是對於同一個remote server只有一個socket來進行數據的發送和接收,這樣的設計網絡的吞吐量很有可能上不去。(一家之言,歡迎指正)

未完待續~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章