JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

RDMA 與奔向微秒的競賽

在資料中心裡,過去以毫秒衡量延遲的軟體,如今開始計較微秒。這篇帶你看清那些微秒躲在哪裡、為什麼作業系統核心是瓶頸,以及 RDMA 如何讓一台機器幾乎像存取本機記憶體一樣讀寫另一台機器的記憶體。

那些毫秒到哪去了?

走到這個階段,你已經看過資料中心網路的特別之處:規模龐大、流量大多是伺服器之間的東西向流量、用胖樹與葉脊架構撐起高對分頻寬,還有像 DCTCP 這種為極短來回時間調校的壅塞控制。現在我們問一個更尖銳的問題:當兩台伺服器在同一張網路上相隔幾公尺,一台到底能多快把資料交給另一台?在公開網際網路上,一次來回是數十毫秒,省一微秒毫無意義。可是在資料中心裡,實體來回可能只有區區幾微秒,於是軟體每一微秒的額外開銷,瞬間就成了整場比賽的勝負關鍵。

這裡值得記起第一個階段就學過的一條規則:頻寬、吞吐量與延遲是三件不同的事,而頻寬變大並不會縮短延遲。一條 100 Gbps 的連線每秒搬更多位元,卻不會讓第一個位元更早抵達。奔向微秒的競賽是一場延遲競賽,而你打不過光速:光在光纖裡橫越資料中心一條一百公尺的走道,遠遠不到一微秒,所以線路幾乎從來不是問題。問題出在兩端的位元在上線之前與下線之後,必須穿過的所有東西。

所以在伸手去拿花俏的解法之前,我們先誠實面對時間花在哪裡。想像用普通的 TCP 送一則小訊息:你的應用程式呼叫進作業系統,資料被複製進核心緩衝區,核心組出區段與 IP 標頭、交給驅動程式,網路卡把它送出去;在另一端,一個硬體中斷喚醒核心,資料又被複製進一個核心緩衝區,接收端的應用程式從睡眠中被叫醒、再把資料複製進自己的記憶體。每一步都很便宜,但加總起來每則訊息要好幾微秒,遠遠蓋過不到一微秒的線路時間。

核心就是那座收費站

把作業系統核心想成一座收費站,每個位元組進出時都得在那裡停下來。它的存在自有道理:它保護一個程式不受另一個程式干擾、把一張網路卡多工分給許多應用程式,還維持那個讓 TCP 如此好用的可靠且有序的串流。但每一次通過收費站,都要付出一次情境切換(CPU 停下你的程式碼、改跑核心程式碼)、一次或多次記憶體複製,外加 TCP 的記帳工作。對於大檔案傳輸,這點開銷只是進位誤差;可是對於一個每秒回答上百萬個微小請求的鍵值儲存來說,這座收費站就是塞車本身。

ordinary TCP send/receive (each '|' is time at a CPU)

  sender                              receiver
  app: write() ---> kernel copy
       build headers
       NIC sends  =====[ ~0.5 us wire ]=====> NIC, interrupt
                                              kernel copy
                                              wake app, copy
                                              app: read()

  several us of CPU work  >>  the ~0.5 us on the wire
在普通的 TCP 裡,兩端的 CPU 工作才是主角;線路反而是最快的一段。

縮小這座收費站有兩條互補的路。第一條是核心繞過(kernel bypass):讓應用程式幾乎直接和網路卡對話,把網路卡的佇列對映進程式自己的記憶體,於是送一個封包只是往一個網路卡已經在盯著的環狀緩衝區寫資料,不需要系統呼叫、也不需要核心複製。第二條是把工作往硬體推:網路卡自己處理標頭、檢查碼,甚至可靠性,CPU 幾乎不必插手。RDMA 這個我們的目的地兩者兼具,但同樣的直覺早已驅動使用者空間的封包框架,以及現代 SmartNIC 上的卸載引擎。

RDMA:讀另一台機器的記憶體

遠端直接記憶體存取(Remote Direct Memory Access,即 RDMA)把繞過的想法推到邏輯上的終點。在一台電腦內部,裝置本來就能用直接記憶體存取去讀寫主記憶體、不必勞動 CPU。RDMA 把這件事延伸到網路上:一張特製的網路卡,讓一台伺服器把資料直接放進另一台伺服器記憶體裡的某塊區域、或從中讀出,而遠端的 CPU 完全不碰那則訊息,往往甚至毫無察覺。核心只在事前一次設定好權限;在那之後,資料路徑就是純硬體。收費站從快速路徑上徹底消失了。

  1. 註冊記憶體:兩邊各自請自己的 RDMA 卡把一塊緩衝區釘在實體記憶體裡,並交回一把命名該區域的金鑰。「釘住」很重要,因為硬體稍後會直接伸進那塊記憶體,不會再問作業系統一次。
  2. 交換金鑰:兩邊透過一條普通連線,把記憶體位址與金鑰交換一次。這是唯一緩慢、需要核心參與的步驟,而且發生在熱迴圈之前。
  3. 投遞請求:要傳送時,應用程式把一個小小的工作請求寫進一個對映到自己記憶體的佇列裡,指明本機緩衝區、遠端位址與金鑰。完全不需要系統呼叫。
  4. 其餘交給網路卡:本機網路卡讀出緩衝區、把它送過網路,遠端網路卡直接寫進指定的記憶體。對於單邊寫入,遠端 CPU 完全不會被打斷;資料就這麼出現了。
  5. 完成通知:網路卡把一張紙條丟進一個完成佇列,讓寄件者知道傳輸結束了。應用程式輪詢那個佇列、而不是去睡覺,這正是延遲能降到幾微秒的原因。

有兩種操作風味值得分清楚。雙邊操作(RDMA send 與 receive)仍然像在送訊息:接收端必須事先投遞一塊緩衝區來接住它,很像普通的通訊端。單邊操作(RDMA read 與 write)才是那招魔術:發起方直接指名一個遠端位址,遠端應用程式完全不參與。單邊寫入正是分散式資料庫能更新對等節點記憶體、或儲存系統能取回遠端頁面的方式——在短短幾微秒內完成,把遠端 CPU 空出來做真正的工作。

RoCE:在你既有的乙太網路上跑 RDMA

RDMA 誕生於 InfiniBand,那是一張獨立、為此目的打造的網路,有自己的纜線與交換器。如果你負擔得起第二張網路,那很美好;但多數雲端業者想在跑所有其他東西的同一張乙太網路上跑 RDMA。答案是 RoCE(RDMA over Converged Ethernet),它把 RDMA 訊息包進普通的乙太網路訊框與 IP 封包裡,於是它們就騎在你早已建好的葉脊網路上。這個承諾是真的,但它附帶一個尖銳的條件,而這條件直接連回上一篇。

問題就在這裡。最早的 RoCE 硬體假設一張無損網路:只要線路從不丟封包,網路卡那套簡單的可靠性邏輯就能維持又快又便宜。但普通乙太網路在緩衝區溢位時確實會丟封包,正是你先前遇過的 TCP incast微爆量情況。於是 RoCE 倚賴兩個你已經認識的工具。優先權流量控制(Priority Flow Control)讓交換器告訴上游連接埠短暫暫停、而不是直接丟棄;ECN則在佇列開始堆積時標記封包,讓寄件者在任何東西丟失之前先放慢。這正是 DCTCP 背後那套相同的壅塞訊號直覺,如今用來讓一張 RDMA 網路永遠不必重傳。

什麼時候微秒值得追(什麼時候不值得)

我們很容易以為越快越好,但 RDMA 只在特定形狀的工作上才划得來。分離式儲存、分散式資料庫、需要上千張 GPU 每一步都交換梯度的大型機器學習訓練,以及記憶體內鍵值儲存,都有一個共同點:大量微小的東西向傳輸,而定速度的是核心開銷、不是資料量。在那裡把一則訊息從十微秒砍到兩微秒,可能讓整個叢集每秒多出數百萬次操作。

對於大多數其他流量,純 TCP 才是正確而誠實的選擇。一台串流影片的網頁伺服器、一場整夜複製數 TB 的備份、一個橫越公開網際網路而來回本就是毫秒級的請求:在這些情況裡,核心的過路費根本看不見,而 TCP 無所不至的觸及、壅塞公平性,以及數十年的千錘百鍊,遠比省下一個你永遠感覺不到的微秒重要。也別忘了,和 TCP 一樣,RDMA 本身不加密任何東西;機密性是另一層的事,就像 TLS 坐在 TCP 之上。真正的本事不在於永遠抓最快的工具,而在於認清你實際上在跟哪一個瓶頸搏鬥。