
檔案鎖
檔案鎖是檔案系統的最基本特性之一,應用程式借助檔案鎖可以控制其他應用對檔案的并發通路。NFS作為類UNIX系統的标準網絡檔案系統,在發展過程中逐漸地原生地支援了檔案鎖(從NFSv4開始)。NFS從上個世界80年代誕生至今,共釋出了3個版本:NFSv2、NFSv3、NFSv4。
NFSv4最大的變化是有“狀态”了。某些操作需要服務端維持相關狀态,如檔案鎖,例如用戶端申請了檔案鎖,服務端就需要維護該檔案鎖的狀态,否則和其他用戶端沖突的通路就無法檢測。如果是NFSv3就需要NLM協助才能實作檔案鎖功能,但是有的時候兩者配合不夠協調就會容易出錯。而NFSv4設計成了一種有狀态的協定,自身就可以實作檔案鎖功能,也就不需要NLM協定了。
應用接口
應用程式可以通過 fcntl() 或 flock() 系統調用管理NFS檔案鎖,下面是NAS使用NFSv4挂載時擷取檔案鎖的調用過程:
從上圖調用棧容易看出,NFS檔案鎖實作邏輯基本複用了VFS層設計和資料結構,在通過RPC從Server成功擷取檔案鎖後,調用 locks_lock_inode_wait() 函數将獲得的檔案鎖交給VFS層管理,關于VFS層檔案鎖設計的相關資料比較多,在此就不再贅述了。
EOS原理
檔案鎖是典型的非幂等操作,檔案鎖操作的重試和Failover會導緻檔案鎖狀态視圖在用戶端和服務端間的不一緻。NFSv4借助SeqId機制設計了最多執行一次的機制,具體方法如下:
針對每個open/lock狀态,Client和Server同時獨立維護seqid,Client在發起會引起狀态變化的操作時(open/close/lock/unlock/release_lockowner)會将seqid加1,并作為參數發送給Server,假定Client發送的seqid為R,Server維護的seqid為L,則:
- 若R == L +1,表示合法請求,正常處理之。
- 若R == L,表示重試請求,Server将緩存的reply傳回即可。
- 其他情況均為非法請求,決絕通路。
根據上述規則,Server可判斷操作是否為正常、重試或非法請求。
該方法能夠保證每個檔案鎖操作在服務端最多執行一次,解決了RPC重試帶來的重複執行的問題,但是僅靠這一點是不夠的。比如LOCK操作發送後調用線程被信号中斷,此後服務端又成功接受并執行了該LOCK操作,這樣服務端就記錄了用戶端持有了鎖,但用戶端中卻因為中斷而沒有維護這把鎖,于是就造成了用戶端和服務端間的鎖狀态視圖不一緻。是以,用戶端還需要配合處理異常場景,最終才能夠保證檔案鎖視圖一緻性。
異常處理
由上一節的分析可知,用戶端需要配合處理異常場景才能夠保證檔案視圖一緻性,那麼用戶端設計者主要做了哪些配合的設計呢?目前用戶端主要從SunRPC和NFS協定實作兩個次元互相配合解決該問題,下面分别介紹這兩個次元的設計如何保證檔案鎖狀态視圖一緻性。
SunRPC設計
SunRPC是Sun公司專門為遠端過程調用設計的網絡通訊協定,這裡從保障檔案鎖視圖一緻性的次元來了解一下SunRPC實作層面的設計理念:
(1)用戶端使用int32_t類型的xid辨別上層使用者發起的每個遠端過程調用過程,每個遠端過程調用的多次RPC重試使用相同的xid辨別,這樣就保障了多次RPC重試中任何一個傳回都可以告知上層遠端過程調用已經成功,保證了服務端執行遠端過程調用執行耗時較長時也能拿到結果,這一點和傳統的netty/mina/brpc等都需要每個RPC都要有獨立的xid/packetid不同。
(2)服務端設計了DRC(duplicate request cache)緩存最近執行的RPC結果,接收到RPC時會首先通過xid檢索DRC緩存,若命中則表明RPC為重試操作,直接傳回緩存的結果即可,這在一定程度上規避了RPC重試帶來的重複執行的問題。為了避免xid複用導緻DRC緩存傳回非預期的結果,開發者通過下述設計進一步有效地減少複用引起錯誤的機率:
- 用戶端建立新連結時初始xid采用随機值。
- 服務端DRC會額外記錄請求的校驗資訊,緩存命中時會同時校驗這些資訊。
(3)用戶端允許在獲得服務端響應前無限重試,保證調用者能夠獲得服務端确定性的執行結果,當然這樣的政策會導緻無響應時調用者會一直hang。
(4)NFS允許使用者在挂載時通過soft/hard參數指定SunRPC的重試政策,其中soft模式禁止逾時後重試,hard模式則持續重試。當使用者使用soft模式挂載時NFS實作不保證用戶端和服務端狀态視圖的一緻性,在遇到遠端過程調用傳回逾時要求應用程式配合狀态的清理和恢複,比如關閉通路出錯的檔案等,然而實踐中很少有應用程式會配合,是以一般情況下NAS使用者都使用hard模式挂載。
總之,SunRPC要解決的核心問題之一是,遠端過程調用執行時間是不可控的,協定設計者為此定制化設計,盡量避免非幂等操作RPC重試帶來的副作用。
信号中斷
應用程式等待遠端過程調用結果時允許被信号中斷。當發生信号中斷時,由于沒有得到遠端過程調用的執行結果,是以用戶端和服務端的狀态很可能就不一緻了,比如加鎖操作在服務端已經成功執行,但用戶端并不知道這個情況。這就要求用戶端做額外的工作将狀态和服務端恢複一緻。下面簡要分析擷取檔案鎖被信号中斷後的處理,來說明NFS協定實作層面的一緻性設計。
通過擷取NFSv4檔案鎖的過程可知,NFSv4擷取檔案鎖最終會調用 _nfs4_do_setlk() 函數發起RPC操作,最終調用 nfs4_wait_for_completion_rpc_task() 等待,下面是相關代碼:
static int _nfs4_do_setlk(struct nfs4_state *state, int cmd, struct file_lock *fl, int recovery_type)
{
......
task = rpc_run_task(&task_setup_data);
if (IS_ERR(task))
return PTR_ERR(task);
ret = nfs4_wait_for_completion_rpc_task(task);
if (ret == 0) {
ret = data->rpc_status;
if (ret)
nfs4_handle_setlk_error(data->server, data->lsp,
data->arg.new_lock_owner, ret);
} else
data->cancelled = 1;
......
}
通過分析 nfs4_wait_for_completion_rpc_task() 實作可知,當ret < 0時,表明擷取鎖過程被信号中斷,并使用 struct nfs4_lockdata 的 cancelled 成員記錄。繼續檢視rpc_task完成後釋放時的回調函數 nfs4_lock_release():
從上面紅色框中的代碼可知,nfs4_lock_release() 檢測到存在信号中斷時會調用 nfs4_do_unlck()函數嘗試将可能成功獲得檔案鎖釋放掉,注意此時沒有調用 nfs_free_seqid() 函數将持有的nfs_seqid釋放掉,這是為了:
- 保證訂正狀态過程中不會有使用者新發起的并發加鎖或者釋放鎖操作,簡化實作。
- 保證hard模式下UNLOCK操作隻會在LOCK操作傳回後才會發送,保障已經獲得鎖能夠被釋放掉。
用戶端通過上面的方法能夠有效地保證信号中斷後用戶端和服務端鎖狀态的最終一緻性,但也是在損失一部分可用性為代價的。
總結
檔案鎖是檔案系統原生支援的基礎特性,NAS作為共享的檔案系統要面臨用戶端和服務端鎖狀态視圖一緻性的問題,NFSv4.0在一定程度上解決了這個問題,當然,技術前進的腳步不會停止,NFS的更新疊代也就不會停止,未來的NFS将會有更多的期待。
最後
我們相信技術的力量,更相信擁有技術力量的人。我們期待存儲的未來,更期待與你一起創造未來。