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。