// Copyright (c) Vitaliy Filippov, 2019+ // License: VNPL-1.1 (see README.md for details) // // NFS RDMA support #ifdef WITH_RDMACM #define _XOPEN_SOURCE #include #include #include "addr_util.h" #include "proto/nfs.h" #include "proto/rpc.h" #include "proto/rpc_rdma.h" #include "nfs_proxy.h" #include "rdma_alloc.h" #define NFS_RDMACM_PRIVATE_DATA_MAGIC_LE 0x180eabf6 struct __attribute__((__packed__)) nfs_rdmacm_private { uint32_t format_identifier; // magic, should be 0xf6ab0e18 in big endian uint8_t version; // version, 1 uint8_t remote_invalidate; // remote invalidation flag (1 or 0) uint8_t max_send_size; // maximum RDMA Send operation size / 1024 - 1 (i.e. 0 is 1 KB, 255 is 256 KB) uint8_t max_recv_size; // maximum RDMA Receive operation size / 1024 - 1 (i.e. 0 is 1 KB, 255 is 256 KB) }; struct nfs_rdma_buf_t { void *buf = NULL; size_t len = 0; ibv_mr *mr = NULL; }; struct nfs_rdma_conn_t; struct nfs_rdma_dev_state_t { rdma_cm_id *cmid = NULL; ibv_device_attr dev_attr; ibv_comp_channel *channel = NULL; ibv_cq *cq = NULL; rdma_allocator_t *alloc = NULL; int max_cqe = 0; static nfs_rdma_dev_state_t *create(rdma_cm_id *cmid, uint64_t rdma_malloc_round_to, uint64_t rdma_max_unused_buffers); ~nfs_rdma_dev_state_t(); }; nfs_rdma_dev_state_t *nfs_rdma_dev_state_t::create(rdma_cm_id *cmid, uint64_t rdma_malloc_round_to, uint64_t rdma_max_unused_buffers) { nfs_rdma_dev_state_t *self = new nfs_rdma_dev_state_t; self->cmid = cmid; int r = ibv_query_device(cmid->verbs, &self->dev_attr); if (r != 0) { fprintf(stderr, "Failed to query listerning RDMA device: %s (code %d)\n", strerror(r), r); delete self; return NULL; } self->channel = ibv_create_comp_channel(cmid->verbs); if (!self->channel) { fprintf(stderr, "Couldn't create RDMA completion channel\n"); delete self; return NULL; } self->max_cqe = 4096; self->cq = ibv_create_cq(cmid->verbs, self->max_cqe, NULL, self->channel, 0); if (!self->cq) { fprintf(stderr, "Couldn't create RDMA completion queue\n"); delete self; return NULL; } self->alloc = rdma_malloc_create(cmid->pd, rdma_malloc_round_to, rdma_max_unused_buffers, IBV_ACCESS_LOCAL_WRITE); fcntl(self->channel->fd, F_SETFL, fcntl(self->channel->fd, F_GETFL, 0) | O_NONBLOCK); return self; } nfs_rdma_dev_state_t::~nfs_rdma_dev_state_t() { if (cq) { ibv_destroy_cq(cq); cq = NULL; } if (channel) { ibv_destroy_comp_channel(channel); channel = NULL; } if (alloc) { rdma_malloc_destroy(alloc); alloc = NULL; } } struct nfs_rdma_context_t { std::string bind_address; int rdmacm_port = 0; uint32_t max_iodepth = 16, max_send_wr = 1024; uint64_t rdma_malloc_round_to = 1048576, rdma_max_unused_buffers = 64*1048576; uint64_t max_send_size = 256*1024, max_recv_size = 256*1024; nfs_proxy_t *proxy = NULL; epoll_manager_t *epmgr = NULL; rdma_event_channel *rdmacm_evch = NULL; rdma_cm_id *listener_id = NULL; std::map rdma_connections; ~nfs_rdma_context_t(); void handle_rdmacm_events(); void rdmacm_accept(rdma_cm_event *ev); void rdmacm_established(rdma_cm_event *ev); }; struct nfs_rdma_conn_t { nfs_rdma_context_t *ctx = NULL; nfs_client_t *client = NULL; nfs_rdma_dev_state_t *conn_dev = NULL; rdma_cm_id *id = NULL; int max_send_size = 256*1024, max_recv_size = 256*1024; int max_buf_size = 256*1024; int remote_max_send_size = 1024, remote_max_recv_size = 1024; int max_rdma_reads = 16; bool remote_invalidate = true; bool established = false; uint32_t cur_credit = 16; uint32_t cur_send = 0; uint32_t cur_rdma_reads = 0; std::vector recv_buffers; std::map used_buffers; int next_recv_buf = 0; std::vector outbox; std::vector outbox_wrs; std::vector chunk_inbox, chunk_read_postponed; int outbox_pos = 0; void handle_io(); void post_initial_receives(); ~nfs_rdma_conn_t(); nfs_rdma_buf_t create_buf(size_t len); void post_recv(nfs_rdma_buf_t b); void post_send(); int post_chunk_reads(rpc_op_t *rop, bool push); bool handle_recv(void *buf, size_t len); void free_rdma_rpc_op(rpc_op_t *rop); void reuse_buffer(void *buf); void rdma_encode_header(XDR *xdrs, rpc_op_t *rop, bool nomsg); }; nfs_rdma_context_t* nfs_proxy_t::create_rdma(const std::string & bind_address, int rdmacm_port, uint32_t max_iodepth, uint32_t max_send_wr, uint64_t rdma_malloc_round_to, uint64_t rdma_max_unused_buffers) { nfs_rdma_context_t* self = new nfs_rdma_context_t; self->proxy = this; self->epmgr = epmgr; self->bind_address = bind_address; self->rdmacm_port = rdmacm_port; self->max_iodepth = max_iodepth; self->max_send_wr = max_send_wr ? max_send_wr : 1024; self->rdma_malloc_round_to = rdma_malloc_round_to; self->rdma_max_unused_buffers = rdma_max_unused_buffers; self->rdmacm_evch = rdma_create_event_channel(); if (!self->rdmacm_evch) { fprintf(stderr, "Failed to initialize RDMA-CM event channel: %s (code %d)\n", strerror(errno), errno); delete self; return NULL; } fcntl(self->rdmacm_evch->fd, F_SETFL, fcntl(self->rdmacm_evch->fd, F_GETFL, 0) | O_NONBLOCK); epmgr->tfd->set_fd_handler(self->rdmacm_evch->fd, false, [self](int rdmacm_eventfd, int epoll_events) { self->handle_rdmacm_events(); }); int r = rdma_create_id(self->rdmacm_evch, &self->listener_id, NULL, RDMA_PS_TCP); if (r != 0) { fprintf(stderr, "Failed to create RDMA-CM ID: %s (code %d)\n", strerror(errno), errno); delete self; return NULL; } sockaddr_storage addr; if (!string_to_addr(bind_address, 0, rdmacm_port, &addr)) { fprintf(stderr, "Server address: %s is not valid\n", bind_address.c_str()); delete self; return NULL; } r = rdma_bind_addr(self->listener_id, (sockaddr*)&addr); if (r != 0) { fprintf(stderr, "Failed to bind RDMA-CM to %s:%d: %s (code %d)\n", bind_address.c_str(), rdmacm_port, strerror(errno), errno); delete self; return NULL; } r = rdma_listen(self->listener_id, 128); if (r != 0) { fprintf(stderr, "Failed to listen RDMA-CM: %s (code %d)\n", strerror(errno), errno); delete self; return NULL; } return self; } void nfs_proxy_t::destroy_rdma() { if (rdma_context) { delete rdma_context; rdma_context = NULL; } } nfs_rdma_context_t::~nfs_rdma_context_t() { if (listener_id) { int r = rdma_destroy_id(listener_id); if (r != 0) fprintf(stderr, "Failed to destroy RDMA-CM ID: %s (code %d)\n", strerror(errno), errno); else listener_id = NULL; } if (rdmacm_evch) { epmgr->tfd->set_fd_handler(rdmacm_evch->fd, false, NULL); rdma_destroy_event_channel(rdmacm_evch); rdmacm_evch = NULL; } } void nfs_rdma_context_t::handle_rdmacm_events() { rdma_cm_event *ev = NULL; std::vector stop_clients; while (1) { int r = rdma_get_cm_event(rdmacm_evch, &ev); if (r != 0) { if (errno == EAGAIN || errno == EINTR) break; fprintf(stderr, "Failed to get RDMA-CM event: %s (code %d)\n", strerror(errno), errno); exit(1); } if (ev->event == RDMA_CM_EVENT_CONNECT_REQUEST) { rdmacm_accept(ev); } else if (ev->event == RDMA_CM_EVENT_CONNECT_ERROR || ev->event == RDMA_CM_EVENT_REJECTED || ev->event == RDMA_CM_EVENT_DISCONNECTED || ev->event == RDMA_CM_EVENT_DEVICE_REMOVAL) { auto event_type_name = ev->event == RDMA_CM_EVENT_CONNECT_ERROR ? "RDMA_CM_EVENT_CONNECT_ERROR" : ( ev->event == RDMA_CM_EVENT_REJECTED ? "RDMA_CM_EVENT_REJECTED" : ( ev->event == RDMA_CM_EVENT_DISCONNECTED ? "RDMA_CM_EVENT_DISCONNECTED" : "RDMA_CM_EVENT_DEVICE_REMOVAL")); auto conn_it = rdma_connections.find(ev->id); if (conn_it == rdma_connections.end()) { fprintf(stderr, "Received %s event for an unknown connection 0x%jx - ignoring\n", event_type_name, (uint64_t)ev->id); } else { fprintf(stderr, "Received %s event for connection 0x%jx - closing it\n", event_type_name, (uint64_t)ev->id); auto conn = conn_it->second; stop_clients.push_back(conn->client); } } else if (ev->event == RDMA_CM_EVENT_ESTABLISHED) { rdmacm_established(ev); } else if (ev->event == RDMA_CM_EVENT_ADDR_CHANGE || ev->event == RDMA_CM_EVENT_TIMEWAIT_EXIT) { // Do nothing } else { // Other events are unexpected fprintf(stderr, "Unexpected RDMA-CM event type: %d\n", ev->event); } r = rdma_ack_cm_event(ev); if (r != 0) { fprintf(stderr, "Failed to ack (free) RDMA-CM event: %s (code %d)\n", strerror(errno), errno); exit(1); } } // Stop only after flushing all events, otherwise rdma_destroy_id infinitely waits for pthread_cond for (auto cli: stop_clients) { cli->stop(); } } void nfs_rdma_context_t::rdmacm_accept(rdma_cm_event *ev) { nfs_rdma_dev_state_t *conn_dev = nfs_rdma_dev_state_t::create(ev->id, rdma_malloc_round_to, rdma_max_unused_buffers); if (!conn_dev) { rdma_destroy_id(ev->id); return; } ibv_qp_init_attr init_attr = { .send_cq = conn_dev->cq, .recv_cq = conn_dev->cq, .cap = { // each op at each moment takes 1 RDMA_RECV or 1 RDMA_READ or 1 RDMA_WRITE + 1 RDMA_SEND .max_send_wr = max_send_wr, .max_recv_wr = max_iodepth, .max_send_sge = 1, .max_recv_sge = 1, // we don't need S/G currently }, .qp_type = IBV_QPT_RC, }; int r = rdma_create_qp(ev->id, NULL, &init_attr); if (r != 0) { fprintf(stderr, "Failed to create a queue pair via RDMA-CM: %s (code %d)\n", strerror(errno), errno); delete conn_dev; rdma_destroy_id(ev->id); return; } assert(ev->id->qp->send_cq == conn_dev->cq); assert(ev->id->qp->recv_cq == conn_dev->cq); nfs_rdmacm_private private_data = { .format_identifier = NFS_RDMACM_PRIVATE_DATA_MAGIC_LE, .version = 1, .remote_invalidate = 1, .max_send_size = (uint8_t)(max_send_size <= 256*1024 ? max_send_size/1024 - 1 : 255), .max_recv_size = (uint8_t)(max_recv_size <= 256*1024 ? max_recv_size/1024 - 1 : 255), }; // should be responder_resources, but it's 0 for Linux NFS client // so probably RDMA-CM swaps these values in _accept for the "convenience" :) int max_rdma_reads = ev->param.conn.initiator_depth; // just 16 on ConnectX-4... if (!max_rdma_reads || max_rdma_reads > conn_dev->dev_attr.max_qp_rd_atom) max_rdma_reads = conn_dev->dev_attr.max_qp_rd_atom; if (max_rdma_reads > conn_dev->dev_attr.max_qp_init_rd_atom) max_rdma_reads = conn_dev->dev_attr.max_qp_init_rd_atom; if (max_rdma_reads > 255) max_rdma_reads = 255; rdma_conn_param conn_params = { .private_data = &private_data, .private_data_len = sizeof(private_data), .responder_resources = (uint8_t)max_rdma_reads, // min(max_qp_rd_atom of the local device, max_qp_init_rd_atom of the remote device) .initiator_depth = (uint8_t)max_rdma_reads, // max_qp_init_rd_atom of the local device .rnr_retry_count = 7, }; r = rdma_accept(ev->id, &conn_params); if (r != 0) { fprintf(stderr, "Failed to accept RDMA-CM connection: %s (code %d)\n", strerror(errno), errno); rdma_destroy_qp(ev->id); delete conn_dev; rdma_destroy_id(ev->id); } else { auto conn = new nfs_rdma_conn_t(); conn->ctx = this; conn->id = ev->id; conn->cur_credit = max_iodepth; conn->max_send_size = max_send_size; conn->max_recv_size = max_recv_size; conn->max_rdma_reads = max_rdma_reads; rdma_connections[ev->id] = conn; // Handle NFS private_data if (ev->param.conn.private_data_len >= sizeof(nfs_rdmacm_private)) { nfs_rdmacm_private *private_data = (nfs_rdmacm_private *)ev->param.conn.private_data; if (private_data->format_identifier == NFS_RDMACM_PRIVATE_DATA_MAGIC_LE && private_data->version == 1) { conn->remote_invalidate = private_data->remote_invalidate; conn->remote_max_send_size = (private_data->max_send_size+1) * 1024; conn->remote_max_recv_size = (private_data->max_recv_size+1) * 1024; if (conn->remote_max_recv_size < conn->max_send_size) conn->max_send_size = conn->remote_max_recv_size; } } auto cli = this->proxy->create_client(); cli->rdma_conn = conn; conn->client = cli; conn->conn_dev = conn_dev; epmgr->tfd->set_fd_handler(conn_dev->channel->fd, false, [conn](int channel_eventfd, int epoll_events) { conn->handle_io(); }); // run handle_io() once to reset poll state conn->handle_io(); // Post initial receive requests conn->post_initial_receives(); } } nfs_rdma_conn_t::~nfs_rdma_conn_t() { assert(!outbox.size()); assert(!chunk_inbox.size()); assert(!chunk_read_postponed.size()); for (auto & b: recv_buffers) { ibv_dereg_mr(b.mr); free(b.buf); } recv_buffers.clear(); if (conn_dev) { ctx->epmgr->tfd->set_fd_handler(conn_dev->channel->fd, false, NULL); delete conn_dev; conn_dev = NULL; } if (id) { ctx->rdma_connections.erase(id); if (id->qp) { rdma_destroy_qp(id); } rdma_destroy_id(id); } } void nfs_rdma_context_t::rdmacm_established(rdma_cm_event *ev) { auto conn_it = rdma_connections.find(ev->id); if (conn_it == rdma_connections.end()) { fprintf(stderr, "Received RDMA_CM_EVENT_ESTABLISHED event for an unknown connection 0x%jx - ignoring\n", (uint64_t)ev->id); return; } fprintf(stderr, "Received RDMA_CM_EVENT_ESTABLISHED event for connection 0x%jx - connection established\n", (uint64_t)ev->id); auto conn = conn_it->second; conn->established = true; } void nfs_rdma_conn_t::post_initial_receives() { for (int i = 0; i < cur_credit; i++) { auto b = create_buf(max_recv_size); recv_buffers.push_back(b); used_buffers[b.buf] = b; post_recv(b); } } nfs_rdma_buf_t nfs_rdma_conn_t::create_buf(size_t len) { nfs_rdma_buf_t b; b.buf = malloc_or_die(len); b.len = len; b.mr = ibv_reg_mr(id->pd, b.buf, len, IBV_ACCESS_LOCAL_WRITE); if (!b.mr) { fprintf(stderr, "Failed to register RDMA memory region: %s\n", strerror(errno)); exit(1); } return b; } void nfs_rdma_conn_t::post_recv(nfs_rdma_buf_t b) { if (client->stopped) { return; } ibv_sge sge = { .addr = (uintptr_t)b.buf, .length = (uint32_t)b.len, .lkey = b.mr->lkey, }; ibv_recv_wr *bad_wr = NULL; ibv_recv_wr wr = { .wr_id = 1, // 1 is any read, 2 is any write :) .sg_list = &sge, .num_sge = 1, }; int err = ibv_post_recv(id->qp, &wr, &bad_wr); if (err || bad_wr) { fprintf(stderr, "RDMA receive failed: %s\n", strerror(err)); exit(1); } } void nfs_rdma_conn_t::rdma_encode_header(XDR *xdrs, rpc_op_t *rop, bool nomsg) { rdma_msg outrmsg = { .rdma_xid = rop->in_rdma_msg.rdma_xid, .rdma_vers = rop->in_rdma_msg.rdma_vers, .rdma_credit = cur_credit, .rdma_body = { .proc = rop->rdma_error ? RDMA_ERROR : (nomsg ? RDMA_NOMSG : RDMA_MSG), }, }; if (rop->rdma_error) { outrmsg.rdma_body.rdma_error.err = rop->rdma_error; if (rop->rdma_error == ERR_VERS) outrmsg.rdma_body.rdma_error.range = (rpc_rdma_errvers){ 1, 1 }; } else { // Copy chunks... it's a real shit outrmsg.rdma_body.rdma_msg = (rpc_rdma_header){ .rdma_writes = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes, .rdma_reply = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reply, }; } int r = xdr_encode(xdrs, (xdrproc_t)xdr_rdma_msg, &outrmsg); assert(r); } void nfs_client_t::rdma_queue_reply(rpc_op_t *rop) { rdma_conn->outbox.push_back(rop); rdma_conn->post_send(); } void nfs_rdma_conn_t::post_send() { while (chunk_read_postponed.size() > 0) { int posted = post_chunk_reads(chunk_read_postponed[0], false); if (posted) chunk_read_postponed.erase(chunk_read_postponed.begin(), chunk_read_postponed.begin()+1); else break; } while (outbox.size() > outbox_pos) { auto rop = outbox[outbox_pos]; if (rop->buffer) { reuse_buffer(rop->buffer); rop->buffer = NULL; } XDR *hdr_xdr = ctx->proxy->get_xdr(); send_again: rdma_encode_header(hdr_xdr, rop, false); size_t hdr_size = xdr_encode_get_size(hdr_xdr); iovec *chunk_iov = NULL; iovec *iov_list = NULL; unsigned iov_count = 0; if (!rop->rdma_error) { xdr_encode_finish(rop->xdrs, &iov_list, &iov_count); assert(iov_count > 0); // READ3resok and READLINK3resok - extract last byte buffer from iovecs and send it in a "write chunk" if (rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes && rop->in_msg.body.cbody.prog == NFS_PROGRAM && (rop->in_msg.body.cbody.proc == NFS3_READ && ((READ3res*)rop->reply)->status == NFS3_OK || rop->in_msg.body.cbody.proc == NFS3_READLINK && ((READLINK3res*)rop->reply)->status == NFS3_OK)) { assert(iov_count > 1); iov_count--; chunk_iov = &iov_list[iov_count]; auto expected_size = (rop->in_msg.body.cbody.proc == NFS3_READ ? ((READ3res*)rop->reply)->resok.count : ((READLINK3res*)rop->reply)->resok.data.size); assert(chunk_iov->iov_len == expected_size); } } size_t msg_size = 0; for (unsigned i = 0; i < iov_count; i++) { msg_size += iov_list[i].iov_len; } // Estimate reply WR count, create WR and SGE arrays xdr_write_chunk *reply_chunk = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reply; int reply_chunk_wr_count = (reply_chunk ? reply_chunk->target.target_len : 0); uint32_t wr_count = 1 + (chunk_iov ? 1 : 0) + (reply_chunk ? reply_chunk_wr_count : 0); if (wr_count > ctx->max_send_wr) { fprintf(stderr, "Reply fragmentation (%u) exceeds max_send_wr (%u), sending ERR_CHUNK\n", wr_count, ctx->max_send_wr); chunk_error: xdr_reset(hdr_xdr); rop->rdma_error = ERR_CHUNK; goto send_again; } if (msg_size+hdr_size > max_send_size && !reply_chunk) { fprintf(stderr, "RPC message size (%zu) exceeds client's max_send_size (%u)" " and reply chunk is not provided, sending ERR_CHUNK\n", msg_size+hdr_size, max_send_size); goto chunk_error; } if (chunk_iov && chunk_iov->iov_len > rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val[0].length) { fprintf(stderr, "%s write chunk size (%u) is smaller than read size (%zu), sending ERR_CHUNK\n", rop->in_msg.body.cbody.proc == NFS3_READ ? "READ3" : "READLINK3", rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val[0].length, chunk_iov->iov_len); goto chunk_error; } if (cur_send + wr_count > ctx->max_send_wr) { // Retry later ctx->proxy->free_xdr(hdr_xdr); return; } // Check reply chunk size and set actual reply segment sizes bool reencode = false; if (reply_chunk) { size_t reply_chunk_len = 0; size_t left = msg_size; for (uint32_t i = 0; i < reply_chunk->target.target_len; i++) { reply_chunk_len += reply_chunk->target.target_val[i].length; if (reply_chunk->target.target_val[i].length > left) reply_chunk->target.target_val[i].length = left; left -= reply_chunk->target.target_val[i].length; } if (left > 0) { // Message doesn't fit even in the reply chunk fprintf(stderr, "RPC message payload size (%zu) exceeds reply chunk size (%zu), sending ERR_CHUNK\n", msg_size, reply_chunk_len); goto chunk_error; } reencode = true; } if (chunk_iov && chunk_iov->iov_len < rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val[0].length) { // Set actual write chunk size reencode = true; rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val[0].length = chunk_iov->iov_len; } if (reencode) { // ...and re-encode the header :-( shitty protocol xdr_reset(hdr_xdr); rdma_encode_header(hdr_xdr, rop, reply_chunk != NULL); hdr_size = xdr_encode_get_size(hdr_xdr); } ibv_sge sges[wr_count]; ibv_send_wr wrs[wr_count]; int wr_pos = 0; // Use a buffer from rdma_malloc for the reply assert(!rop->buffer); rop->buffer = rdma_malloc_alloc(conn_dev->alloc, hdr_size+msg_size); auto buf_lkey = rdma_malloc_get_lkey(conn_dev->alloc, rop->buffer); size_t pos = 0; { // Copy and free the RDMA-RPC header iovec *hdr_iov_list = NULL; unsigned hdr_iov_count = 0; xdr_encode_finish(hdr_xdr, &hdr_iov_list, &hdr_iov_count); assert(hdr_iov_count > 0); for (unsigned i = 0; i < hdr_iov_count; i++) { memcpy((uint8_t*)rop->buffer + pos, hdr_iov_list[i].iov_base, hdr_iov_list[i].iov_len); pos += hdr_iov_list[i].iov_len; } assert(pos == hdr_size); ctx->proxy->free_xdr(hdr_xdr); } for (unsigned i = 0; i < iov_count; i++) { memcpy((uint8_t*)rop->buffer + pos, iov_list[i].iov_base, iov_list[i].iov_len); pos += iov_list[i].iov_len; } // Include header size in msg_size now and then msg_size += hdr_size; assert(msg_size == pos); // Check if we have to use the reply chunk if (reply_chunk) { size_t pos = hdr_size; for (uint32_t i = 0; i < reply_chunk->target.target_len && pos < msg_size; i++) { uint32_t len = (reply_chunk->target.target_val[i].length < msg_size-pos ? reply_chunk->target.target_val[i].length : msg_size-pos); sges[wr_pos] = { .addr = (uintptr_t)((uint8_t*)rop->buffer + pos), .length = len, .lkey = buf_lkey, }; wrs[wr_pos] = (ibv_send_wr){ .wr_id = 4, // 4 is chunk write .opcode = IBV_WR_RDMA_WRITE, .wr = { .rdma = { .remote_addr = reply_chunk->target.target_val[i].offset, .rkey = reply_chunk->target.target_val[i].handle, }, }, }; wr_pos++; pos += len; } } // Check if we have a DDP chunk xdr_rdma_segment *wr_chunk = NULL; if (chunk_iov != NULL) { wr_chunk = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_writes->entry.target.target_val; sges[wr_pos] = { .addr = (uintptr_t)chunk_iov->iov_base, .length = (uint32_t)chunk_iov->iov_len, .lkey = rdma_malloc_get_lkey(conn_dev->alloc, chunk_iov->iov_base), }; wrs[wr_pos] = (ibv_send_wr){ .wr_id = 4, // 4 is chunk write .opcode = IBV_WR_RDMA_WRITE, .wr = { .rdma = { .remote_addr = wr_chunk->offset, .rkey = wr_chunk->handle, }, }, }; wr_pos++; } // Add the normal send sges[wr_pos] = { .addr = (uintptr_t)rop->buffer, .length = (uint32_t)(reply_chunk ? hdr_size : msg_size), .lkey = buf_lkey, }; wrs[wr_pos] = (ibv_send_wr){ .wr_id = 2, // 2 is send .opcode = remote_invalidate && !reply_chunk && wr_chunk ? IBV_WR_SEND_WITH_INV : IBV_WR_SEND, .send_flags = IBV_SEND_SIGNALED, .invalidate_rkey = remote_invalidate && !reply_chunk && wr_chunk ? wr_chunk->handle : 0, }; wr_pos++; // Send it all for (int i = 0; i < wr_pos; i++) { wrs[i].next = &wrs[i+1]; wrs[i].sg_list = &sges[i]; wrs[i].num_sge = 1; } wrs[wr_pos-1].next = NULL; ibv_send_wr *bad_wr = NULL; int err = ibv_post_send(id->qp, &wrs[0], &bad_wr); if (err || bad_wr) { fprintf(stderr, "Posting RDMA send failed: %s\n", strerror(err)); exit(1); } cur_send += wr_pos; outbox_wrs.push_back(wr_pos); outbox_pos++; } } #define RDMA_EVENTS_AT_ONCE 32 void nfs_rdma_conn_t::handle_io() { auto conn = this; // Request next notification ibv_cq *ev_cq; void *ev_ctx; // This is inefficient as it calls read()... (but there is no other way) if (ibv_get_cq_event(conn_dev->channel, &ev_cq, &ev_ctx) == 0) { ibv_ack_cq_events(conn_dev->cq, 1); } if (ibv_req_notify_cq(conn_dev->cq, 0) != 0) { fprintf(stderr, "Failed to request RDMA completion notification, exiting\n"); exit(1); } ibv_wc wc[RDMA_EVENTS_AT_ONCE]; int event_count; do { event_count = ibv_poll_cq(conn_dev->cq, RDMA_EVENTS_AT_ONCE, wc); for (int i = 0; i < event_count; i++) { if (wc[i].status != IBV_WC_SUCCESS) { fprintf(stderr, "RDMA work request failed for queue %d with status: %s, stopping client\n", wc[i].qp_num, ibv_wc_status_str(wc[i].status)); conn->client->stop(); // but continue to handle events to purge the queue } if (wc[i].wr_id == 1) { // 1 = receive auto b = conn->recv_buffers[conn->next_recv_buf]; // Due to the credit-based flow control in RPC-RDMA, we can just remove that buffer and reuse it later conn->recv_buffers.erase(conn->recv_buffers.begin()+conn->next_recv_buf, conn->recv_buffers.begin()+conn->next_recv_buf+1); if (conn->cur_credit > 0 && !conn->client->stopped) { // Increase client refcount while the RPC call is being processed conn->client->refs++; conn->cur_credit--; conn->handle_recv(b.buf, wc[i].byte_len); } else { fprintf(stderr, "Warning: NFS client credit exceeded for queue %d, stopping client\n", wc[i].qp_num); conn->client->stop(); } } else if (wc[i].wr_id == 2) { // 2 = send auto rop = conn->outbox[0]; conn->cur_send -= conn->outbox_wrs[0]; conn->outbox.erase(conn->outbox.begin(), conn->outbox.begin()+1); conn->outbox_wrs.erase(conn->outbox_wrs.begin(), conn->outbox_wrs.begin()+1); conn->outbox_pos--; // Retry send for corner cases of exceeded max_send_wr conn->post_send(); // Free rpc_op conn->free_rdma_rpc_op(rop); } else if (wc[i].wr_id == 3) { // 3 = chunk read auto rop = conn->chunk_inbox[0]; conn->chunk_inbox.erase(conn->chunk_inbox.begin(), conn->chunk_inbox.begin()+1); size_t read_chunk_count = 0; for (auto cur = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reads; cur; cur = cur->next) read_chunk_count++; conn->cur_send -= read_chunk_count; conn->cur_rdma_reads -= read_chunk_count; conn->client->handle_rpc_op(rop); // Retry send for corner cases of exceeded max_send_wr conn->post_send(); } else { fprintf(stderr, "BUG: unknown RDMA work completion with wr_id=%ju\n", wc[i].wr_id); } } } while (event_count > 0); } void nfs_rdma_conn_t::free_rdma_rpc_op(rpc_op_t *rop) { if (rop->buffer) { rdma_malloc_free(conn_dev->alloc, rop->buffer); rop->buffer = NULL; } auto rdma_chunk = xdr_get_rdma_chunk(rop->xdrs); if (rdma_chunk) { rdma_malloc_free(conn_dev->alloc, rdma_chunk); xdr_set_rdma_chunk(rop->xdrs, NULL); } ctx->proxy->free_xdr(rop->xdrs); free(rop); cur_credit++; client->deref(); } void nfs_rdma_conn_t::reuse_buffer(void *buf) { auto ub = used_buffers.at(buf); recv_buffers.push_back(ub); post_recv(ub); } // returns false if handling is done, returns true if handling is continued asynchronously bool nfs_rdma_conn_t::handle_recv(void *buf, size_t len) { XDR *xdrs = ctx->proxy->get_xdr(); xdr_set_rdma(xdrs); // Decode the RDMA-RPC header rdma_msg rmsg; if (!xdr_decode(xdrs, buf, len, (xdrproc_t)xdr_rdma_msg, &rmsg)) { // Invalid message, ignore it fprintf(stderr, "Invalid RDMA-RPC header on connection 0x%jx, ignoring message\n", (uint64_t)this); ignore_msg: reuse_buffer(buf); ctx->proxy->free_xdr(xdrs); client->deref(); return 0; } if (rmsg.rdma_vers != 1 || rmsg.rdma_body.proc != RDMA_MSG) { // Bad RDMA-RPC version or message type fprintf( stderr, "Unsupported RDMA-RPC version (%d) or message type (%d), sending %s\n", rmsg.rdma_vers, rmsg.rdma_body.proc, rmsg.rdma_vers != 1 ? "ERR_VERS" : "ERR_CHUNK" ); rpc_op_t *rop = (rpc_op_t*)malloc_or_die(sizeof(rpc_op_t)); *rop = (rpc_op_t){ .client = this, .xdrs = xdrs, .in_msg = { .xid = rmsg.rdma_xid, }, .in_rdma_msg = rmsg, .rdma_error = rmsg.rdma_vers != 1 ? ERR_VERS : ERR_CHUNK, }; rpc_queue_reply(rop); // Incoming buffer isn't needed to handle request, so return 0 return 0; } rpc_msg inmsg; if (!xdr_rpc_msg(xdrs, &inmsg)) { // Invalid message, ignore it fprintf(stderr, "Invalid RDMA-RPC body on connection 0x%jx, ignoring message\n", (uint64_t)this); goto ignore_msg; } if (inmsg.xid != rmsg.rdma_xid) { fprintf(stderr, "RPC XID (%u) and RDMA XID (%u) mismatch on connection 0x%jx, ignoring message\n", inmsg.xid, rmsg.rdma_xid, (uint64_t)this); goto ignore_msg; } if (inmsg.body.dir != RPC_CALL) { fprintf(stderr, "Non-RPC_CALL message direction (%d) on connection 0x%jx, ignoring message\n", inmsg.body.dir, (uint64_t)this); goto ignore_msg; } rpc_op_t *rop = client->create_rpc_op(xdrs, buf, &inmsg, &rmsg); if (!rop) { // No such procedure return 0; } // Read chunks may only be provided for WRITE3 and SYMLINK3 if (inmsg.body.cbody.prog == NFS_PROGRAM && rmsg.rdma_body.rdma_msg.rdma_reads && inmsg.body.cbody.proc != NFS3_WRITE && inmsg.body.cbody.proc != NFS3_SYMLINK) { fprintf(stderr, "Read chunk(s) are provided for non-WRITE or SYMLINK operation (procedure %u), sending ERR_CHUNK\n", inmsg.body.cbody.proc); rop->rdma_error = ERR_CHUNK; rpc_queue_reply(rop); return 0; } // Write chunk may be provided only for READ3 and READLINK3 if (inmsg.body.cbody.prog == NFS_PROGRAM && rmsg.rdma_body.rdma_msg.rdma_writes && inmsg.body.cbody.proc != NFS3_READ && inmsg.body.cbody.proc != NFS3_READLINK) { fprintf(stderr, "Write chunk(s) are provided for non-READ or READLINK operation (procedure %u), sending ERR_CHUNK\n", inmsg.body.cbody.proc); rop->rdma_error = ERR_CHUNK; rpc_queue_reply(rop); return 0; } // Only 1 write chunk is supported if (inmsg.body.cbody.prog == NFS_PROGRAM && rmsg.rdma_body.rdma_msg.rdma_writes && ( rmsg.rdma_body.rdma_msg.rdma_writes->next || rmsg.rdma_body.rdma_msg.rdma_writes->entry.target.target_len != 1)) { int n = 0, seg = 0; for (auto cur = rmsg.rdma_body.rdma_msg.rdma_writes; cur; cur = cur->next) { n++; seg += cur->entry.target.target_len; } fprintf(stderr, "Only 1 write chunk with 1 segment may be provided, but we got %d chunks with %d segments, sending ERR_CHUNK\n", n, seg); rop->rdma_error = ERR_CHUNK; rpc_queue_reply(rop); return 0; } // Process read chunk(s) if (rmsg.rdma_body.rdma_msg.rdma_reads) { int r = post_chunk_reads(rop, true); return (r >= 0 ? 1 : 0); } return client->handle_rpc_op(rop); } int nfs_rdma_conn_t::post_chunk_reads(rpc_op_t *rop, bool push) { rop->referenced = 1; size_t read_chunk_size = 0; size_t read_chunk_count = 0; for (auto cur = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reads; cur; cur = cur->next) { read_chunk_size += cur->entry.target.length; read_chunk_count++; } if (read_chunk_count > max_rdma_reads) { fprintf(stderr, "Read chunk count (%zu) exceeds max_rdma_reads (%d), sending ERR_CHUNK\n", read_chunk_count, max_rdma_reads); rop->rdma_error = ERR_CHUNK; rpc_queue_reply(rop); return -1; } if (cur_send+read_chunk_count > ctx->max_send_wr || cur_rdma_reads+read_chunk_count > max_rdma_reads) { // Send it later if (push) chunk_read_postponed.push_back(rop); return 0; } void *buf = rdma_malloc_alloc(conn_dev->alloc, read_chunk_size); auto buf_lkey = rdma_malloc_get_lkey(conn_dev->alloc, buf); ibv_sge chunk_sge[read_chunk_count]; ibv_send_wr chunk_wr[read_chunk_count]; size_t i = 0; size_t pos = 0; for (auto cur = rop->in_rdma_msg.rdma_body.rdma_msg.rdma_reads; cur; cur = cur->next) { chunk_sge[i] = { .addr = (uintptr_t)((uint8_t*)buf + pos), .length = cur->entry.target.length, .lkey = buf_lkey, }; chunk_wr[i] = (ibv_send_wr){ .wr_id = 3, // 3 is chunk read .next = (i == read_chunk_count-1 ? NULL : &chunk_wr[i+1]), .sg_list = &chunk_sge[i], .num_sge = 1, .opcode = IBV_WR_RDMA_READ, .send_flags = (unsigned)(i == read_chunk_count-1 ? IBV_SEND_SIGNALED : 0), .wr = { .rdma = { .remote_addr = cur->entry.target.offset, .rkey = cur->entry.target.handle, }, }, }; pos += cur->entry.target.length; i++; } assert(i == read_chunk_count); assert(pos == read_chunk_size); ibv_send_wr *bad_wr = NULL; int err = ibv_post_send(id->qp, &chunk_wr[0], &bad_wr); if (err || bad_wr) { fprintf(stderr, "Posting RDMA read failed: %s\n", strerror(err)); exit(1); } xdr_set_rdma_chunk(rop->xdrs, buf); chunk_inbox.push_back(rop); cur_send += read_chunk_count; cur_rdma_reads += read_chunk_count; return 1; } void *nfs_client_t::rdma_malloc(size_t size) { return rdma_malloc_alloc(rdma_conn->conn_dev->alloc, size); } void nfs_client_t::rdma_free(void *buf) { rdma_malloc_free(rdma_conn->conn_dev->alloc, buf); } void nfs_client_t::destroy_rdma_conn() { if (rdma_conn) { delete rdma_conn; rdma_conn = NULL; } } #endif