RDS

概述

此自述檔案嘗試提供有關 RDS 的原理和原因的一些背景資訊,並有望幫助您瞭解程式碼。

此外,請參閱此關於 RDS 起源的電子郵件:http://oss.oracle.com/pipermail/rds-devel/2007-November/000228.html

RDS 架構

RDS 透過在叢集中任意兩個節點之間使用單個可靠連線來提供可靠的、有序的資料報傳遞。 這允許應用程式使用單個套接字與叢集中的任何其他程序通訊 - 因此在具有 N 個程序的叢集中,您需要 N 個套接字,與使用面向連線的套接字傳輸(如 TCP)的 N*N 相比。

RDS 並非特定於 Infiniband;它旨在支援不同的傳輸。 當前的實現用於支援透過 TCP 以及 IB 的 RDS。

從應用程式的角度來看,RDS 的高階語義是

  • 定址

    RDS 使用 IPv4 地址和 16 位埠號來標識連線的端點。 所有涉及在核心和使用者空間之間傳遞地址的套接字操作通常都使用 struct sockaddr_in。

    使用 IPv4 地址的事實並不意味著底層傳輸必須基於 IP。 事實上,IB 上的 RDS 使用可靠的 IB 連線; IP 地址專門用於定位遠端節點的 GID(透過 ARPing 給定的 IP)。

    埠空間完全獨立於 UDP、TCP 或任何其他協議。

  • 套接字介面

    RDS 套接字的工作方式大部分如您所期望的 BSD 套接字。 下一節將介紹詳細資訊。 無論如何,所有 I/O 都透過標準的 BSD 套接字 API 執行。 一些新增,如零複製支援,透過控制訊息實現,而其他擴充套件使用 getsockopt/setsockopt 呼叫。

    套接字必須先繫結,然後才能傳送或接收資料。 這是必需的,因為繫結還會選擇傳輸並將其附加到套接字。 繫結後,傳輸分配不會更改。 RDS 將容忍 IP 地址移動(例如,在主動-主動 HA 場景中),但前提是地址不會移動到不同的傳輸。

  • sysctl

    RDS 在 /proc/sys/net/rds 中支援多個 sysctl

套接字介面

AF_RDS、PF_RDS、SOL_RDS

AF_RDS 和 PF_RDS 是與 socket(2) 一起用於建立 RDS 套接字的域型別。 SOL_RDS 是與 setsockopt(2) 和 getsockopt(2) 一起用於 RDS 特定套接字選項的套接字級別。

fd = socket(PF_RDS, SOCK_SEQPACKET, 0);

這將建立一個新的、未繫結的 RDS 套接字。

setsockopt(SOL_SOCKET):傳送和接收緩衝區大小

RDS 遵循傳送和接收緩衝區大小套接字選項。 您不允許將超過 SO_SNDSIZE 位元組的資料排隊到套接字。 訊息在呼叫 sendmsg 時排隊,並在遠端系統確認其到達時離開佇列。

SO_RCVSIZE 選項控制最大接收佇列長度。 這是一個軟限制,而不是硬限制 - RDS 將繼續接受和排隊傳入訊息,即使這會使佇列長度超過限制。 但是,它還會將埠標記為“擁塞”,並將擁塞更新發送到源節點。 源節點應該限制任何傳送到此擁塞埠的程序。

bind(fd, &sockaddr_in, ...)

這將套接字繫結到本地 IP 地址和埠以及傳輸,如果尚未透過 SO_RDS_TRANSPORT 套接字選項選擇傳輸

sendmsg(fd, ...)

將訊息傳送到指示的接收者。 如果底層可靠連線尚未建立,核心將透明地建立該連線。

嘗試傳送超過 SO_SNDSIZE 的訊息將返回 -EMSGSIZE

嘗試傳送將使排隊的總位元組數超過 SO_SNDSIZE 閾值的訊息將返回 EAGAIN。

嘗試將訊息傳送到標記為“擁塞”的目的地將返回 ENOBUFS。

recvmsg(fd, ...)

接收排隊到此套接字的訊息。 套接字的 recv 佇列記帳已調整,如果佇列長度降至 SO_SNDSIZE 以下,則該埠被標記為未擁塞,並且擁塞更新將傳送到所有對等方。

應用程式可以要求 RDS 核心模組透過控制訊息接收通知(例如,當擁塞更新到達或 RDMA 操作完成時,會有通知)。 這些通知透過 struct msghdr 的 msg.msg_control 緩衝區接收。 訊息的格式在手冊頁中描述。

poll(fd)

RDS 支援 poll 介面,允許應用程式實現非同步 I/O。

POLLIN 處理非常簡單。 當有傳入訊息排隊到套接字或有掛起的通知時,我們會發出 POLLIN 訊號。

POLLOUT 有點困難。 由於您基本上可以傳送到任何目的地,因此只要傳送佇列上有空間(即排隊的位元組數小於 sendbuf 大小),RDS 將始終發出 POLLOUT 訊號。

但是,核心將拒絕接受傳送到標記為擁塞的目的地的訊息 - 在這種情況下,如果您依賴 poll 告訴您該怎麼做,您將永遠迴圈。 這不是一個微不足道的問題,但應用程式可以透過使用擁塞通知以及檢查 sendmsg 返回的 ENOBUFS 錯誤來解決此問題。

setsockopt(SOL_RDS, RDS_CANCEL_SENT_TO, &sockaddr_in)

這允許應用程式放棄此特定套接字上排隊到特定目的地的所有訊息。

如果應用程式檢測到超時,則允許應用程式取消未完成的訊息。 例如,如果它嘗試傳送一條訊息,但遠端主機無法訪問,RDS 將永遠嘗試。 應用程式可能會認為不值得,並取消該操作。 在這種情況下,它將使用 RDS_CANCEL_SENT_TO 來清除任何掛起的訊息。

setsockopt(fd, SOL_RDS, SO_RDS_TRANSPORT, (int *)&transport ..), getsockopt(fd, SOL_RDS, SO_RDS_TRANSPORT, (int *)&transport ..)

設定或讀取一個整數,該整數定義要用於套接字上 RDS 資料包的底層封裝傳輸。 設定選項時,整數引數可以是 RDS_TRANS_TCP 或 RDS_TRANS_IB 之一。 檢索值時,將在未繫結的套接字上返回 RDS_TRANS_NONE。 此套接字選項只能在套接字上設定一次,即透過 bind(2) 系統呼叫繫結它之前。 嘗試在先前已顯式(透過 SO_RDS_TRANSPORT)或隱式(透過 bind(2))附加傳輸的套接字上設定 SO_RDS_TRANSPORT 將返回 EOPNOTSUPP 錯誤。 嘗試將 SO_RDS_TRANSPORT 設定為 RDS_TRANS_NONE 將始終返回 EINVAL。

RDS 的 RDMA

請參閱 rds-rdma(7) 手冊頁(可在 rds-tools 中找到)

擁塞通知

請參閱 rds(7) 手冊頁

RDS 協議

訊息頭

訊息頭是一個“struct rds_header”(請參閱 rds.h)

欄位

h_sequence

每個資料包的序列號

h_ack

收到的最後一個數據包的捎帶確認

h_len

資料長度,不包括標頭

h_sport

源埠

h_dport

目標埠

h_flags

可以是

CONG_BITMAP

這是一個擁塞更新點陣圖

ACK_REQUIRED

接收者必須確認此資料包

RETRANSMITTED

資料包以前已傳送

h_credit

向連線的另一端指示它有更多可用信用(即,有更多的傳送空間)

h_padding[4]

未使用,供將來使用

h_csum

標頭校驗和

h_exthdr

可選資料可以在此處傳遞。 目前用於傳遞與 RDMA 相關的資訊。

ACK 和重傳處理

人們可能會認為,使用可靠的 IB 連線,您不需要確認已收到的訊息。 問題是 IB 硬體在將訊息 DMA 到記憶體之前會生成 ACK 訊息。 如果 HCA 在傳送 ACK 和 DMA 並處理訊息之間因任何原因被停用,這會造成潛在的訊息丟失。 僅當另一個 HCA 可用於故障轉移時,這才是潛在問題。

立即傳送 ACK 將允許傳送方快速從其傳送佇列中釋放已傳送的訊息,但可能會導致過多的流量用於 ACK。 RDS 在傳送的資料包上捎帶 ACK。 透過僅允許一次傳送一個 ACK-only 資料包,以及僅當傳送方的傳送緩衝區開始填滿時,傳送方才請求 ACK,從而減少了 ACK-only 資料包。 所有重傳也會被確認。

流量控制

RDS 的 IB 傳輸使用基於信用的機制來驗證對等方的接收緩衝區中是否有空間用於更多資料。 這消除了連線上硬體重試的需要。

擁塞

接收套接字上接收佇列中等待的訊息會根據套接字的 SO_RCVBUF 選項值進行計算。 僅計算訊息中的有效負載位元組。 如果排隊的位元組數等於或超過 rcvbuf,則套接字將擁塞。 嘗試傳送到此套接字地址的所有傳送都應返回阻塞或返回 -EWOULDBLOCK。

期望應用程式進行合理調整,以便這種情況很少發生。 遇到這種“反壓”的應用程式被認為是 bug。

這是透過讓每個節點維護點陣圖來實現的,該點陣圖指示繫結地址上的哪些埠已擁塞。 隨著點陣圖的變化,它會透過所有以已更改的點陣圖的本地地址終止的連線傳送。

點陣圖在連線啟動時分配。 這避免了在中斷處理路徑中分配,該路徑在套接字上排隊訊息。 密集的點陣圖使傳輸可以在任何點陣圖更改時合理有效地傳送整個點陣圖。 這比某些更細粒度的每埠擁塞通訊更容易實現。 傳送方執行非常便宜的位測試,以測試它即將傳送到的埠是否擁塞。

RDS 傳輸層

如上所述,RDS 並非特定於 IB。 它的程式碼分為通用 RDS 層和傳輸層。

通用層處理套接字 API、擁塞處理、環回、統計資訊、usermem 鎖定和連線狀態機。

傳輸層處理傳輸的詳細資訊。 例如,IB 傳輸處理所有佇列對、工作請求、CM 事件處理程式和其他 Infiniband 詳細資訊。

RDS 核心結構

struct rds_message

也可能稱為“rds_outgoing”,通用 RDS 層複製要傳送的資料,並根據套接字 API 需要設定標頭欄位。 然後,將其排隊到單個連線,並由連線的傳輸傳送。

struct rds_incoming

一個通用結構,引用可以從傳輸傳遞到通用程式碼並透過通用程式碼排隊的傳入資料,同時喚醒套接字。 然後,將其傳遞迴傳輸程式碼以處理實際的複製到使用者。

struct rds_socket

每個套接字的資訊

struct rds_connection

每個連線的資訊

struct rds_transport

指向傳輸特定函式的指標

struct rds_statistics

非傳輸特定統計資訊

struct rds_cong_map

包裝原始擁塞點陣圖,包含 rbnode、waitq 等。

連線管理

連線可能處於 UP、DOWN、CONNECTING、DISCONNECTING 和 ERROR 狀態。

RDS 套接字首次嘗試將資料傳送到節點時,會分配並連線一個連線。 然後,該連線將永久維護 - 如果存在傳輸錯誤,則該連線將被刪除並重新建立。

刪除連線時,如果資料包已排隊,則當連線重新建立時,將重新傳輸排隊或部分發送的資料報。

傳送路徑

rds_sendmsg()
  • struct rds_message 從傳入資料構建

  • 解析 CMSGs(例如,RDMA 操作)

  • 傳輸連線已分配並連線(如果尚未)

  • rds_message 放置在傳送佇列中

  • 喚醒傳送 worker

rds_send_worker()
  • 呼叫 rds_send_xmit() 直到佇列為空

rds_send_xmit()
  • 如果有一個掛起,則傳輸擁塞圖

  • 可能會設定 ACK_REQUIRED

  • 呼叫傳輸以傳送非 RDMA 或 RDMA 訊息(RDMA 操作永遠不會重傳)

rds_ib_xmit()
  • 從傳送環中分配工作請求

  • 將任何可用的新發送信用新增到對等方 (h_credits)

  • 對映 rds_message 的 sg 列表

  • 捎帶確認

  • 填充工作請求

  • 將傳送釋出到連線的佇列對

接收路徑

rds_ib_recv_cq_comp_handler()
  • 檢視寫入完成

  • 從裝置取消對映接收緩衝區

  • 沒有錯誤,呼叫 rds_ib_process_recv()

  • 重新填充接收環

rds_ib_process_recv()
  • 驗證標頭校驗和

  • 如果是一個新資料報的開始,則將標頭複製到 rds_ib_incoming 結構

  • 新增到 ibinc 的 fraglist

  • 如果是已完成的資料報
    • 如果資料報是擁塞更新,則更新 cong 圖

    • 否則呼叫 rds_recv_incoming()

    • 注意是否需要 ACK

rds_recv_incoming()
  • 刪除重複資料包

  • 響應 ping

  • 查詢與此資料報關聯的套接字

  • 新增到套接字佇列

  • 喚醒套接字

  • 執行一些擁塞計算

rds_recvmsg
  • 將資料複製到使用者 iovec

  • 處理 CMSGs

  • 返回到應用程式

多路徑 RDS (mprds)

Mprds 是多路徑 RDS,主要用於 RDS-over-TCP(儘管該概念可以擴充套件到其他傳輸)。 RDS-over-TCP 的經典實現是透過在任意 2 個端點之間(其中端點 == [IP 地址、埠])透過 2 個 IP 地址之間透過單個 TCP 套接字來解複用多個 PF_RDS 套接字來實現的。 這具有以下限制:它最終會在單個 TCP 流上漏斗多個 RDS 流,因此 (a) 上限為單流頻寬,(b) 會受到所有 RDS 套接字中的隊首阻塞的影響。

透過每個 rds/tcp 連線具有多個 TCP/IP 流(即多路徑 RDS (mprds)),可以實現更好的吞吐量(對於固定的較小資料包大小,MTU)。 每個這樣的 TCP/IP 流構成 rds/tcp 連線的路徑。 RDS 套接字將基於某些雜湊(例如,本地地址和 RDS 埠號)附加到路徑,並且該 RDS 套接字的資料包將透過 TCP 在附加路徑上傳送,以在該路徑上分段/重新組裝 RDS 資料報。

多路徑 RDS 是透過將 struct rds_connection 拆分為一個公共(所有路徑)部分和一個每個路徑的 struct rds_conn_path 來實現的。 所有 I/O 工作佇列和重新連線執行緒都由 rds_conn_path 驅動。 然後,具有多路徑能力的傳輸(如 TCP)可以為每個 rds_conn_path 設定一個 TCP 套接字,這由傳輸透過傳輸私有 cp_transport_data 指標進行管理。

傳輸透過在向 rds 核心模組註冊期間設定 t_mp_capable 位來宣告自己具有多路徑能力。 當傳輸具有多路徑能力時,rds_sendmsg() 會在多個路徑上對傳出流量進行雜湊處理。 傳出雜湊是基於 PF_RDS 套接字繫結到的本地地址和埠計算的。

此外,即使傳輸具有 MP 能力,我們也可能與某些不支援 mprds 或支援不同數量路徑的節點對等。 因此,對等節點需要就用於連線的路徑數量達成一致。 這是透過在第一個資料包之前傳送控制資料包交換來完成的。 當傳輸具有多路徑能力時,控制資料包交換必須在 rds_sendmsg() 中的傳出雜湊完成之前完成。

控制資料包是一個 RDS ping 資料包(即,傳送到 rds 目標埠 0 的資料包),該 ping 資料包具有型別為 RDS_EXTHDR_NPATHS、長度為 2 位元組的 rds 擴充套件標頭選項,並且該值是傳送方支援的路徑數。 “探測”ping 資料包將從某個保留埠 RDS_FLAG_PROBE_PORT(在 <linux/rds.h> 中)傳送。 來自 RDS_FLAG_PROBE_PORT 的 ping 的接收者因此可以立即計算 min(sender_paths, rcvr_paths)。 響應於探測 ping 傳送的 pong 應該包含 rcvr 的 npaths,前提是 rcvr 具有 mprds 能力。

如果 rcvr 不具有 mprds 能力,則 ping 中的 exthdr 將被忽略。 在這種情況下,pong 將沒有任何 exthdr,因此探測 ping 的傳送方可以預設為單路徑 mprds。