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

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

    遠端程序呼叫(Remote Procedure Call,RPC)是構建分散式系統時所使用的一種常見元件。它使得客戶端可以像進行本地呼叫一樣進行遠端的過程呼叫,即客戶端可以忽略訊息傳遞的細節,而專注於過程呼叫的效果。

    Portal RPC是Lustre 的RPC元件,它構建於LNET之上,提供客戶端和伺服器之間進行訊息通訊的介面。

2.1      訊息通訊連線的建立

    進行Portal RPC通訊的是位於伺服器和客戶端的兩個訊息終端。伺服器上的訊息終端稱為出口端(export),而客戶端上的訊息中斷稱為入口端(import)。

    入口端是通過型別為obd_import的物件描述的。這個物件由class_new_import()函式新建。MGC、MDC、OSC的啟動函式mgc_setup()、mdc_setup()和osc_setup(),都會呼叫一個通用的函式client_obd_setup()。在這個函式裡,將會呼叫class_new_import()函式為該客戶端新建一個入口端。

    在客戶端新建了入口端之後,為了完成與伺服器的連線,會緊接著呼叫client_connect_import()函式向對應伺服器傳送一個請求訊息。請求訊息由型別為ptlrpc_request的物件描述,client_connect_import()函式小心地設定它的rq_import欄位,這樣當回覆被接收到後,就可以知道該回復是屬於哪個入口端了。

    與入口端相對的出口端使用型別為obd_export的物件來描述。在服務端,對來自每個客戶端的連線,都有一個對應的出口物件。這個出口物件並不在事先建立好,而是在接收到客戶端連線請求時新建。

    MGS、MDT、OST的在處理連線請求時都會呼叫通用的target_handle_connect()函式。在這個函式中,因為存在客戶端的連線恢復等情況,因此需要進行復雜的處理。在此,我們只分析最簡單的情形,即服務端首次接收到來自該客戶端的連線請求。在這種情況下,該函式通過obd_connect()這個通用的封裝函式,呼叫各服務定製的連線函式,如mgs_connect、mdt_obd_connect和filter_connect。這些函式又會呼叫通用的class_connect()函式,最終通過class_new_export()函式新建一個出口端。這樣,在連線請求的處理過程中,該入口端對應的出口端就被建立完成了。隨後,target_handle_connect()函式將會把回覆訊息的控制代碼通過lustre_msg_set_handle()設定為出口端的控制代碼。

    當服務端的這個連接回復訊息被客戶端接收到時,作為在client_connect_import()中被註冊的處理函式,ptlrpc_connect_interpret()將被呼叫。這個函式使用lustre_msg_get_handle()函式從回覆訊息中取得服務端設定的控制代碼,作為遠端控制代碼(remotehandle)放在obd_import物件的imp_remote_handle欄位中。自此以後,客戶端通過ptl_send_rpc()函式傳送訊息時,都用lustre_msg_set_handle()函式把訊息的控制代碼設定為遠端控制代碼的值。這樣,在服務端接收到在請求訊息後,請求處理函式ptlrpc_server_handle_req_in()可以從訊息中獲得這個控制代碼,並由此取得對應的出口端。

2.2      訊息的傳送與接收

    Portal RPC既支援同步的訊息通訊也支援非同步的訊息通訊。在同步訊息通訊中,請求傳送程序掛起直至訊息的回覆接受完成。在非同步訊息通訊中,請求傳送者不等待回覆,而交由Portal RPC的守護程序處理回覆。通訊可以以單一訊息或訊息集合的方式進行。

    下表給出了可以用來發送訊息的一些函式。

表 RPC傳送函式列表

函式

描述

ptl_send_rpc()

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

ptlrpc_queue_wait()

同步的傳送方法,只有在請求傳送完,且接收到回覆後才返回。

ptlrpcd_add_req()

完全非同步的傳送方法,請求在加入Portal RPC守護程序處理的佇列中後,即返回。隨後的處理由守護程序完成。

ptlrpc_set_wait()

同時傳送多條訊息的方法,只有在它得到集合中所有訊息的回覆後才會返回。集合裡的訊息是ptlrpc_set_add_req()函式事先放入的。

    在上述函式中,無論是同步傳送還是非同步傳送函式,最終的傳送操作都由ptl_send_rpc()函式呼叫LNET層提供的介面函式來完成。

    當訊息被LNET層軟體傳送/接受完畢或超時時,就會在事件佇列中新添一個事件。事件可以有很多不同的型別,其中一種經常會被用到的事件型別是NET_EVENT_UNLINK。該類事件可以發生在以下幾種時機:

  •  請求/回覆長時間未傳送,發生超時。此時由ptlrpc_unregister_reply()函式、ptlrpc_unregister_bulk()函式、ptlrpc_abort_bulk()函式直接呼叫LNetMDUnlink()函式。
  • 傳送/接受失敗,即LNetPut/LNetGet返回非零值。
  • LNET路由建立失敗或停止時。
  • PTLRPC服務停止時。此時由ptlrpc_unregister_service呼叫LNetMDUnlink()函式。

    LNetMDUnlink()函式首先呼叫lnet_build_unlink_event()函式在MD的事件佇列中新增一個事件,然後呼叫lnet_enq_event_locked ()函式處理這個事件。至於這裡MD的含義將在LNET一節講到。

    lnet_enq_event_locked()處理事件的方式是呼叫Portal RPC註冊的回撥函式。註冊回撥函式的過程發生在ptlrpc_ni_init()函式裡。它過LNetEQAlloc()函式將回調函式註冊為ptlrpc_master_callback()。ptlrpc_master_callback會根據LNET事件的回撥ID呼叫以下回調函式中的某一個。

表 PTLRPC回撥函式列表

函式

描述

註冊者

request_out_callback()

客戶端發出請求回撥函式

ptlrpc_prep_req_pool()

reply_in_callback()

客戶端接回覆回撥函式

ptlrpc_prep_req_pool()

client_bulk_callback()

客戶端的塊傳輸已完成

ptlrpc_prep_bulk_imp()

request_in_callback()

服務端接收請求回撥函式

ptlrpc_grow_req_bufs()

reply_out_callback()

服務端發出回覆回撥函式

ptlrpc_send_reply()

server_bulk_callback()

服務端塊傳輸完成回撥函式

ptlrpc_prep_bulk_exp()

    從客戶端傳送一個請求,到它接受到回覆,就是整個RPC過程。在這個RPC過程中,客戶端會產生兩次回撥,回撥函式分別是request_out_callback()和reply_in_callback()。在請求傳送完成時呼叫的回撥函式是request_out_callback()函式。如果請求被成功傳送,那麼LENT層被激發的事件型別是LNET_EVENT_SEND。否則,如果請求傳送失敗,被激發的事件型別是LNET_EVENT_UNLINK。在兩種情況下,request_out_callback()函式都會將ptlrpc_request型別的請求釋放掉。但是隻有在事件型別是LNET_EVENT_UNLINK的情況下,request_out_callback()才會喚醒傳送程序,以返回出錯,在傳送成功的情況下,傳送程序不會被喚醒。

    與此相比,reply_in_callback()回撥函式則不管事件型別是LNET_EVENT_UNLINK還是LNET_EVENT_PUT都會喚醒接收程序。同樣地,作為RPC最終完成時呼叫的回撥函式,client_bulk_callback()也會不論如何都喚醒程序。

    訊息的傳送是Porltal RPC呼叫LNET的API發起的,而訊息的接受流程則呈現相反的方向。LENT層在接收完一個訊息之後,將呼叫lnet_finalize()函式。這個函式會呼叫lnet_enq_event_locked()函式在MD的事件佇列中新建一個事件。注意到,lnet_enq_event_locked()函式的呼叫也發生在上面已經提到過的LNetMDUnlink()函式中。通過lnet_enq_event_locked()函式,LNET呼叫了Portal RPC註冊的回撥函式,也就將接受到的訊息通知到了上層。因此,可以說回撥函式是LNET將訊息通知到Portal RPC的方法。同時,回撥這種方式,作為軟體層間的一種通知方式,被廣泛應用在Lustre的各個地方。在以後的內容中,我們會更深刻地理解到這一點。

2.3      訊息處理服務

    在Lustre中的伺服器一般將自己的工作分為若干種服務。每個服務都用型別為ptlrpc_service的物件來描述。該物件的初始化是由ptlrpc_init_svc()函式完成的。ptlrpc_init_svc()函式將接受的req_portal和rep_portal引數分別被賦值到ptlrpc_service物件的srv_req_portal欄位和srv_rep_portal欄位中。它們是請求/回覆能被正確傳送到目標的關鍵。

    這兩個欄位分別對應了服務的請求portal和回覆portal。Portal直接翻譯出來是入口的意思,在這裡的含義類似於IP地址埠。我們知道,TCP或UDP伺服器會在眾所周知的埠上等待請求,客戶端嘗試連線這些固定埠,以獲得某種服務。在Portal RPC中,portal扮演著與IP埠類似的角色。

    在諸如mgs_setup()、mdt_start_ptlrpc_service()、ost_setup()、ldlm_setup()、等服務啟動函式中,除了呼叫ptlrpc_init_svc()函式外,還要呼叫ptlrpc_start_threads()函式,為服務建立執行緒池。執行緒池中的所有執行緒都各用一個型別為ptlrpc_thread的物件來描述,它們共用同樣的處理函式。

    我們需要注意到,一個服務啟動函式可能會建立多種服務。每個服務有其獨佔的請求portal和的執行緒池。例如,OST建立了執行緒名字首為ll_ost、ll_ost_creat和ll_ost_io的三個執行緒池,這些執行緒池分別使用了OST_REQUEST_PORTAL、OST_CREATE_PORTAL和OST_IO_PORTAL,不過它們使用了相同的回覆portal:OST_IO_PORTAL。

    在客戶端傳送請求時,為了讓請求能傳送到正確的服務,它小心地填充ptlrpc_request物件的rq_request_portal,使其值等於對應的ptlrpc_service服務物件的srv_req_portal欄位。

    服務端接收到客戶端發起的RPC請求後,會根據請求訊息的型別,呼叫對應的請求處理函式,並把處理結果以回覆訊息的方式發給客戶端。這符合經典的RPC呼叫流程。而在服務通過ptlrpc_send_reply()函式傳送回覆訊息時,也將從固定的回覆portal發出。

    伺服器之所以要建立多個不同的執行緒池,使用不同的portal,提供分立的服務,其主要原因是為了防止飢餓。假如使用統一執行緒池和portal,我們可以試想這樣一種情況:眾多客戶端進行頻繁大I/O,portal的被大量I/O請求訊息佔據,此時其他型別請求可能無法獲得portal的及時傳送和執行緒池的及時處理,從而出現大量請求處理超時。服務的分離避免了這一問題。同時,服務的分離也避免了在一些情況下可能出現的死鎖。在死鎖情況下,所有執行緒都在等待某些資源變成可用,雖然它們等待的資源可能不同,但結果是誰都無法處理新請求。

2.4      訊息的格式

    Portal RPC的每個訊息,不管是請求訊息還是回覆訊息,都由一個由型別為ptlrpc_request物件描述。這個物件包含了關於該訊息的所有資訊。Portal RPC的每個訊息對應著一個固定的訊息格式。訊息的格式由型別為req_format物件給出。每種訊息格式不僅包括了請求訊息的格式,而且包括了回覆訊息的格式。ptlrpc_request物件擁有關於型別為req_capsule的rq_pill欄位。這個物件不僅有一個指向req_format物件的rc_fmt欄位,還用rc_loc欄位來標識該訊息是請求訊息還是回覆訊息。

    Portal RPC所有可能用到的訊息格式都被定義在了全域性陣列req_formats中。陣列中的每個元素的型別都是req_format。所有req_capsule物件的rc_fmt欄位都指向這個陣列中的某個元素。

    req_format物件描述的不是一種訊息的格式,而是一對請求及其回覆訊息的格式。Portal RPC的每個請求訊息都有固定的格式,而對於每種格式的請求訊息,其回覆訊息的格式是固定的。req_format物件就描述了一對訊息的格式,它的rf_fields欄位是一個數組,其中每個元素就對應了一種訊息的格式,每個訊息格式由一個型別為req_msg_field的物件陣列來描述。

    請求訊息及其回覆訊息都被分成多個固定的域(field),每個域都對應了一個req_msg_field物件。每種訊息格式各自對應著某個型別為req_msg_field的物件陣列,這個陣列中的每個元素就對應著訊息的一個域。

    綜合上面的知識,我們可以知道,對於每個由ptlrpc_request物件描述的訊息,為了獲得它的格式,我們可以首先從ptlrpc_request物件的rq_pill欄位中獲得req_capsule物件,然後從req_capsule物件的rc_loc欄位獲知該訊息是請求訊息還是回覆訊息,隨後從req_format的rf_fields陣列中選取對應的元素,最後從這個元素獲得對各個域的描述。

    為了使得不同位元組序的節點間正常通訊,訊息的傳送是以“線上格式”進行的。在每個訊息終端,訊息都會被打包成線上格式,或者從線上格式解包。下圖以LDLM_CANCEL訊息為例,給出了上面所講到的各個物件之間的關係。

圖 訊息格式中各個物件的關係

1.5      最簡單訊息:ping訊息

ping訊息是Portal RPC最簡單和最基本的訊息。它的訊息格式是RQF_OBD_PING。這個訊息格式的請求和回覆都是ping訊息。Ping訊息只包含一個域,RMF_PTLRPC_BODY。實質上,Portal RPC發出的所有種類的訊息都以RMF_PTLRPC_BODY作為第一個域。

ping訊息由客戶端首先發出,服務端在接受到這種訊息之後,也立即回覆一個ping訊息。訊息包的發起者是一個被稱為pinger的核心執行緒。pinger的執行緒名被設定為ll_ping。它維護了一個包含所有入口端的連結串列pinger_imports。客戶端在嘗試與服務端進行網路連線時都將把入口端加入這個連結串列,這一事件發生在client_connect_import()函式呼叫ptlrpc_pinger_add_import()時。

pinger每個一段時間就輪詢一次這個連結串列,但不會在每次輪詢都向每個入口端傳送一個訊息。假如這樣做,在某個入口端非常忙碌的時候,將會給這個入口端帶來不必要的額外壓力。pinger只有在發現入口端未能以足夠頻率傳送請求的時候,才在該入口端傳送額外的ping訊息。

一個已連線好但長期保持空閒狀態的入口端,需要pinger的這種機制來維持連線的暢通。否則,可以想象,服務端將無法辨別客戶端是否已離線,因此為了確保資源不被離線客戶端永久佔用,伺服器能做的只有將之驅逐。在服務端上,負責這項任務的是一個名字為ll_evictor的執行緒。

我們應該注意到,對於那些未連線好的入口端,pinger傳送沒有必要傳送無用的ping訊息,而是直接傳送RQF_MDS_CONNECT格式的訊息。

綜上所述,pinger有兩種作用,一方面是客戶端用來嘗試與服務端網路連通的方法,一方面是客戶端確保在長期空閒時不被服務端驅逐的方法。

1.6      正在研究的課題:網路請求排程優化

網路請求排程(Network Request Scheduler,NRS)優化是一個研究在服務端對接收到的RPC進行重新排程的課題。這個課題早在2008年就已經在Lustre社群被提出來的,直到現在(2012年)仍然不斷地被研究。

網路請求排程優化基於一個簡單的事實,就是在伺服器端上,由於請求在接收到之後會在佇列中停留一段時間,因而呈現請求的接受和處理的兩階段現象。由於這一現象的存在,伺服器可以重新排程這些請求,以達到以下目的:

l 通過請求的重排序,向客戶端提供一致的效能,避免飢餓的出現。

l 通過請求的重排序,優化負載模式,使得後端檔案系統更容易優化效能。

l 平衡網路流量,提高吞吐率。

l 控制處理資源和網路資源的分配,提供檔案系統層的複製質量控制。

為了達到這些目標,可以實現以下策略:

l FIFO。這是現有的服務方式,當時需要包裝為一種網路請求排程策略。

l 基於物件的輪詢(Object-Based Round Robin,OBRR),即根據檔案的便宜量,按照屬於某個資料物件的方式將RPC組合成一組。這種策略可以通過減少磁碟尋道而提高吞吐率。

l  基於客戶端的輪詢(Client-Based Round Robin,CBRR),即根據請求所屬的客戶端,將RPC組合成一組。這種策略可以平衡網路流量。

l 基於程序ID的輪詢(PID-BasedRound Robin,PBRR),即根據發起I/O的應用的NID::PID,將RPC組合成一組。NID是Lustre標識節點的方法。與基於客戶端的輪詢策略類似,這種策略可以平衡網路流量。此外,該策略還可以利用到客戶端訪問的空間區域性性。

l 區分客戶端或使用者優先次序的策略。這種策略可以通過優先處理眾多負載中相對重要的那一部分,而提供一種服務質量控制方法。在這種策略中,可以通過採用不同的方法計算請求的重要性,從而實現不同的目標。

1.7      總結

Portal RPC是Lustre中極為重要的一個組成部分。它一方面主要是Lustre這個叢集檔案系統的通訊部件,一方面也可以作為各層之間非同步傳遞訊息的方式(參看ptlrpc_prep_fakereq()函式)。

Portal RPC的效能對Lustre的效能有著非常大的影響。因此,Lustre社群一方面通過研究網路請求排程,以嘗試優化I/O效能,一方面通過把ptlrpcd的單執行緒、單佇列的服務模式改成多執行緒、多佇列的服務模式,以減少對元資料效能的影響。對於後者,可以通過對比Lustre-1.8和Lustre-2.2的相關程式碼可以發現這些優化的痕跡。

本文儘量全面地介紹了Portal RPC的設計原理和實現細節。但是Portal RPC是如此複雜的一個軟體層,本文遠遠沒有設計到它的全部內容,其中包括網路重連、請求重發等異常處理。這將在以後的內容中加以介紹。


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