NFS LOCALIO

概述

LOCALIO 輔助 RPC 協議允許 Linux NFS 客戶端和伺服器可靠地握手,以確定它們是否位於同一主機上。在 menuconfig 中選擇“NFS client and server support for LOCALIO auxiliary protocol”以在核心配置中啟用 CONFIG_NFS_LOCALIO(CONFIG_NFS_FS 和 CONFIG_NFSD 也必須啟用)。

一旦 NFS 客戶端和伺服器握手確定為“本地”,客戶端將繞過網路 RPC 協議進行讀、寫和提交操作。由於這種 XDR 和 RPC 繞過,這些操作將執行得更快。

LOCALIO 輔助協議的實現使用了與 NFS 流量相同的連線,遵循了 NFS ACL 協議擴充套件建立的模式。

LOCALIO 輔助協議是實現客戶端可靠發現其本地伺服器所必需的。在 LOCALIO 協議使用之前的一個私有實現中,曾嘗試基於 sockaddr 網路地址與所有本地網路介面進行脆弱的匹配。但與 LOCALIO 協議不同,基於 sockaddr 的匹配無法處理 iptables 或容器的使用。

本地客戶端和伺服器之間強大的握手只是開始,這種本地性實現的最終用例是客戶端能夠直接向伺服器開啟檔案併發出讀、寫和提交操作,而無需透過網路。要求是儘可能高效地執行這些迴環 NFS 操作,這對於容器用例(例如 Kubernetes)特別有用,在這些用例中,可以在伺服器本地執行 IO 作業。

LOCALIO 能夠繞過 XDR 和 RPC 進行讀、寫和提交操作所實現的效能優勢可能非常顯著,例如:

fio,directio,qd 為 8,16 個 libaio 執行緒,執行 20 秒
  • 使用 LOCALIO:4K 讀取:IOPS=979k, 頻寬=3825MiB/s (4011MB/s)(74.7GiB/20002msec) 4K 寫入:IOPS=165k, 頻寬=646MiB/s (678MB/s)(12.6GiB/20002msec) 128K 讀取:IOPS=402k, 頻寬=49.1GiB/s (52.7GB/s)(982GiB/20002msec) 128K 寫入:IOPS=11.5k, 頻寬=1433MiB/s (1503MB/s)(28.0GiB/20004msec)

  • 不使用 LOCALIO:4K 讀取:IOPS=79.2k, 頻寬=309MiB/s (324MB/s)(6188MiB/20003msec) 4K 寫入:IOPS=59.8k, 頻寬=234MiB/s (245MB/s)(4671MiB/20002msec) 128K 讀取:IOPS=33.9k, 頻寬=4234MiB/s (4440MB/s)(82.7GiB/20004msec) 128K 寫入:IOPS=11.5k, 頻寬=1434MiB/s (1504MB/s)(28.0GiB/20011msec)

fio,directio,qd 為 8,1 個 libaio 執行緒,執行 20 秒
  • 使用 LOCALIO:4K 讀取:IOPS=230k, 頻寬=898MiB/s (941MB/s)(17.5GiB/20001msec) 4K 寫入:IOPS=22.6k, 頻寬=88.3MiB/s (92.6MB/s)(1766MiB/20001msec) 128K 讀取:IOPS=38.8k, 頻寬=4855MiB/s (5091MB/s)(94.8GiB/20001msec) 128K 寫入:IOPS=11.4k, 頻寬=1428MiB/s (1497MB/s)(27.9GiB/20001msec)

  • 不使用 LOCALIO:4K 讀取:IOPS=77.1k, 頻寬=301MiB/s (316MB/s)(6022MiB/20001msec) 4K 寫入:IOPS=32.8k, 頻寬=128MiB/s (135MB/s)(2566MiB/20001msec) 128K 讀取:IOPS=24.4k, 頻寬=3050MiB/s (3198MB/s)(59.6GiB/20001msec) 128K 寫入:IOPS=11.4k, 頻寬=1430MiB/s (1500MB/s)(27.9GiB/20001msec)

常見問題

  1. LOCALIO 的用例有哪些?

    1. 當 NFS 客戶端和伺服器位於同一主機上的工作負載能夠實現改進的 IO 效能。特別是在執行容器化工作負載時,作業通常會發現自己與用於儲存的 knfsd 伺服器執行在同一主機上。

  2. LOCALIO 的要求是什麼?

    1. 儘可能繞過網路 RPC 協議。這包括繞過 XDR 和 RPC 進行開啟、讀取、寫入和提交操作。

    2. 允許客戶端和伺服器自主發現它們是否在彼此的本地執行,而無需對本地網路拓撲做出任何假設。

    3. 透過與相關名稱空間(例如網路、使用者、掛載)相容來支援容器的使用。

    4. 支援所有 NFS 版本。NFSv3 尤其重要,因為它在企業中廣泛使用,並且 pNFS flexfiles 利用它作為資料路徑。

  3. 為什麼 LOCALIO 在決定 NFS 客戶端和伺服器是否位於同一主機上時不直接比較 IP 地址或主機名?

    由於其中一個主要用例是容器化工作負載,我們不能假設客戶端和伺服器之間會共享 IP 地址。這就提出了對握手協議的要求,該協議需要透過與 NFS 流量相同的連線進行,以便識別客戶端和伺服器是否確實在同一主機上執行。握手使用一個透過網路傳送的秘密,如果客戶端和伺服器確實位於同一位置,雙方可以透過與共享核心記憶體中儲存的值進行比較來驗證該秘密。

  4. LOCALIO 能否改進 pNFS flexfiles?

    是的,LOCALIO 透過允許 pNFS flexfiles 利用 NFS 客戶端和伺服器的本地性來對其進行補充。將客戶端 IO 儘可能地發起在資料儲存的伺服器附近,這樣的策略自然會受益於 LOCALIO 提供的資料路徑最佳化。

  5. 為什麼不開發一個新的 pNFS 佈局來啟用 LOCALIO?

    可以開發一個新的 pNFS 佈局,但這樣做會將發現客戶端是否共置的責任放在伺服器上,以便決定分發佈局。更簡單的方法(如 LOCALIO 提供的那樣)更有價值,它允許 NFS 客戶端協商和利用本地性,而無需以更集中的方式進行更復雜的建模和發現。

  6. 讓客戶端在不使用 RPC 的情況下執行伺服器端檔案 OPEN 有何益處?這種益處是 pNFS 特有的嗎?

    無論是否使用 pNFS,避免在檔案開啟時使用 XDR 和 RPC 都有助於提高效能。特別是處理小檔案時,最好儘可能避免網路傳輸,否則可能會減少甚至抵消避免網路傳輸本身帶來的小檔案 I/O 優勢。鑑於 LOCALIO 的要求,目前讓客戶端在不使用 RPC 的情況下執行伺服器端檔案開啟的方法是理想的。如果將來需求發生變化,我們可以相應地進行調整。

  7. 為什麼 LOCALIO 僅支援 UNIX 認證 (AUTH_UNIX)?

    強認證通常與連線本身繫結。它透過建立一個由伺服器快取的上下文來工作,該上下文作為發現授權令牌的金鑰,然後該令牌可以傳遞給 rpc.mountd 來完成認證過程。另一方面,在 AUTH_UNIX 的情況下,透過網路傳遞的憑證直接用作向上呼叫 rpc.mountd 的金鑰。這簡化了認證過程,從而使 AUTH_UNIX 更易於支援。

  8. 將 RPC 使用者 ID 轉換的匯出選項(例如 root_squash, all_squash)在 LOCALIO 操作中如何表現?

    翻譯使用者 ID 的匯出選項由 nfsd_setuser() 管理,該函式由 nfsd_setuser_and_check_port() 呼叫,而 nfsd_setuser_and_check_port() 又由 __fh_verify() 呼叫。因此,它們對 LOCALIO 的處理方式與非 LOCALIO 完全相同。

  9. 鑑於 NFSD 和 NFS 在不同的上下文中執行,LOCALIO 如何確保物件生命週期得到妥善管理?

    請參閱下面的詳細“NFS 客戶端和伺服器互鎖”部分。

RPC

LOCALIO 輔助 RPC 協議包含一個單獨的“UUID_IS_LOCAL”RPC 方法,該方法允許 Linux NFS 客戶端驗證本地 Linux NFS 伺服器是否能夠看到客戶端生成並放入 nfs_common 中的 nonce(一次性 UUID)。此協議並非 IETF 標準的一部分,也無需是,因為它是一個 Linux 到 Linux 的輔助 RPC 協議,屬於實現細節。

UUID_IS_LOCAL 方法根據固定的 UUID_SIZE(16 位元組)編碼客戶端生成的 uuid_t。使用固定大小的 opaque 編碼和解碼 XDR 方法,而不是效率較低的變長方法。

NFS_LOCALIO_PROGRAM 的 RPC 程式號是 400122(由 IANA 分配,參見 https://www.iana.org/assignments/rpc-program-numbers/):Linux Kernel Organization 400122 nfslocalio

rpcgen 語法中的 LOCALIO 協議規範是

/* raw RFC 9562 UUID */
#define UUID_SIZE 16
typedef u8 uuid_t<UUID_SIZE>;

program NFS_LOCALIO_PROGRAM {
    version LOCALIO_V1 {
        void
            NULL(void) = 0;

        void
            UUID_IS_LOCAL(uuid_t) = 1;
    } = 1;
} = 400122;

LOCALIO 使用與 NFS 流量相同的傳輸連線。因此,LOCALIO 未在 rpcbind 中註冊。

NFS Common 和客戶端/伺服器握手

fs/nfs_common/nfslocalio.c 提供了介面,使 NFS 客戶端能夠生成一個 nonce(一次性 UUID)及相關的短生命週期 nfs_uuid_t 結構體,並將其註冊到 nfs_common 中,以便 NFS 伺服器後續查詢和驗證,如果匹配,NFS 伺服器會填充 nfs_uuid_t 結構體中的成員。然後 NFS 客戶端使用 nfs_common 將 nfs_uuid_t 從其 nfs_uuids 列表傳輸到 nfs_common 的 uuids_list 中的 nn->nfsd_serv clients_list。參見:fs/nfs/localio.c:nfs_local_probe()

nfs_common 的 nfs_uuids 列表是啟用 LOCALIO 的基礎,因此它具有指向 nfsd 記憶體的成員,供客戶端直接使用(例如,'net' 是伺服器的網路名稱空間,透過它客戶端可以以適當的 RCU 讀取訪問許可權訪問 nn->nfsd_serv)。正是這種客戶端和伺服器的同步,使得高階用法和物件生命週期能夠從宿主核心的 nfsd 擴充套件到連線到在同一本地主機上執行的 nfs 客戶端的每個容器 knfsd 例項。

NFS 客戶端和伺服器互鎖

LOCALIO 提供 nfs_uuid_t 物件和相關介面,以實現正確的網路名稱空間 (net-ns) 和 NFSD 物件引用計數。

LOCALIO 需要引入和使用 NFSD 的 percpu nfsd_net_ref 來互鎖 nfsd_shutdown_net() 和 nfsd_open_local_fh(),以確保每個 net-ns 在被 nfsd_open_local_fh() 使用時不會被銷燬,這需要更詳細的解釋。

nfsd_open_local_fh() 在開啟其 nfsd_file 控制代碼之前使用 nfsd_net_try_get(),然後呼叫方(NFS 客戶端)在完成 IO 後必須使用 nfsd_file_put_local() 釋放 nfsd_file 和相關 net-ns 的引用。

這種互鎖工作在很大程度上依賴於 nfsd_open_local_fh() 能夠安全地處理 NFSD 的 net-ns(以及關聯的 nfsd_net)可能已被 nfsd_destroy_serv() 透過 nfsd_shutdown_net() 銷燬的情況。

這種 NFS 客戶端和伺服器的互鎖已被驗證可以修復一個容易發生的崩潰問題,即當容器中執行的 NFSD 例項在掛載 LOCALIO 客戶端的情況下關閉時會發生崩潰。在容器和相關 NFSD 重啟後,客戶端會因為 LOCALIO 客戶端嘗試呼叫 nfsd_open_local_fh() 而沒有正確引用 NFSD 的 net-ns,導致空指標解引用而崩潰。

NFS 客戶端而非伺服器發出 IO

由於 LOCALIO 專注於協議繞過以實現改進的 IO 效能,因此必須提供替代傳統 NFS 網路協議(帶 XDR 的 SUNRPC)的方式來訪問後端檔案系統。

請參閱 fs/nfs/localio.c:nfs_local_open_fh() 和 fs/nfsd/localio.c:nfsd_open_local_fh(),瞭解該介面如何集中使用選定的 nfs 伺服器物件,以允許位於伺服器本地的客戶端開啟檔案指標而無需透過網路。

客戶端的 fs/nfs/localio.c:nfs_local_open_fh() 將呼叫伺服器的 fs/nfsd/localio.c:nfsd_open_local_fh(),並謹慎地透過 RCU 訪問相關的 nfsd 網路名稱空間和 nn->nfsd_serv。如果 nfsd_open_local_fh() 發現客戶端不再看到有效的 nfsd 物件(無論是 struct net 還是 nn->nfsd_serv),它將向 nfs_local_open_fh() 返回 -ENXIO,客戶端將嘗試透過再次呼叫 nfs_local_probe() 來重新建立所需的 LOCALIO 資源。當容器中執行的 nfsd 例項在 LOCALIO 客戶端連線到它時重啟,就需要這種恢復。

一旦客戶端獲得一個開啟的 nfsd_file 指標,它將直接對底層本地檔案系統(通常由 nfs 伺服器完成)發出讀、寫和提交操作。因此,對於這些操作,NFS 客戶端正在對其與 NFS 伺服器共享的底層本地檔案系統發出 IO。參見:fs/nfs/localio.c:nfs_local_doio() 和 fs/nfs/localio.c:nfs_local_commit()。

在傳統使用 RPC 向伺服器發出 IO 的 NFS 中,如果應用程式使用 O_DIRECT,NFS 客戶端將繞過頁快取,但 NFS 伺服器不會。NFS 伺服器使用緩衝 IO,使得應用程式在向 NFS 客戶端發出 IO 時對齊要求不那麼嚴格。但如果所有應用程式都能正確對齊其 IO,LOCALIO 可以透過將 'localio_O_DIRECT_semantics' nfs 模組引數設定為 Y 來配置,以使用從 NFS 客戶端到它與 NFS 伺服器共享的底層本地檔案系統的端到端 O_DIRECT 語義,例如:

echo Y > /sys/module/nfs/parameters/localio_O_DIRECT_semantics

啟用後,它將使 LOCALIO 使用端到端 O_DIRECT 語義(但請注意,如果應用程式未能正確對齊其 IO,這可能會導致 IO 失敗)。

安全性

LOCALIO 僅在使用 UNIX 風格認證(AUTH_UNIX,也稱 AUTH_SYS)時受支援。

無論使用 LOCALIO 還是常規 NFS 訪問,都會注意確保使用相同的 NFS 安全機制(認證等)。作為傳統 NFS 客戶端訪問 NFS 伺服器的一部分建立的 auth_domain 也用於 LOCALIO。

相對於容器,LOCALIO 允許客戶端訪問伺服器的網路名稱空間。這是允許客戶端訪問伺服器的每個名稱空間 nfsd_net 結構所必需的。對於傳統 NFS,客戶端也享有同等訪問許可權(儘管是透過 SUNRPC 的 NFS 協議)。沒有其他名稱空間(使用者、掛載等)從伺服器到客戶端被更改或特意擴充套件。

模組引數

/sys/module/nfs/parameters/localio_enabled (布林值) 控制 LOCALIO 是否啟用,預設為 Y。如果客戶端和伺服器是本地的,但‘localio_enabled’設定為 N,則 LOCALIO 將不會被使用。

/sys/module/nfs/parameters/localio_O_DIRECT_semantics (布林值) 控制 O_DIRECT 是否擴充套件到底層檔案系統,預設為 N。應用程式 IO 必須與邏輯塊大小對齊,否則 O_DIRECT 將失敗。

/sys/module/nfsv3/parameters/nfs3_localio_probe_throttle (無符號整型) 控制 NFSv3 讀寫 IO 是否每 N (nfs3_localio_probe_throttle) 次 IO 觸發 LOCALIO 的(重新)啟用,預設為 0(停用)。必須是 2 的冪次方,如果配置錯誤(值過低或非 2 的冪次方),管理員將承擔所有後果。

測試

LOCALIO 輔助協議及相關的 NFS LOCALIO 讀、寫和提交訪問已在各種測試場景中證明穩定

  • 客戶端和伺服器都在同一主機上。

  • 本地和遠端客戶端和伺服器的所有客戶端和伺服器支援啟用組合。

  • 還對不支援 LOCALIO 協議的 NFS 儲存產品進行了測試。

  • 主機上的客戶端,容器內的伺服器(適用於 v3 和 v4.2)。容器測試是針對 podman 管理的容器進行的,幷包括成功的容器停止/重啟場景。

  • 將這些測試場景在現有測試基礎設施中正式化正在進行中。初步的定期覆蓋是透過 ktest 針對啟用 LOCALIO 的 NFS 環回掛載配置執行 xfstests 提供,幷包括 lockdep 和 KASAN 覆蓋,參見:https://evilpiepirate.org/~testdashboard/ci?user=snitzer&branch=snitm-nfs-next https://github.com/koverstreet/ktest

  • 已進行了各種 kdevops 測試(透過“Chuck's BuildBot”)以定期驗證 LOCALIO 更改未對非 LOCALIO NFS 用例造成任何迴歸。

  • 啟用 LOCALIO 後,Hammerspace 的各種健全性測試均透過(這包括大量的 pNFS 和 flexfiles 測試)。