| |
| 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_TRANSPPORT 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: |
| 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 |
| sages 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 alloced 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 competed 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 privatee 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 mutlipath 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. |
| |