요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
===
RDS
===
Overview
========
This readme tries to provide some background on the hows and whys of RDS,
and will hopefully help you find your way around the code.
In addition, please see this email about RDS origins:
http://oss.oracle.com/pipermail/rds-devel/2007-November/000228.html
RDS Architecture
================
RDS provides reliable, ordered datagram delivery by using a single
reliable connection between any two nodes in the cluster. This allows
applications to use a single socket to talk to any other process in the
cluster - so in a cluster with N processes you need N sockets, in contrast
to N*N if you use a connection-oriented socket transport like TCP.
RDS is not Infiniband-specific; it was designed to support different
transports. The current implementation used to support RDS over TCP as well
as IB.
The high-level semantics of RDS from the application's point of view are
* Addressing
RDS uses IPv4 addresses and 16bit port numbers to identify
the end point of a connection. All socket operations that involve
passing addresses between kernel and user space generally
use a struct sockaddr_in.
The fact that IPv4 addresses are used does not mean the underlying
transport has to be IP-based. In fact, RDS over IB uses a
reliable IB connection; the IP address is used exclusively to
locate the remote node's GID (by ARPing for the given IP).
The port space is entirely independent of UDP, TCP or any other
protocol.
* Socket interface
RDS sockets work *mostly* as you would expect from a BSD
socket. The next section will cover the details. At any rate,
all I/O is performed through the standard BSD socket API.
Some additions like zerocopy support are implemented through
control messages, while other extensions use the getsockopt/
setsockopt calls.
Sockets must be bound before you can send or receive data.
This is needed because binding also selects a transport and
attaches it to the socket. Once bound, the transport assignment
does not change. RDS will tolerate IPs moving around (eg in
a active-active HA scenario), but only as long as the address
doesn't move to a different transport.
* sysctls
RDS supports a number of sysctls in /proc/sys/net/rds
Socket Interface
================
AF_RDS, PF_RDS, SOL_RDS
AF_RDS and PF_RDS are the domain type to be used with socket(2)
to create RDS sockets. SOL_RDS is the socket-level to be used
with setsockopt(2) and getsockopt(2) for RDS specific socket
options.
fd = socket(PF_RDS, SOCK_SEQPACKET, 0);
This creates a new, unbound RDS socket.
setsockopt(SOL_SOCKET): send and receive buffer size
RDS honors the send and receive buffer size socket options.
You are not allowed to queue more than SO_SNDSIZE bytes to
a socket. A message is queued when sendmsg is called, and
it leaves the queue when the remote system acknowledges
its arrival.
The SO_RCVSIZE option controls the maximum receive queue length.
This is a soft limit rather than a hard limit - RDS will
continue to accept and queue incoming messages, even if that
takes the queue length over the limit. However, it will also
mark the port as "congested" and send a congestion update to
the source node. The source node is supposed to throttle any
processes sending to this congested port.
bind(fd, &sockaddr_in, ...)
This binds the socket to a local IP address and port, and a
transport, if one has not already been selected via the
SO_RDS_TRANSPORT socket option
sendmsg(fd, ...)
Sends a message to the indicated recipient. The kernel will
transparently establish the underlying reliable connection
if it isn't up yet.
An attempt to send a message that exceeds SO_SNDSIZE will
return with -EMSGSIZE
An attempt to send a message that would take the total number
of queued bytes over the SO_SNDSIZE threshold will return
EAGAIN.
An attempt to send a message to a destination that is marked
as "congested" will return ENOBUFS.
recvmsg(fd, ...)
Receives a message that was queued to this socket. The sockets
recv queue accounting is adjusted, and if the queue length
drops below SO_SNDSIZE, the port is marked uncongested, and
a congestion update is sent to all peers.
Applications can ask the RDS kernel module to receive
notifications via control messages (for instance, there is a
notification when a congestion update arrived, or when a RDMA
operation completes). These notifications are received through
the msg.msg_control buffer of struct msghdr. The format of the
messages is described in manpages.
poll(fd)
RDS supports the poll interface to allow the application
to implement async I/O.
POLLIN handling is pretty straightforward. When there's an
incoming message queued to the socket, or a pending notification,
we signal POLLIN.
POLLOUT is a little harder. Since you can essentially send
to any destination, RDS will always signal POLLOUT as long as
there's room on the send queue (ie the number of bytes queued
is less than the sendbuf size).
However, the kernel will refuse to accept messages to
a destination marked congested - in this case you will loop
forever if you rely on poll to tell you what to do.
This isn't a trivial problem, but applications can deal with
this - by using congestion notifications, and by checking for
ENOBUFS errors returned by sendmsg.
setsockopt(SOL_RDS, RDS_CANCEL_SENT_TO, &sockaddr_in)
This allows the application to discard all messages queued to a
specific destination on this particular socket.
This allows the application to cancel outstanding messages if
it detects a timeout. For instance, if it tried to send a message,
and the remote host is unreachable, RDS will keep trying forever.
The application may decide it's not worth it, and cancel the
operation. In this case, it would use RDS_CANCEL_SENT_TO to
nuke any pending messages.
``setsockopt(fd, SOL_RDS, SO_RDS_TRANSPORT, (int *)&transport ..), getsockopt(fd, SOL_RDS, SO_RDS_TRANSPORT, (int *)&transport ..)``
Set or read an integer defining the underlying
encapsulating transport to be used for RDS packets on the
socket. When setting the option, integer argument may be
one of RDS_TRANS_TCP or RDS_TRANS_IB. When retrieving the
value, RDS_TRANS_NONE will be returned on an unbound socket.
This socket option may only be set exactly once on the socket,
prior to binding it via the bind(2) system call. Attempts to
set SO_RDS_TRANSPORT on a socket for which the transport has
been previously attached explicitly (by SO_RDS_TRANSPORT) or
implicitly (via bind(2)) will return an error of EOPNOTSUPP.
An attempt to set SO_RDS_TRANSPORT to RDS_TRANS_NONE will
always return EINVAL.
RDMA for RDS
============
see rds-rdma(7) manpage (available in rds-tools)
Congestion Notifications
========================
see rds(7) manpage
RDS Protocol
============
Message header
The message header is a 'struct rds_header' (see rds.h):
Fields:
h_sequence:
per-packet sequence number
h_ack:
piggybacked acknowledgment of last packet received
h_len:
length of data, not including header
h_sport:
source port
h_dport:
destination port
h_flags:
Can be:
============= ==================================
CONG_BITMAP this is a congestion update bitmap
ACK_REQUIRED receiver must ack this packet
RETRANSMITTED packet has previously been sent
============= ==================================
h_credit:
indicate to other end of connection that
it has more credits available (i.e. there is
more send room)
h_padding[4]:
unused, for future use
h_csum:
header checksum
h_exthdr:
optional data can be passed here. This is currently used for
passing RDMA-related information.
ACK and retransmit handling
One might think that with reliable IB connections you wouldn't need
to ack messages that have been received. The problem is that IB
hardware generates an ack message before it has DMAed the message
into memory. This creates a potential message loss if the HCA is
disabled for any reason between when it sends the ack and before
the message is DMAed and processed. This is only a potential issue
if another HCA is available for fail-over.
Sending an ack immediately would allow the sender to free the sent
message from their send queue quickly, but could cause excessive
traffic to be used for acks. RDS piggybacks acks on sent data
packets. Ack-only packets are reduced by only allowing one to be
in flight at a time, and by the sender only asking for acks when
its send buffers start to fill up. All retransmissions are also
acked.
Flow Control
RDS's IB transport uses a credit-based mechanism to verify that
there is space in the peer's receive buffers for more data. This
eliminates the need for hardware retries on the connection.
Congestion
Messages waiting in the receive queue on the receiving socket
are accounted against the sockets SO_RCVBUF option value. Only
the payload bytes in the message are accounted for. If the
number of bytes queued equals or exceeds rcvbuf then the socket
is congested. All sends attempted to this socket's address
should return block or return -EWOULDBLOCK.
Applications are expected to be reasonably tuned such that this
situation very rarely occurs. An application encountering this
"back-pressure" is considered a bug.
This is implemented by having each node maintain bitmaps which
indicate which ports on bound addresses are congested. As the
bitmap changes it is sent through all the connections which
terminate in the local address of the bitmap which changed.
The bitmaps are allocated as connections are brought up. This
avoids allocation in the interrupt handling path which queues
messages on sockets. The dense bitmaps let transports send the
entire bitmap on any bitmap change reasonably efficiently. This
is much easier to implement than some finer-grained
communication of per-port congestion. The sender does a very
inexpensive bit test to test if the port it's about to send to
is congested or not.
RDS Transport Layer
===================
As mentioned above, RDS is not IB-specific. Its code is divided
into a general RDS layer and a transport layer.
The general layer handles the socket API, congestion handling,
loopback, stats, usermem pinning, and the connection state machine.
The transport layer handles the details of the transport. The IB
transport, for example, handles all the queue pairs, work requests,
CM event handlers, and other Infiniband details.
RDS Kernel Structures
=====================
struct rds_message
aka possibly "rds_outgoing", the generic RDS layer copies data to
be sent and sets header fields as needed, based on the socket API.
This is then queued for the individual connection and sent by the
connection's transport.
struct rds_incoming
a generic struct referring to incoming data that can be handed from
the transport to the general code and queued by the general code
while the socket is awoken. It is then passed back to the transport
code to handle the actual copy-to-user.
struct rds_socket
per-socket information
struct rds_connection
per-connection information
struct rds_transport
pointers to transport-specific functions
struct rds_statistics
non-transport-specific statistics
struct rds_cong_map
wraps the raw congestion bitmap, contains rbnode, waitq, etc.
Connection management
=====================
Connections may be in UP, DOWN, CONNECTING, DISCONNECTING, and
ERROR states.
The first time an attempt is made by an RDS socket to send data to
a node, a connection is allocated and connected. That connection is
then maintained forever -- if there are transport errors, the
connection will be dropped and re-established.
Dropping a connection while packets are queued will cause queued or
partially-sent datagrams to be retransmitted when the connection is
re-established.
The send path
=============
rds_sendmsg()
- struct rds_message built from incoming data
- CMSGs parsed (e.g. RDMA ops)
- transport connection allocated and connected if not already
- rds_message placed on send queue
- send worker awoken
rds_send_worker()
- calls rds_send_xmit() until queue is empty
rds_send_xmit()
- transmits congestion map if one is pending
- may set ACK_REQUIRED
- calls transport to send either non-RDMA or RDMA message
(RDMA ops never retransmitted)
rds_ib_xmit()
- allocs work requests from send ring
- adds any new send credits available to peer (h_credits)
- maps the rds_message's sg list
- piggybacks ack
- populates work requests
- post send to connection's queue pair
The recv path
=============
rds_ib_recv_cq_comp_handler()
- looks at write completions
- unmaps recv buffer from device
- no errors, call rds_ib_process_recv()
- refill recv ring
rds_ib_process_recv()
- validate header checksum
- copy header to rds_ib_incoming struct if start of a new datagram
- add to ibinc's fraglist
- if completed datagram:
- update cong map if datagram was cong update
- call rds_recv_incoming() otherwise
- note if ack is required
rds_recv_incoming()
- drop duplicate packets
- respond to pings
- find the sock associated with this datagram
- add to sock queue
- wake up sock
- do some congestion calculations
rds_recvmsg
- copy data into user iovec
- handle CMSGs
- return to application
Multipath RDS (mprds)
=====================
Mprds is multipathed-RDS, primarily intended for RDS-over-TCP
(though the concept can be extended to other transports). The classical
implementation of RDS-over-TCP is implemented by demultiplexing multiple
PF_RDS sockets between any 2 endpoints (where endpoint == [IP address,
port]) over a single TCP socket between the 2 IP addresses involved. This
has the limitation that it ends up funneling multiple RDS flows over a
single TCP flow, thus it is
(a) upper-bounded to the single-flow bandwidth,
(b) suffers from head-of-line blocking for all the RDS sockets.
Better throughput (for a fixed small packet size, MTU) can be achieved
by having multiple TCP/IP flows per rds/tcp connection, i.e., multipathed
RDS (mprds). Each such TCP/IP flow constitutes a path for the rds/tcp
connection. RDS sockets will be attached to a path based on some hash
(e.g., of local address and RDS port number) and packets for that RDS
socket will be sent over the attached path using TCP to segment/reassemble
RDS datagrams on that path.
Multipathed RDS is implemented by splitting the struct rds_connection into
a common (to all paths) part, and a per-path struct rds_conn_path. All
I/O workqs and reconnect threads are driven from the rds_conn_path.
Transports such as TCP that are multipath capable may then set up a
TCP socket per rds_conn_path, and this is managed by the transport via
the transport private cp_transport_data pointer.
Transports announce themselves as multipath capable by setting the
t_mp_capable bit during registration with the rds core module. When the
transport is multipath-capable, rds_sendmsg() hashes outgoing traffic
across multiple paths. The outgoing hash is computed based on the
local address and port that the PF_RDS socket is bound to.
Additionally, even if the transport is MP capable, we may be
peering with some node that does not support mprds, or supports
a different number of paths. As a result, the peering nodes need
to agree on the number of paths to be used for the connection.
This is done by sending out a control packet exchange before the
first data packet. The control packet exchange must have completed
prior to outgoing hash completion in rds_sendmsg() when the transport
is multipath capable.
The control packet is an RDS ping packet (i.e., packet to rds dest
port 0) with the ping packet having a rds extension header option of
type RDS_EXTHDR_NPATHS, length 2 bytes, and the value is the
number of paths supported by the sender. The "probe" ping packet will
get sent from some reserved port, RDS_FLAG_PROBE_PORT (in <linux/rds.h>)
The receiver of a ping from RDS_FLAG_PROBE_PORT will thus immediately
be able to compute the min(sender_paths, rcvr_paths). The pong
sent in response to a probe-ping should contain the rcvr's npaths
when the rcvr is mprds-capable.
If the rcvr is not mprds-capable, the exthdr in the ping will be
ignored. In this case the pong will not have any exthdrs, so the sender
of the probe-ping can default to single-path mprds.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
RDS architecture와 application semantics
1-66이 문서는 RDS가 어떤 이유로 어떤 방식으로 설계되었는지 배경을 제공하고 kernel code를 탐색할 출발점을 제시합니다. RDS의 기원은 원문에 연결된 2007년 `rds-devel` email에서도 확인할 수 있습니다.
RDS는 cluster의 node 두 개 사이에 신뢰할 수 있는 connection 하나를 유지하면서 application에는 reliable하고 ordered인 datagram delivery를 제공합니다. Application process 하나는 socket 하나로 cluster의 모든 다른 process와 통신할 수 있습니다. Process가 N개라면 socket도 N개면 되며, TCP 같은 connection-oriented socket transport에서 endpoint pair마다 필요할 수 있는 N*N개와 대비됩니다.
RDS는 Infiniband 전용 protocol이 아니며 여러 transport를 지원하도록 계층화되었습니다. 구현은 RDS over TCP와 RDS over IB를 지원해 왔습니다. Application endpoint는 IPv4 address와 독립적인 16-bit RDS port로 식별하고 kernel과 userspace 사이 주소 전달에는 보통 `struct sockaddr_in`을 사용합니다.
IPv4 address를 쓴다고 underlying transport가 IP 기반이어야 하는 것은 아닙니다. RDS over IB는 reliable IB connection을 사용하고 IP address는 ARP를 통해 remote node의 GID를 찾는 데만 씁니다. RDS port namespace는 UDP, TCP 및 다른 protocol의 port 공간과 완전히 독립적입니다.
RDS socket I/O는 대체로 BSD socket과 같고 표준 API로 수행합니다. Zerocopy 같은 추가 기능은 control message로, 다른 extension은 `getsockopt()`와 `setsockopt()`로 제공합니다. 송수신 전에 반드시 `bind()`해야 하는 이유는 local address뿐 아니라 transport도 선택해 socket에 고정하기 때문입니다. Bind 후 transport는 바뀌지 않으며 IP 이동은 같은 transport 안에서만 허용됩니다. Active-active HA에서 주소가 옮겨가더라도 다른 transport로 넘어가면 안 됩니다.
RDS 관련 sysctl은 `/proc/sys/net/rds` 아래에 있습니다.
N개 process가 endpoint pair별 socket 없이 공통 RDS connection을 공유합니다.
Application 주소와 실제 transport 식별을 구분합니다.
.. SPDX-License-Identifier: GPL-2.0
===
RDS
===
Overview
========
This readme tries to provide some background on the hows and whys of RDS,
and will hopefully help you find your way around the code.
In addition, please see this email about RDS origins:
http://oss.oracle.com/pipermail/rds-devel/2007-November/000228.html
RDS Architecture
================
RDS provides reliable, ordered datagram delivery by using a single
reliable connection between any two nodes in the cluster. This allows
applications to use a single socket to talk to any other process in the
cluster - so in a cluster with N processes you need N sockets, in contrast
to N*N if you use a connection-oriented socket transport like TCP.
RDS is not Infiniband-specific; it was designed to support different
transports. The current implementation used to support RDS over TCP as well
as IB.
The high-level semantics of RDS from the application's point of view are
* Addressing
RDS uses IPv4 addresses and 16bit port numbers to identify
the end point of a connection. All socket operations that involve
passing addresses between kernel and user space generally
use a struct sockaddr_in.
The fact that IPv4 addresses are used does not mean the underlying
transport has to be IP-based. In fact, RDS over IB uses a
reliable IB connection; the IP address is used exclusively to
locate the remote node's GID (by ARPing for the given IP).
The port space is entirely independent of UDP, TCP or any other
protocol.
* Socket interface
RDS sockets work *mostly* as you would expect from a BSD
socket. The next section will cover the details. At any rate,
all I/O is performed through the standard BSD socket API.
Some additions like zerocopy support are implemented through
control messages, while other extensions use the getsockopt/
setsockopt calls.
Sockets must be bound before you can send or receive data.
This is needed because binding also selects a transport and
attaches it to the socket. Once bound, the transport assignment
does not change. RDS will tolerate IPs moving around (eg in
a active-active HA scenario), but only as long as the address
doesn't move to a different transport.
* sysctls
RDS supports a number of sysctls in /proc/sys/net/rds
Socket 생성, buffer, bind와 send·receive
67-126RDS socket domain은 `AF_RDS` 또는 `PF_RDS`이고 RDS 전용 socket option level은 `SOL_RDS`입니다. `socket(PF_RDS, SOCK_SEQPACKET, 0)`은 아직 bind되지 않은 RDS socket을 만듭니다.
RDS는 `SOL_SOCKET`의 send·receive buffer 크기 option을 따릅니다. `SO_SNDSIZE`보다 많은 byte를 socket send queue에 둘 수 없습니다. `sendmsg()`가 호출되면 message가 queue에 들어가고 remote system이 도착을 acknowledge할 때 queue에서 제거됩니다.
`SO_RCVSIZE`는 최대 receive queue length를 제어하지만 hard limit가 아닌 soft limit입니다. Queue가 limit를 넘어도 incoming message를 계속 받아 쌓는 대신 해당 port를 `congested`로 표시하고 source node에 congestion update를 보냅니다. Source node는 이 port로 보내는 process를 throttle해야 합니다.
`bind(fd, &sockaddr_in, ...)`는 local IP address와 port에 socket을 묶고, `SO_RDS_TRANSPORT`로 미리 선택하지 않았다면 transport도 선택합니다. `sendmsg()`는 지정한 recipient로 message를 보내며 underlying reliable connection이 아직 없으면 kernel이 투명하게 설정합니다.
Message 하나가 `SO_SNDSIZE`를 넘으면 `-EMSGSIZE`를 반환합니다. Message를 추가했을 때 전체 queued byte가 `SO_SNDSIZE` threshold를 넘으면 `EAGAIN`, destination이 congested로 표시되어 있으면 `ENOBUFS`를 반환합니다.
`recvmsg()`는 socket receive queue의 message를 받고 queue accounting을 갱신합니다. 원문은 queue length가 `SO_SNDSIZE` 아래로 내려가면 port를 uncongested로 표시하고 모든 peer에 update를 보낸다고 설명합니다. Application은 `struct msghdr`의 `msg.msg_control` buffer에서 congestion update 도착이나 RDMA operation 완료 같은 kernel notification도 control message로 받을 수 있으며 정확한 형식은 manpage에 정의됩니다.
크기, queue와 congestion 조건별 결과입니다.
Soft receive limit가 sender throttling으로 이어집니다.
Socket Interface
================
AF_RDS, PF_RDS, SOL_RDS
AF_RDS and PF_RDS are the domain type to be used with socket(2)
to create RDS sockets. SOL_RDS is the socket-level to be used
with setsockopt(2) and getsockopt(2) for RDS specific socket
options.
fd = socket(PF_RDS, SOCK_SEQPACKET, 0);
This creates a new, unbound RDS socket.
setsockopt(SOL_SOCKET): send and receive buffer size
RDS honors the send and receive buffer size socket options.
You are not allowed to queue more than SO_SNDSIZE bytes to
a socket. A message is queued when sendmsg is called, and
it leaves the queue when the remote system acknowledges
its arrival.
The SO_RCVSIZE option controls the maximum receive queue length.
This is a soft limit rather than a hard limit - RDS will
continue to accept and queue incoming messages, even if that
takes the queue length over the limit. However, it will also
mark the port as "congested" and send a congestion update to
the source node. The source node is supposed to throttle any
processes sending to this congested port.
bind(fd, &sockaddr_in, ...)
This binds the socket to a local IP address and port, and a
transport, if one has not already been selected via the
SO_RDS_TRANSPORT socket option
sendmsg(fd, ...)
Sends a message to the indicated recipient. The kernel will
transparently establish the underlying reliable connection
if it isn't up yet.
An attempt to send a message that exceeds SO_SNDSIZE will
return with -EMSGSIZE
An attempt to send a message that would take the total number
of queued bytes over the SO_SNDSIZE threshold will return
EAGAIN.
An attempt to send a message to a destination that is marked
as "congested" will return ENOBUFS.
recvmsg(fd, ...)
Receives a message that was queued to this socket. The sockets
recv queue accounting is adjusted, and if the queue length
drops below SO_SNDSIZE, the port is marked uncongested, and
a congestion update is sent to all peers.
Applications can ask the RDS kernel module to receive
notifications via control messages (for instance, there is a
notification when a congestion update arrived, or when a RDMA
operation completes). These notifications are received through
the msg.msg_control buffer of struct msghdr. The format of the
messages is described in manpages.
poll, 전송 취소와 transport 선택
127-171RDS는 asynchronous I/O를 위한 `poll()`을 지원합니다. Incoming message가 socket에 queue되어 있거나 pending notification이 있으면 `POLLIN`을 알립니다.
`POLLOUT`은 destination별 congestion 때문에 더 복잡합니다. RDS socket은 어느 destination으로든 보낼 수 있으므로 send queue에 여유가 있으면 항상 `POLLOUT`을 알립니다. 그러나 선택한 destination이 congested이면 kernel은 message를 거부합니다. 따라서 `poll()` 결과만 의존하면 계속 writable로 깨어난 뒤 `sendmsg()`가 실패하는 loop에 빠질 수 있습니다. Application은 congestion notification을 사용하고 `sendmsg()`의 `ENOBUFS`도 반드시 처리해야 합니다.
`setsockopt(SOL_RDS, RDS_CANCEL_SENT_TO, &sockaddr_in)`는 이 socket에서 특정 destination으로 queue한 모든 message를 폐기합니다. Remote host가 unreachable인 경우 RDS는 기본적으로 계속 재시도하므로 application timeout 정책상 더 기다릴 가치가 없을 때 outstanding message를 취소하는 용도입니다.
`SO_RDS_TRANSPORT`는 RDS packet을 감쌀 underlying transport를 integer로 설정하거나 읽습니다. 설정값은 `RDS_TRANS_TCP` 또는 `RDS_TRANS_IB`이고, 아직 bind되지 않은 socket을 조회하면 `RDS_TRANS_NONE`을 돌려줍니다. 이 option은 `bind(2)` 전에 정확히 한 번만 설정할 수 있습니다.
Transport가 이미 `SO_RDS_TRANSPORT`로 명시적으로 붙었거나 `bind(2)`로 암묵적으로 붙은 socket에서 다시 설정하면 `EOPNOTSUPP`입니다. `RDS_TRANS_NONE`으로 설정하려는 시도는 항상 `EINVAL`입니다.
비동기 처리와 transport 고정 규칙입니다.
poll(fd)
RDS supports the poll interface to allow the application
to implement async I/O.
POLLIN handling is pretty straightforward. When there's an
incoming message queued to the socket, or a pending notification,
we signal POLLIN.
POLLOUT is a little harder. Since you can essentially send
to any destination, RDS will always signal POLLOUT as long as
there's room on the send queue (ie the number of bytes queued
is less than the sendbuf size).
However, the kernel will refuse to accept messages to
a destination marked congested - in this case you will loop
forever if you rely on poll to tell you what to do.
This isn't a trivial problem, but applications can deal with
this - by using congestion notifications, and by checking for
ENOBUFS errors returned by sendmsg.
setsockopt(SOL_RDS, RDS_CANCEL_SENT_TO, &sockaddr_in)
This allows the application to discard all messages queued to a
specific destination on this particular socket.
This allows the application to cancel outstanding messages if
it detects a timeout. For instance, if it tried to send a message,
and the remote host is unreachable, RDS will keep trying forever.
The application may decide it's not worth it, and cancel the
operation. In this case, it would use RDS_CANCEL_SENT_TO to
nuke any pending messages.
``setsockopt(fd, SOL_RDS, SO_RDS_TRANSPORT, (int *)&transport ..), getsockopt(fd, SOL_RDS, SO_RDS_TRANSPORT, (int *)&transport ..)``
Set or read an integer defining the underlying
encapsulating transport to be used for RDS packets on the
socket. When setting the option, integer argument may be
one of RDS_TRANS_TCP or RDS_TRANS_IB. When retrieving the
value, RDS_TRANS_NONE will be returned on an unbound socket.
This socket option may only be set exactly once on the socket,
prior to binding it via the bind(2) system call. Attempts to
set SO_RDS_TRANSPORT on a socket for which the transport has
been previously attached explicitly (by SO_RDS_TRANSPORT) or
implicitly (via bind(2)) will return an error of EOPNOTSUPP.
An attempt to set SO_RDS_TRANSPORT to RDS_TRANS_NONE will
always return EINVAL.
RDMA·congestion 문서와 rds_header
172-223RDS의 RDMA interface 세부 사항은 `rds-tools`에 포함된 `rds-rdma(7)` manpage에 있고 congestion notification API는 `rds(7)` manpage에 있습니다.
Wire message header는 `rds.h`의 `struct rds_header`입니다. `h_sequence`는 packet별 sequence number, `h_ack`는 마지막으로 받은 packet에 piggyback한 acknowledgement, `h_len`은 header를 제외한 data 길이입니다. `h_sport`와 `h_dport`는 source·destination RDS port입니다.
`h_flags`의 `CONG_BITMAP`은 congestion update bitmap임을, `ACK_REQUIRED`는 receiver가 이 packet을 acknowledge해야 함을, `RETRANSMITTED`는 이전에 전송된 packet임을 표시합니다. `h_credit`은 상대에게 추가 send room, 즉 사용 가능한 receive credit이 생겼음을 알립니다.
`h_padding[4]`는 미래 사용을 위한 미사용 공간이고 `h_csum`은 header checksum입니다. `h_exthdr`에는 optional data를 넣을 수 있으며 현재는 RDMA 관련 정보를 전달하는 데 사용합니다.
Message header field와 역할입니다.
RDMA for RDS
============
see rds-rdma(7) manpage (available in rds-tools)
Congestion Notifications
========================
see rds(7) manpage
RDS Protocol
============
Message header
The message header is a 'struct rds_header' (see rds.h):
Fields:
h_sequence:
per-packet sequence number
h_ack:
piggybacked acknowledgment of last packet received
h_len:
length of data, not including header
h_sport:
source port
h_dport:
destination port
h_flags:
Can be:
============= ==================================
CONG_BITMAP this is a congestion update bitmap
ACK_REQUIRED receiver must ack this packet
RETRANSMITTED packet has previously been sent
============= ==================================
h_credit:
indicate to other end of connection that
it has more credits available (i.e. there is
more send room)
h_padding[4]:
unused, for future use
h_csum:
header checksum
h_exthdr:
optional data can be passed here. This is currently used for
passing RDMA-related information.
ACK, credit flow control과 congestion bitmap
224-275Reliable IB connection을 사용해도 RDS message ACK가 필요합니다. IB hardware는 message를 memory로 DMA하기 전에 hardware ACK를 만들 수 있습니다. ACK 뒤 DMA·처리 전에 HCA가 비활성화되고 다른 HCA로 failover하면 message가 손실될 수 있기 때문입니다.
매 message마다 즉시 ACK를 보내면 sender가 send queue에서 message를 빨리 해제할 수 있지만 ACK traffic이 과도해집니다. RDS는 가능한 한 data packet에 ACK를 piggyback합니다. ACK-only packet은 한 번에 하나만 flight 상태가 되도록 제한하고 sender의 send buffer가 차기 시작할 때만 ACK를 요청합니다. 모든 retransmission은 반드시 acknowledge합니다.
RDS IB transport는 credit 기반 flow control로 peer receive buffer에 data를 더 받을 공간이 있는지 확인합니다. 이 mechanism은 connection에서 hardware retry가 필요하지 않게 합니다.
Receive socket queue의 payload byte만 `SO_RCVBUF` 값에 대해 accounting합니다. Queued byte가 `rcvbuf` 이상이면 socket은 congested가 되고 해당 address로의 send는 block하거나 `-EWOULDBLOCK`을 반환해야 합니다. 잘 조정된 application에서는 이 back-pressure가 매우 드물어야 하며 문서는 이를 자주 겪는 application을 bug로 간주합니다.
각 node는 bind된 address에서 어느 port가 congested인지 나타내는 bitmap을 유지합니다. Bitmap이 바뀌면 그 local address에서 끝나는 모든 connection으로 전체 bitmap을 보냅니다. Connection을 올릴 때 미리 bitmap을 할당해 socket에 message를 queue하는 interrupt path에서는 allocation하지 않습니다.
Dense bitmap 전체를 변경 때마다 보내는 방식은 per-port congestion을 세밀한 message로 전달하는 것보다 구현이 단순하면서도 충분히 효율적입니다. Sender는 전송 직전 아주 저렴한 bit test 한 번으로 destination port가 congested인지 판단합니다.
Data piggyback을 우선하고 필요한 경우에만 ACK-only packet을 제한적으로 사용합니다.
Receive queue 상태가 cluster peer의 send decision으로 전달됩니다.
ACK and retransmit handling
One might think that with reliable IB connections you wouldn't need
to ack messages that have been received. The problem is that IB
hardware generates an ack message before it has DMAed the message
into memory. This creates a potential message loss if the HCA is
disabled for any reason between when it sends the ack and before
the message is DMAed and processed. This is only a potential issue
if another HCA is available for fail-over.
Sending an ack immediately would allow the sender to free the sent
message from their send queue quickly, but could cause excessive
traffic to be used for acks. RDS piggybacks acks on sent data
packets. Ack-only packets are reduced by only allowing one to be
in flight at a time, and by the sender only asking for acks when
its send buffers start to fill up. All retransmissions are also
acked.
Flow Control
RDS's IB transport uses a credit-based mechanism to verify that
there is space in the peer's receive buffers for more data. This
eliminates the need for hardware retries on the connection.
Congestion
Messages waiting in the receive queue on the receiving socket
are accounted against the sockets SO_RCVBUF option value. Only
the payload bytes in the message are accounted for. If the
number of bytes queued equals or exceeds rcvbuf then the socket
is congested. All sends attempted to this socket's address
should return block or return -EWOULDBLOCK.
Applications are expected to be reasonably tuned such that this
situation very rarely occurs. An application encountering this
"back-pressure" is considered a bug.
This is implemented by having each node maintain bitmaps which
indicate which ports on bound addresses are congested. As the
bitmap changes it is sent through all the connections which
terminate in the local address of the bitmap which changed.
The bitmaps are allocated as connections are brought up. This
avoids allocation in the interrupt handling path which queues
messages on sockets. The dense bitmaps let transports send the
entire bitmap on any bitmap change reasonably efficiently. This
is much easier to implement than some finer-grained
communication of per-port congestion. The sender does a very
inexpensive bit test to test if the port it's about to send to
is congested or not.
Transport 계층과 kernel 핵심 구조체
276-319RDS code는 transport 독립적인 general RDS layer와 transport-specific layer로 나뉩니다. General layer는 socket API, congestion handling, loopback, statistics, user memory pinning과 connection state machine을 담당합니다. Transport layer는 실제 transport 세부 사항을 처리하며 IB implementation은 queue pair, work request, CM event handler와 기타 Infiniband 동작을 맡습니다.
`struct rds_message`는 `rds_outgoing`이라고도 볼 수 있는 outgoing datagram 표현입니다. General layer가 socket API로 받은 data를 복사하고 header field를 채운 뒤 connection queue에 넣으면 해당 connection transport가 전송합니다.
`struct rds_incoming`은 transport가 general code에 넘길 incoming data를 가리키는 공통 구조체입니다. General code는 socket을 깨우는 동안 이를 queue하고, 실제 copy-to-user 단계에서는 다시 transport code에 넘깁니다.
`struct rds_socket`은 socket별 정보, `struct rds_connection`은 connection별 정보, `struct rds_transport`는 transport-specific function pointer를 보관합니다. `struct rds_statistics`는 transport 독립 statistics이고 `struct rds_cong_map`은 raw congestion bitmap과 rbnode, wait queue 등을 감쌉니다.
General layer와 transport 사이 data·state object입니다.
Application API에서 실제 transport resource까지의 책임 분리입니다.
RDS Transport Layer
===================
As mentioned above, RDS is not IB-specific. Its code is divided
into a general RDS layer and a transport layer.
The general layer handles the socket API, congestion handling,
loopback, stats, usermem pinning, and the connection state machine.
The transport layer handles the details of the transport. The IB
transport, for example, handles all the queue pairs, work requests,
CM event handlers, and other Infiniband details.
RDS Kernel Structures
=====================
struct rds_message
aka possibly "rds_outgoing", the generic RDS layer copies data to
be sent and sets header fields as needed, based on the socket API.
This is then queued for the individual connection and sent by the
connection's transport.
struct rds_incoming
a generic struct referring to incoming data that can be handed from
the transport to the general code and queued by the general code
while the socket is awoken. It is then passed back to the transport
code to handle the actual copy-to-user.
struct rds_socket
per-socket information
struct rds_connection
per-connection information
struct rds_transport
pointers to transport-specific functions
struct rds_statistics
non-transport-specific statistics
struct rds_cong_map
wraps the raw congestion bitmap, contains rbnode, waitq, etc.
Connection state와 영구 재연결
320-335RDS connection은 `UP`, `DOWN`, `CONNECTING`, `DISCONNECTING`, `ERROR` 상태 중 하나입니다. RDS socket이 어떤 node로 처음 data를 보내려 할 때 connection을 할당하고 연결합니다.
한 번 만들어진 connection은 계속 유지됩니다. Transport error가 발생하면 connection을 내렸다가 다시 설정합니다. 이때 queue에 있거나 일부만 전송된 datagram은 connection이 복구된 뒤 retransmit됩니다.
Error 뒤에도 logical connection과 queued datagram delivery를 유지합니다.
Connection management
=====================
Connections may be in UP, DOWN, CONNECTING, DISCONNECTING, and
ERROR states.
The first time an attempt is made by an RDS socket to send data to
a node, a connection is allocated and connected. That connection is
then maintained forever -- if there are transport errors, the
connection will be dropped and re-established.
Dropping a connection while packets are queued will cause queued or
partially-sent datagrams to be retransmitted when the connection is
re-established.
Send path와 receive path
336-392Send path는 `rds_sendmsg()`에서 시작합니다. Userspace data로 `struct rds_message`를 만들고 RDMA operation 같은 CMSG를 parse합니다. 필요한 transport connection을 할당·연결한 뒤 message를 send queue에 넣고 send worker를 깨웁니다.
`rds_send_worker()`는 queue가 빌 때까지 `rds_send_xmit()`을 호출합니다. `rds_send_xmit()`은 pending congestion map을 먼저 보낼 수 있고 필요하면 `ACK_REQUIRED`를 설정한 뒤 transport에 일반 또는 RDMA message 전송을 요청합니다. RDMA operation은 retransmit하지 않습니다.
IB의 `rds_ib_xmit()`은 send ring에서 work request를 할당하고 새 receive credit을 `h_credits`로 peer에 알립니다. `rds_message`의 scatter-gather list를 map하고 ACK를 piggyback한 뒤 work request를 채워 connection queue pair에 post합니다.
Receive path의 `rds_ib_recv_cq_comp_handler()`는 completion을 확인하고 receive buffer를 device에서 unmap합니다. 오류가 없으면 `rds_ib_process_recv()`를 부른 뒤 receive ring을 refill합니다.
`rds_ib_process_recv()`는 header checksum을 검증하고 새 datagram 시작이면 header를 `rds_ib_incoming`에 복사한 뒤 fragment list에 추가합니다. Datagram이 완성되면 congestion update인 경우 congestion map을 갱신하고, 아니면 `rds_recv_incoming()`을 호출하며 ACK 필요 여부도 기록합니다.
`rds_recv_incoming()`은 duplicate packet을 버리고 ping에 응답하며 datagram에 대응하는 socket을 찾아 socket queue에 추가하고 깨운 뒤 congestion 계산을 합니다. 마지막으로 `rds_recvmsg()`가 data를 user iovec으로 복사하고 CMSG를 처리해 application으로 반환합니다.
Socket call에서 IB queue pair post까지입니다.
Completion에서 userspace copy까지입니다.
The send path
=============
rds_sendmsg()
- struct rds_message built from incoming data
- CMSGs parsed (e.g. RDMA ops)
- transport connection allocated and connected if not already
- rds_message placed on send queue
- send worker awoken
rds_send_worker()
- calls rds_send_xmit() until queue is empty
rds_send_xmit()
- transmits congestion map if one is pending
- may set ACK_REQUIRED
- calls transport to send either non-RDMA or RDMA message
(RDMA ops never retransmitted)
rds_ib_xmit()
- allocs work requests from send ring
- adds any new send credits available to peer (h_credits)
- maps the rds_message's sg list
- piggybacks ack
- populates work requests
- post send to connection's queue pair
The recv path
=============
rds_ib_recv_cq_comp_handler()
- looks at write completions
- unmaps recv buffer from device
- no errors, call rds_ib_process_recv()
- refill recv ring
rds_ib_process_recv()
- validate header checksum
- copy header to rds_ib_incoming struct if start of a new datagram
- add to ibinc's fraglist
- if completed datagram:
- update cong map if datagram was cong update
- call rds_recv_incoming() otherwise
- note if ack is required
rds_recv_incoming()
- drop duplicate packets
- respond to pings
- find the sock associated with this datagram
- add to sock queue
- wake up sock
- do some congestion calculations
rds_recvmsg
- copy data into user iovec
- handle CMSGs
- return to application
Multipath RDS(mprds)
393-448`mprds`는 multipathed RDS이며 주 대상은 RDS-over-TCP지만 개념은 다른 transport에도 확장할 수 있습니다. 고전적인 RDS-over-TCP는 두 endpoint 사이의 여러 `PF_RDS` socket을 두 IP address 사이 TCP socket 하나로 demultiplex합니다. 여기서 endpoint는 IP address와 port의 조합입니다.
이 방식은 여러 RDS flow를 TCP flow 하나에 몰아 단일-flow bandwidth가 상한이 되고, 한 flow의 지연이 모든 RDS socket을 막는 head-of-line blocking을 일으킵니다. 고정된 작은 packet size와 MTU에서 connection마다 여러 TCP/IP flow를 두면 throughput을 높일 수 있습니다. 각 TCP/IP flow가 RDS/TCP connection의 path가 됩니다.
RDS socket은 local address와 RDS port number 같은 값을 hash해 path에 붙고, 해당 socket의 packet은 그 path의 TCP를 통해 segmentation·reassembly됩니다. 구현은 `struct rds_connection`을 모든 path가 공유하는 부분과 path별 `struct rds_conn_path`로 나눕니다. I/O workqueue와 reconnect thread는 `rds_conn_path`에서 구동합니다.
TCP처럼 multipath-capable한 transport는 `rds_conn_path`마다 TCP socket을 만들 수 있고 transport-private `cp_transport_data` pointer로 관리합니다. Transport는 RDS core에 등록할 때 `t_mp_capable` bit를 설정해 capability를 알립니다. 이 경우 `rds_sendmsg()`가 `PF_RDS` socket의 bind local address와 port를 바탕으로 outgoing traffic을 여러 path에 hash합니다.
양 peer가 모두 multipath를 지원한다는 보장은 없고 지원 path 수가 다를 수도 있으므로 첫 data packet 전에 사용할 path 수를 control packet으로 합의해야 합니다. Multipath transport에서는 `rds_sendmsg()`의 outgoing hash가 완료되기 전에 이 exchange가 끝나야 합니다.
Control packet은 destination RDS port 0으로 보내는 RDS ping입니다. Ping extension header에는 type `RDS_EXTHDR_NPATHS`, length 2 byte, sender가 지원하는 path 수를 넣습니다. Probe ping은 `<linux/rds.h>`의 reserved port `RDS_FLAG_PROBE_PORT`에서 전송됩니다.
Receiver는 `RDS_FLAG_PROBE_PORT`에서 온 ping을 받으면 `min(sender_paths, rcvr_paths)`를 즉시 계산합니다. Receiver도 mprds-capable이면 pong에 자신의 `npaths`를 넣습니다. 지원하지 않으면 ping extension을 무시하고 pong에도 extension이 없으므로 probe sender는 single-path mprds로 fallback합니다.
Multipath가 해결하는 병목입니다.
첫 data와 outgoing hash보다 먼저 path 수를 합의합니다.
Multipath RDS (mprds)
=====================
Mprds is multipathed-RDS, primarily intended for RDS-over-TCP
(though the concept can be extended to other transports). The classical
implementation of RDS-over-TCP is implemented by demultiplexing multiple
PF_RDS sockets between any 2 endpoints (where endpoint == [IP address,
port]) over a single TCP socket between the 2 IP addresses involved. This
has the limitation that it ends up funneling multiple RDS flows over a
single TCP flow, thus it is
(a) upper-bounded to the single-flow bandwidth,
(b) suffers from head-of-line blocking for all the RDS sockets.
Better throughput (for a fixed small packet size, MTU) can be achieved
by having multiple TCP/IP flows per rds/tcp connection, i.e., multipathed
RDS (mprds). Each such TCP/IP flow constitutes a path for the rds/tcp
connection. RDS sockets will be attached to a path based on some hash
(e.g., of local address and RDS port number) and packets for that RDS
socket will be sent over the attached path using TCP to segment/reassemble
RDS datagrams on that path.
Multipathed RDS is implemented by splitting the struct rds_connection into
a common (to all paths) part, and a per-path struct rds_conn_path. All
I/O workqs and reconnect threads are driven from the rds_conn_path.
Transports such as TCP that are multipath capable may then set up a
TCP socket per rds_conn_path, and this is managed by the transport via
the transport private cp_transport_data pointer.
Transports announce themselves as multipath capable by setting the
t_mp_capable bit during registration with the rds core module. When the
transport is multipath-capable, rds_sendmsg() hashes outgoing traffic
across multiple paths. The outgoing hash is computed based on the
local address and port that the PF_RDS socket is bound to.
Additionally, even if the transport is MP capable, we may be
peering with some node that does not support mprds, or supports
a different number of paths. As a result, the peering nodes need
to agree on the number of paths to be used for the connection.
This is done by sending out a control packet exchange before the
first data packet. The control packet exchange must have completed
prior to outgoing hash completion in rds_sendmsg() when the transport
is multipath capable.
The control packet is an RDS ping packet (i.e., packet to rds dest
port 0) with the ping packet having a rds extension header option of
type RDS_EXTHDR_NPATHS, length 2 bytes, and the value is the
number of paths supported by the sender. The "probe" ping packet will
get sent from some reserved port, RDS_FLAG_PROBE_PORT (in <linux/rds.h>)
The receiver of a ping from RDS_FLAG_PROBE_PORT will thus immediately
be able to compute the min(sender_paths, rcvr_paths). The pong
sent in response to a probe-ping should contain the rcvr's npaths
when the rcvr is mprds-capable.
If the rcvr is not mprds-capable, the exthdr in the ping will be
ignored. In this case the pong will not have any exthdrs, so the sender
of the probe-ping can default to single-path mprds.
요약·해설
rds.rst:1-448RDS는 cluster node pair 사이 reliable connection을 공유하면서 application에 ordered datagram socket을 제공합니다. General RDS layer가 BSD socket API, ACK·congestion·connection state를 맡고 IB 또는 TCP transport가 실제 전송을 수행합니다. Multipath RDS는 `rds_conn_path`별 TCP flow와 사전 path-count 협상으로 단일 flow bandwidth와 head-of-line blocking 한계를 줄입니다.
Application datagram이 공통 계층과 선택한 transport를 거칩니다.