1. 程式人生 > >深入理解Lustre檔案系統-第9篇 Portal RPC

深入理解Lustre檔案系統-第9篇 Portal RPC

Portal RPC為如下內容提供了基礎機制:

  • 通過輸入口傳送請求,接受請求
  • 通過輸出口接收和處理請求,傳送請求
  • 執行塊資料傳輸
  • 錯誤恢復

我們將首先探討Portal RPC的介面,而不深入到實現細節中。我們將用LDLM的傳送機製作為例子。對這個例項,LDLM向客戶端傳送一個阻塞ASTRPC(ldlm_server_blocking_ast),該客戶端是一個給定的鎖的佔有者和管理者。這個例子幫助我們更好地理解客戶端怎樣使用Portal RPC API。

首先,我們下面給出的那樣準備大小。

structldlm_request *req;

__u32 size[] = {[MSG_PTLRPC_BODY_OFF] = sizeof(struct ptlrpc_body),

[DLM_LOCKREQ_OFF] = sizeof (*body) };

請求可以被看成一連串的記錄,第一個記錄的偏移量是0,而第二個記錄的偏移量是2,如此繼續。一旦確定了大小,我們可以呼叫ptlrpc_prep_req()。這個函式的原型是:

structptlrpc_request *

ptlrpc_prep_req(struct obd_import *imp, __u32 version, int opcode,

int count, __u32 *lengths, char **bufs)

要進行RPC通訊,客戶端需要一個輸入口,而它在連線階段就建立了。*imp指標就指向這個輸入口,而version則指出Portal RPC內部使用的用來打包資料的版本。這裡的打包是傳輸線上的格式(on-the-wireformat),定義了在網路報文中,緩衝實際上處於哪個位置。為了區分它們的佈局(layout)請求,每個子系統定義了版本號——例如,MDC和MDS定義了它們的版本,MGC和MGS則定義了另外一個。

opcode確定了這個請求是對於什麼的請求。每個子系統定義了一些操作(更多資訊,參看lustre_idl.h)。count就是這個請求所需的緩衝數,而*length是一個數組,其中每個元素確定了對應請求緩衝的大小。最後一個引數signalsPortalRPC to accept (copy the data over) and process the incoming packet as is(??)。對於我們的例子,這個函式按如下形式被呼叫:

req =ptlrpc_prep_req(lock->l_export->exp_imp_reverse,

      LUSTRE_DLM_VERSION, LDLM_BL_CALLBACK, 2,size, NULL);

這種呼叫表明,請求了兩個緩衝,而每個緩衝的大小由引數列表中的size表示。

除了內部管理(housekeeping)之外,上述呼叫還分配了請求緩衝,並將之儲存在req->rq_reqmsg中。感興趣的地址能夠通過給出記錄偏移量來得到:

body =lustre_msg_buf(req->rq_reqmsg, DLM_LOCKREQ_OFF, sizeof(*body));

在伺服器端,我們能夠看到有著類似輸入引數的相同的幫助方法,它被用來取得感興趣的欄位。一旦得到了緩衝結構體,就可以進一步地填入請求所需的欄位。在所有這些完成之後,有幾種方法來發送請求,如下所述:

ptl_send_rpc() 傳送RPC的一種簡單形式,它不等待回覆,在失敗發生時也不重發。這不是一個傳送RPC的優選方法。

ptlrpcd_add_req()一個完全非同步的RPC傳送方法,由ptl-rpcd守護程序處理。

ptlrpc_set_wait()一個同步地傳送多條訊息的方法,只有在它得到所有回覆時才會返回。首先,使用ptlrpc_set_add_req()來將請求放入一個未初始化集合裡(pre-initialized set),該集合裡面包含了一個或者多個需要一齊發送的請求。

ptlrpc_queue_wait()可能是最常用的傳送RPC的方法。它是同步的,只有在RPC的請求傳送完,且接收到回覆後才返回。

在呼叫RPC請求之後的最後一步是,通過呼叫ptlrpc_req_finished(req)來釋放資源引用。

伺服器端使用Portal RPC的方法和客戶端完全不同。首先,它使用如下函式初始化服務:

structptlrpc_service * ptlrpc_init_svc (

      int nbufs, /* num of buffers to allocate*/

      int bufsize, /* size of above requestedbuffer */

      int max_req_size, /* max request sizeserver will accept */

      int max_reply_size, /* max reply sizeserver will send */

      int req_portal, /* req service port inlnet */

      int rep_portal, /* rep service port inlnet */

      int watchdog_factor, /* wait time forhandler to finish */

      svc_handler_t handler, /* service handlerfunction */

      char *name, /* service name */

      cfs_proc_dir_entry_t *proc_entry, /* forprocfs */

      svcreq_printfn_t svcreq_printfn, /* forprocfs */

      int min_threads, /* min # of threads tostart */

      int max_threads, /* max # of threads tostart */

      char *threadname /* thread name prefix */

)

一旦呼叫返回,請求就可以進入,就將呼叫註冊好的處理函式。通常,伺服器將手頭的工作分為幾類。對每類,它建立一個不同的執行緒池。這些執行緒共享同樣的處理函式。使用不同的池的原因是為了防止餓死。在一些情況下,多個執行緒池也防止了死鎖,在死鎖情況下,為處理一個新的RPC,所有執行緒都在等待某個資源變成可用。

客戶端首先發送一個塊RPC請求。讓我們假設這是一個寫請求。它裡面包含了對傳輸什麼的描述。現在伺服器處理請求,分配空間,然後控制資料傳輸。服務端的下一個RPC將把資料傳輸到先前分配的空間裡。osc_brw_pre_request()裡所做的就是上述過程的一個例項。讓我們看一下這個過程:

1. 塊傳輸的初始化從上述的準備工作開始著手。然而,因為我們是在從一個已分配好的池裡請求,所以準備請求有點不同,如果請求本身可能和記憶體不足的情況相關,那麼這就是導致記憶體不足的那種情形(?)。

req =ptlrpc_pre_req_pool(cli->cl_import, LUSTRE_OST_VERSION,

opc, 4, size, null, pool)

這裡的opc可能是,比如說,OST_WRITE。

2. 接著,我們制定服務入口。在接入口結構中,有一個入口,作為預設情況,請求將由這個入口處理。但是,在這種情況下,讓我們假設請求將由一個特定的入口處理:

req->rq_request_portal= OST_IO_PORTAL;

3. 然後,我們需要準備塊請求。我們傳入指向請求的指標、頁數目、型別和目的入口。返回的是對這個請求的塊描述符。注意,塊請求將傳入另外一個的入口:

structptlrpc_bulk_desc desc = ptlrpc_prep_bulk_imp(req, page_count,

BULK_GET_SOURCE, OST_BULK_PORTAL);

4. 對於每個需要傳輸的頁,我們呼叫ptlrpc_prep_bulk_page(),將該頁及時地加到塊描述符中。這是請求裡面的一個標誌,它表明這是一個塊請求,而我們需要檢查這個描述符,以取得頁的佈局資訊。

structptlrpc_request {

      ...

      struct ptlrpc_bulk_desc *rq_bulk;

      ...

}

在伺服器端,整體的準備結構是類似的,但是現在準備的是輸出口,而不是輸入口。在ost處理函式中的ost_brw_read()裡可以看到一個例子:

desc =ptlrpc_prep_bulk_exp(req, npages, BULK_PUT_SOURCE, OST_BULK_PORTAL);

伺服器端也需要為每個塊頁做準備。然後,伺服器端就可以開始傳輸:

rc =ptlrpc_start_bulk_transfer(desc);

在此時,從客戶端發來的第一個RPC請求已經由伺服器處理了,而伺服器已為塊資料傳輸做好了準備。現在客戶端可以像我們在此節開始時提及的那樣開始塊RPC傳輸。

NRS優化

另外一個需要指出的是,在伺服器端,我們接到了大量的描述符,這些描述符描述了待讀或待寫的頁佈局。如果在相同的方向上,存在鄰接的讀或者寫,就提供了一個優化的機會。如果確實存在,那麼它們可以被聚合和同時處理。這就是網路請求排程(Network Request Scheduler, NRS)的課題。這也顯示出兩階段塊傳輸的重要性,它使我們有了一個想法:在輸入輸出資料時,並不立馬取得資料,而是重新組織,以獲得更好的效能。兩階段操作的另外一個原因是,隨著服務初始化的增加,就會分配一定量的緩衝空間。當客戶端請求到達時,在進一步處理之前,可以將它們先快取在這個空間裡,因為為了容納潛在的塊傳輸而預先分配一大塊空間並不是優選的方法。另外,不用大的資料塊覆蓋伺服器的緩衝空間是非常重要的,在這種情形下,兩階段操作也有作用。

大多數的恢復機制都在Portal RPC層實現。我們以一個從高層傳遞下來的portal RPC請求作為開始。在Portal RPC內部,輸入口維護了兩個連結串列,它們對我們的探討非常重要。它們就是sending和replay連結串列。輸入口同時維護了imp->states和imp->flags。可能的狀態是:full、connecting、disconnecting和 recovery,可能的標誌是invalid、active和inactive。

在檢查輸入口的健康狀態後,傳送請求將繼續。這些步驟序列如下:

1. 傳送RPC請求,然後將它存入在正在傳送連結串列,並在客戶端開始obd計時器。

2. 如果伺服器在計時耗盡之前回復,而請求又是可重複的,那麼將之從傳送連結串列刪除,並加入重複連結串列。如果請求是不可重複的,那麼在接收到回覆時,將之從傳送連結串列中刪除。

從伺服器端發來的回覆並不一定意味著它已經將資料寫入了磁碟(假設請求改變了盤上資料)。所以,我們必須等待從伺服器端發出的事務提交(一個事務號),它意味著現在變更已經安全地提交給磁碟了。這種上一次伺服器提交的事物號通常附加在(piggbacked)在各個伺服器回覆上。

通常,從MDC到MDS的請求是可重複的,但是OSC到OST的請求則不是,而這僅僅在非同步日誌更新未使能時才是對的。這裡有兩個原因:

  • 首先。從OSC到OST的資料請求(讀或寫)可能非常大,為供重複,將他們儲存在記憶體裡,這對於記憶體是一個非常大的負擔。
  • OST只使用直接IO(最少現在如此)。帶有事務號的回覆本身就是對提交已完成的足夠保障。

3. 如果計時器超時,客戶端將這個輸入口狀態從full轉變為disconnect。現在pinger插入一腳,如果伺服器對pinger有響應,那麼客戶端將試著重連(以reconnect標誌連線)。

4. 如果重連成功,那麼我們開始恢復過程。我們現在將狀態標記為recovery,並首先開始傳送重複連結串列中的其請求,然後是傳送正在傳送連結串列中的請求。

關於pinger的關鍵之處是,如果請求以足夠的頻率傳送,那就不需要用pinger了。只有在客戶端有一個較長的空閒時期,pinger才用來和伺服器保持活躍連線,從而防止由於無活動而被驅逐。在另一方面,如果客戶端由於任何原因而離線,伺服器將不會被客戶端ping,伺服器可能也會驅逐這個客戶端。

本文章歡迎轉載,請保留原始部落格連結http://blog.csdn.net/fsdev/article