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, ...)
如果尚未通过 SO_RDS_TRANSPORT 套接字选项选择传输方式,则将套接字绑定到本地 IP 地址和端口以及传输方式
- sendmsg(fd, ...)
向指示的接收者发送消息。如果底层可靠连接尚未建立,则内核将透明地建立该连接。
尝试发送超过 SO_SNDSIZE 的消息将返回 -EMSGSIZE
尝试发送消息,这将导致排队的总字节数超过 SO_SNDSIZE 阈值,将返回 EAGAIN。
尝试向标记为“拥塞”的目标发送消息将返回 ENOBUFS。
- recvmsg(fd, ...)
接收已排队到此套接字的消息。调整套接字接收队列的计数,如果队列长度降至 SO_SNDSIZE 以下,则将端口标记为未拥塞,并向所有对等方发送拥塞更新。
应用程序可以请求 RDS 内核模块通过控制消息接收通知(例如,当拥塞更新到达或 RDMA 操作完成时会收到通知)。这些通知通过 struct msghdr 的 msg.msg_control 缓冲区接收。消息的格式在手册页中描述。
- poll(fd)
RDS 支持轮询接口,允许应用程序实现异步 I/O。
POLLIN 处理非常简单。当套接字中排队了传入消息或有待处理的通知时,我们发出 POLLIN 信号。
POLLOUT 有点困难。由于您基本上可以发送到任何目标,因此只要发送队列上有空间(即排队的字节数小于 sendbuf 大小),RDS 将始终发出 POLLOUT 信号。
但是,内核将拒绝接受发送到标记为拥塞的目标的消息 - 在这种情况下,如果您依赖轮询来告诉您该怎么做,您将永远循环。这不是一个简单的问题,但是应用程序可以通过使用拥塞通知以及检查 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 相关的信息。
确认和重传处理
人们可能会认为,使用可靠的 IB 连接,您不需要确认已收到的消息。问题在于 IB 硬件会在将消息 DMA 到内存之前生成确认消息。如果在发送确认消息和 DMA 消息并处理之前,HCA 因任何原因而被禁用,则会产生潜在的消息丢失。如果另一个 HCA 可用于故障转移,则这只是一个潜在问题。
立即发送确认消息可以让发送者快速地从发送队列中释放已发送的消息,但是可能会导致过多的流量用于确认消息。RDS 将确认消息捎带在已发送的数据包上。仅允许一次发送一个确认消息,并且当发送方的发送缓冲区开始填满时,发送方才会请求确认,从而减少仅确认数据包的数量。所有重传也会被确认。
流量控制
RDS 的 IB 传输使用基于信用的机制来验证对等方的接收缓冲区是否有更多数据的空间。这消除了连接上硬件重试的需要。
拥塞
在接收套接字的接收队列中等待的消息会根据套接字的 SO_RCVBUF 选项值进行计数。仅计算消息中的负载字节数。如果排队的字节数等于或超过 rcvbuf,则套接字会拥塞。所有尝试发送到此套接字地址的发送都应返回阻塞或返回 -EWOULDBLOCK。
预计应用程序经过合理的调整,这种情况很少发生。遇到这种“背压”的应用程序被认为是错误。
这是通过让每个节点维护位图来实现的,这些位图指示绑定地址上的哪些端口拥塞。随着位图的变化,它将通过所有终止于已更改的位图的本地地址的连接发送。
当连接建立时,位图会被分配。这避免了在中断处理路径中分配,该路径在套接字上排队消息。密集的位图让传输可以在任何位图更改时以相当有效的方式发送整个位图。这比一些更精细的每个端口拥塞通信更容易实现。发送者会进行非常便宜的位测试,以测试它即将发送到的端口是否拥塞。
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 放置在发送队列中
唤醒发送工作线程
- 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
- 如果数据报已完成
如果数据报是拥塞更新,则更新拥塞映射
否则调用 rds_recv_incoming()
注意是否需要确认
- rds_recv_incoming()
丢弃重复的数据包
响应 ping
查找与此数据报关联的套接字
添加到套接字队列
唤醒套接字
进行一些拥塞计算
- rds_recvmsg
将数据复制到用户 iovec
处理 CMSGs
返回到应用程序
多路径 RDS (mprds)¶
Mprds 是多路径 RDS,主要用于 RDS over TCP(尽管这个概念可以扩展到其他传输方式)。 RDS over TCP 的经典实现是通过在任意两个端点(其中端点 == [IP 地址, 端口])之间的多个 PF_RDS 套接字之间进行多路复用,从而在涉及的两个 IP 地址之间通过单个 TCP 套接字进行多路复用。 这有一个限制,即它最终将多个 RDS 流汇集到单个 TCP 流上,因此 (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 应该在接收者具有 mprds 功能时包含接收者的 npaths。
如果接收者不具备 mprds 功能,则 ping 中的 exthdr 将被忽略。 在这种情况下,pong 将没有任何 exthdrs,因此探测 ping 的发送方可以默认为单路径 mprds。