From 9556eeae4529cd20a15321710f84ccb09e0cde35 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Thu, 3 Apr 2025 13:05:58 +0300 Subject: [PATCH] Implement io_uring zero-copy send support --- docs/config/network.en.md | 29 ++++++++++++++++++++ docs/config/network.ru.md | 29 ++++++++++++++++++++ docs/config/src/network.yml | 49 +++++++++++++++++++++++++++++++++ src/client/messenger.cpp | 8 ++++++ src/client/messenger.h | 11 +++++--- src/client/msgr_send.cpp | 54 ++++++++++++++++++++++++++++++++----- src/util/ringloop.cpp | 29 +++++++++++++++++++- src/util/ringloop.h | 17 ++++++++++++ 8 files changed, 215 insertions(+), 11 deletions(-) diff --git a/docs/config/network.en.md b/docs/config/network.en.md index 1592bcfa..bd9b0a1c 100644 --- a/docs/config/network.en.md +++ b/docs/config/network.en.md @@ -34,6 +34,7 @@ between clients, OSDs and etcd. - [etcd_ws_keepalive_interval](#etcd_ws_keepalive_interval) - [etcd_min_reload_interval](#etcd_min_reload_interval) - [tcp_header_buffer_size](#tcp_header_buffer_size) +- [min_zerocopy_send_size](#min_zerocopy_send_size) - [use_sync_send_recv](#use_sync_send_recv) ## osd_network @@ -313,6 +314,34 @@ is received without an additional copy. You can try to play with this parameter and see how it affects random iops and linear bandwidth if you want. +## min_zerocopy_send_size + +- Type: integer +- Default: 32768 + +OSDs and clients will attempt to use io_uring-based zero-copy TCP send +for buffers larger than this number of bytes. Zero-copy send with io_uring is +supported since Linux kernel version 6.1. Support is auto-detected and disabled +automatically when not available. It can also be disabled explicitly by setting +this parameter to a negative value. + +⚠️ Warning! Zero-copy send performance may vary greatly from CPU to CPU and from +one kernel version to another. Generally, it tends to only make benefit with larger +messages. With smaller messages (say, 4 KB), it may actually be slower. 32 KB is +enough for almost all CPUs, but even smaller values are optimal for some of them. +For example, 4 KB is OK for EPYC Milan/Genoa and 12 KB is OK for Xeon Ice Lake +(but verify it yourself please). + +Verification instructions: +1. Add `iommu=pt` into your Linux kernel command line and reboot. +2. Upgrade your kernel. For example, it's very important to use 6.11+ with recent AMD EPYCs. +3. Run some tests with the [send-zerocopy liburing example](https://github.com/axboe/liburing/blob/master/examples/send-zerocopy.c) + to find the minimal message size for which zero-copy is optimal. + Use `./send-zerocopy tcp -4 -R` at the server side and + `time ./send-zerocopy tcp -4 -b 0 -s BUFFER_SIZE -D SERVER_IP` at the client side with + `-z 0` (no zero-copy) and `-z 1` (zero-copy), and compare MB/s and used CPU time + (user+system). + ## use_sync_send_recv - Type: boolean diff --git a/docs/config/network.ru.md b/docs/config/network.ru.md index 5f1f7ea5..cddef22b 100644 --- a/docs/config/network.ru.md +++ b/docs/config/network.ru.md @@ -34,6 +34,7 @@ - [etcd_ws_keepalive_interval](#etcd_ws_keepalive_interval) - [etcd_min_reload_interval](#etcd_min_reload_interval) - [tcp_header_buffer_size](#tcp_header_buffer_size) +- [min_zerocopy_send_size](#min_zerocopy_send_size) - [use_sync_send_recv](#use_sync_send_recv) ## osd_network @@ -321,6 +322,34 @@ Vitastor содержат 128-байтные заголовки, за котор поменять этот параметр и посмотреть, как он влияет на производительность случайного и линейного доступа. +## min_zerocopy_send_size + +- Тип: целое число +- Значение по умолчанию: 32768 + +OSD и клиенты будут пробовать использовать TCP-отправку без копирования (zero-copy) на +основе io_uring для буферов, больших, чем это число байт. Отправка без копирования +поддерживается в io_uring, начиная с версии ядра Linux 6.1. Наличие поддержки +проверяется автоматически и zero-copy отключается, когда поддержки нет. Также +её можно отключить явно, установив данный параметр в отрицательное значение. + +⚠️ Внимание! Производительность данной функции может сильно отличаться на разных +процессорах и на разных версиях ядра Linux. В целом, zero-copy обычно быстрее с +большими сообщениями, а с мелкими (например, 4 КБ) zero-copy может быть даже +медленнее. 32 КБ достаточно почти для всех процессоров, но для каких-то можно +использовать даже меньшие значения. Например, для EPYC Milan/Genoa подходит 4 КБ, +а для Xeon Ice Lake - 12 КБ (но, пожалуйста, перепроверьте это сами). + +Инструкция по проверке: +1. Добавьте `iommu=pt` в командную строку загрузки вашего ядра Linux и перезагрузитесь. +2. Обновите ядро. Например, для AMD EPYC очень важно использовать версию 6.11+. +3. Позапускайте тесты с помощью [send-zerocopy из примеров liburing](https://github.com/axboe/liburing/blob/master/examples/send-zerocopy.c), + чтобы найти минимальный размер сообщения, для которого zero-copy отправка оптимальна. + Запускайте `./send-zerocopy tcp -4 -R` на стороне сервера и + `time ./send-zerocopy tcp -4 -b 0 -s РАЗМЕР_БУФЕРА -D АДРЕС_СЕРВЕРА` на стороне клиента + с опцией `-z 0` (обычная отправка) и `-z 1` (отправка без копирования), и сравнивайте + скорость в МБ/с и занятое процессорное время (user+system). + ## use_sync_send_recv - Тип: булево (да/нет) diff --git a/docs/config/src/network.yml b/docs/config/src/network.yml index 53af6b72..47ee5a1f 100644 --- a/docs/config/src/network.yml +++ b/docs/config/src/network.yml @@ -373,6 +373,55 @@ параметра читается без дополнительного копирования. Вы можете попробовать поменять этот параметр и посмотреть, как он влияет на производительность случайного и линейного доступа. +- name: min_zerocopy_send_size + type: int + default: 32768 + info: | + OSDs and clients will attempt to use io_uring-based zero-copy TCP send + for buffers larger than this number of bytes. Zero-copy send with io_uring is + supported since Linux kernel version 6.1. Support is auto-detected and disabled + automatically when not available. It can also be disabled explicitly by setting + this parameter to a negative value. + + ⚠️ Warning! Zero-copy send performance may vary greatly from CPU to CPU and from + one kernel version to another. Generally, it tends to only make benefit with larger + messages. With smaller messages (say, 4 KB), it may actually be slower. 32 KB is + enough for almost all CPUs, but even smaller values are optimal for some of them. + For example, 4 KB is OK for EPYC Milan/Genoa and 12 KB is OK for Xeon Ice Lake + (but verify it yourself please). + + Verification instructions: + 1. Add `iommu=pt` into your Linux kernel command line and reboot. + 2. Upgrade your kernel. For example, it's very important to use 6.11+ with recent AMD EPYCs. + 3. Run some tests with the [send-zerocopy liburing example](https://github.com/axboe/liburing/blob/master/examples/send-zerocopy.c) + to find the minimal message size for which zero-copy is optimal. + Use `./send-zerocopy tcp -4 -R` at the server side and + `time ./send-zerocopy tcp -4 -b 0 -s BUFFER_SIZE -D SERVER_IP` at the client side with + `-z 0` (no zero-copy) and `-z 1` (zero-copy), and compare MB/s and used CPU time + (user+system). + info_ru: | + OSD и клиенты будут пробовать использовать TCP-отправку без копирования (zero-copy) на + основе io_uring для буферов, больших, чем это число байт. Отправка без копирования + поддерживается в io_uring, начиная с версии ядра Linux 6.1. Наличие поддержки + проверяется автоматически и zero-copy отключается, когда поддержки нет. Также + её можно отключить явно, установив данный параметр в отрицательное значение. + + ⚠️ Внимание! Производительность данной функции может сильно отличаться на разных + процессорах и на разных версиях ядра Linux. В целом, zero-copy обычно быстрее с + большими сообщениями, а с мелкими (например, 4 КБ) zero-copy может быть даже + медленнее. 32 КБ достаточно почти для всех процессоров, но для каких-то можно + использовать даже меньшие значения. Например, для EPYC Milan/Genoa подходит 4 КБ, + а для Xeon Ice Lake - 12 КБ (но, пожалуйста, перепроверьте это сами). + + Инструкция по проверке: + 1. Добавьте `iommu=pt` в командную строку загрузки вашего ядра Linux и перезагрузитесь. + 2. Обновите ядро. Например, для AMD EPYC очень важно использовать версию 6.11+. + 3. Позапускайте тесты с помощью [send-zerocopy из примеров liburing](https://github.com/axboe/liburing/blob/master/examples/send-zerocopy.c), + чтобы найти минимальный размер сообщения, для которого zero-copy отправка оптимальна. + Запускайте `./send-zerocopy tcp -4 -R` на стороне сервера и + `time ./send-zerocopy tcp -4 -b 0 -s РАЗМЕР_БУФЕРА -D АДРЕС_СЕРВЕРА` на стороне клиента + с опцией `-z 0` (обычная отправка) и `-z 1` (отправка без копирования), и сравнивайте + скорость в МБ/с и занятое процессорное время (user+system). - name: use_sync_send_recv type: bool default: false diff --git a/src/client/messenger.cpp b/src/client/messenger.cpp index 617f2b3f..8afc9b47 100644 --- a/src/client/messenger.cpp +++ b/src/client/messenger.cpp @@ -167,6 +167,10 @@ void osd_messenger_t::init() } } #endif + if (ringloop) + { + has_sendmsg_zc = ringloop->has_sendmsg_zc(); + } if (ringloop && iothread_count > 0) { for (int i = 0; i < iothread_count; i++) @@ -329,6 +333,9 @@ void osd_messenger_t::parse_config(const json11::Json & config) this->receive_buffer_size = 65536; this->use_sync_send_recv = config["use_sync_send_recv"].bool_value() || config["use_sync_send_recv"].uint64_value(); + this->min_zerocopy_send_size = config["min_zerocopy_send_size"].is_null() + ? DEFAULT_MIN_ZEROCOPY_SEND_SIZE + : (int)config["min_zerocopy_send_size"].int64_value(); this->peer_connect_interval = config["peer_connect_interval"].uint64_value(); if (!this->peer_connect_interval) this->peer_connect_interval = 5; @@ -897,6 +904,7 @@ static const char* local_only_params[] = { "tcp_header_buffer_size", "use_rdma", "use_sync_send_recv", + "min_zerocopy_send_size", }; static const char **local_only_end = local_only_params + (sizeof(local_only_params)/sizeof(local_only_params[0])); diff --git a/src/client/messenger.h b/src/client/messenger.h index e44861ad..3cf93707 100644 --- a/src/client/messenger.h +++ b/src/client/messenger.h @@ -32,6 +32,8 @@ #define VITASTOR_CONFIG_PATH "/etc/vitastor/vitastor.conf" +#define DEFAULT_MIN_ZEROCOPY_SEND_SIZE 32*1024 + #define MSGR_SENDP_HDR 1 #define MSGR_SENDP_FREE 2 @@ -88,6 +90,7 @@ struct osd_client_t int write_state = 0; std::vector send_list, next_send_list; std::vector outbox, next_outbox; + std::vector zc_free_list; ~osd_client_t(); }; @@ -176,6 +179,7 @@ protected: int osd_ping_timeout = 0; int log_level = 0; bool use_sync_send_recv = false; + int min_zerocopy_send_size = DEFAULT_MIN_ZEROCOPY_SEND_SIZE; int iothread_count = 0; #ifdef WITH_RDMA @@ -202,8 +206,9 @@ protected: std::vector set_immediate_ops; public: - timerfd_manager_t *tfd; - ring_loop_t *ringloop; + timerfd_manager_t *tfd = NULL; + ring_loop_t *ringloop = NULL; + bool has_sendmsg_zc = false; // osd_num_t is only for logging and asserts osd_num_t osd_num; uint64_t next_subop_id = 1; @@ -262,7 +267,7 @@ protected: void cancel_op(osd_op_t *op); bool try_send(osd_client_t *cl); - void handle_send(int result, osd_client_t *cl); + void handle_send(int result, bool prev, bool more, osd_client_t *cl); bool handle_read(int result, osd_client_t *cl); bool handle_read_buffer(osd_client_t *cl, void *curbuf, int remain); diff --git a/src/client/msgr_send.cpp b/src/client/msgr_send.cpp index b43d243e..cc445a06 100644 --- a/src/client/msgr_send.cpp +++ b/src/client/msgr_send.cpp @@ -203,8 +203,24 @@ bool osd_messenger_t::try_send(osd_client_t *cl) cl->write_msg.msg_iovlen = cl->send_list.size() < IOV_MAX ? cl->send_list.size() : IOV_MAX; cl->refs++; ring_data_t* data = ((ring_data_t*)sqe->user_data); - data->callback = [this, cl](ring_data_t *data) { handle_send(data->res, cl); }; - my_uring_prep_sendmsg(sqe, peer_fd, &cl->write_msg, 0); + data->callback = [this, cl](ring_data_t *data) { handle_send(data->res, data->prev, data->more, cl); }; + bool use_zc = has_sendmsg_zc && min_zerocopy_send_size >= 0; + if (use_zc && min_zerocopy_send_size > 0) + { + size_t avg_size = 0; + for (size_t i = 0; i < cl->write_msg.msg_iovlen; i++) + avg_size += cl->write_msg.msg_iov[i].iov_len; + if (avg_size/cl->write_msg.msg_iovlen < min_zerocopy_send_size) + use_zc = false; + } + if (use_zc) + { + my_uring_prep_sendmsg_zc(sqe, peer_fd, &cl->write_msg, MSG_WAITALL); + } + else + { + my_uring_prep_sendmsg(sqe, peer_fd, &cl->write_msg, MSG_WAITALL); + } if (iothread) { iothread->add_sqe(sqe_local); @@ -220,7 +236,7 @@ bool osd_messenger_t::try_send(osd_client_t *cl) { result = -errno; } - handle_send(result, cl); + handle_send(result, false, false, cl); } return true; } @@ -240,10 +256,16 @@ void osd_messenger_t::send_replies() write_ready_clients.clear(); } -void osd_messenger_t::handle_send(int result, osd_client_t *cl) +void osd_messenger_t::handle_send(int result, bool prev, bool more, osd_client_t *cl) { - cl->write_msg.msg_iovlen = 0; - cl->refs--; + if (!prev) + { + cl->write_msg.msg_iovlen = 0; + } + if (!more) + { + cl->refs--; + } if (cl->peer_state == PEER_STOPPED) { if (cl->refs <= 0) @@ -261,6 +283,16 @@ void osd_messenger_t::handle_send(int result, osd_client_t *cl) } if (result >= 0) { + if (prev) + { + // Second notification - only free a batch of postponed ops + int i = 0; + for (; i < cl->zc_free_list.size() && cl->zc_free_list[i]; i++) + delete cl->zc_free_list[i]; + if (i > 0) + cl->zc_free_list.erase(cl->zc_free_list.begin(), cl->zc_free_list.begin()+i+1); + return; + } int done = 0; while (result > 0 && done < cl->send_list.size()) { @@ -270,7 +302,10 @@ void osd_messenger_t::handle_send(int result, osd_client_t *cl) if (cl->outbox[done].flags & MSGR_SENDP_FREE) { // Reply fully sent - delete cl->outbox[done].op; + if (more) + cl->zc_free_list.push_back(cl->outbox[done].op); + else + delete cl->outbox[done].op; } result -= iov.iov_len; done++; @@ -282,6 +317,11 @@ void osd_messenger_t::handle_send(int result, osd_client_t *cl) break; } } + if (more) + { + assert(done == cl->send_list.size()); + cl->zc_free_list.push_back(NULL); // end marker + } if (done > 0) { cl->send_list.erase(cl->send_list.begin(), cl->send_list.begin()+done); diff --git a/src/util/ringloop.cpp b/src/util/ringloop.cpp index c447b289..6ca044b4 100644 --- a/src/util/ringloop.cpp +++ b/src/util/ringloop.cpp @@ -10,6 +10,10 @@ #include "ringloop.h" +#ifndef IORING_CQE_F_MORE +#define IORING_CQE_F_MORE (1U << 1) +#endif + ring_loop_t::ring_loop_t(int qd, bool multithreaded) { mt = multithreaded; @@ -30,6 +34,16 @@ ring_loop_t::ring_loop_t(int qd, bool multithreaded) free_ring_data[i] = i; } in_loop = false; + auto probe = io_uring_get_probe(); + if (probe) + { + support_zc = io_uring_opcode_supported(probe, IORING_OP_SENDMSG_ZC); +#ifdef IORING_SETUP_R_DISABLED /* liburing 2.0 check */ + io_uring_free_probe(probe); +#else + free(probe); +#endif + } } ring_loop_t::~ring_loop_t() @@ -108,7 +122,17 @@ void ring_loop_t::loop() if (mt) mu.lock(); struct ring_data_t *d = (struct ring_data_t*)cqe->user_data; - if (d->callback) + if (cqe->flags & IORING_CQE_F_MORE) + { + // There will be a second notification + d->res = cqe->res; + d->more = true; + if (d->callback) + d->callback(d); + d->prev = true; + d->more = false; + } + else if (d->callback) { // First free ring_data item, then call the callback // so it has at least 1 free slot for the next event @@ -116,7 +140,10 @@ void ring_loop_t::loop() struct ring_data_t dl; dl.iov = d->iov; dl.res = cqe->res; + dl.more = false; + dl.prev = d->prev; dl.callback.swap(d->callback); + d->prev = d->more = false; free_ring_data[free_ring_data_ptr++] = d - ring_datas; if (mt) mu.unlock(); diff --git a/src/util/ringloop.h b/src/util/ringloop.h index b9fa9100..6d713ed8 100644 --- a/src/util/ringloop.h +++ b/src/util/ringloop.h @@ -18,6 +18,10 @@ #define RINGLOOP_DEFAULT_SIZE 1024 +#ifndef IORING_RECV_MULTISHOT /* liburing-2.3 check */ +#define IORING_OP_SENDMSG_ZC 48 +#endif + static inline void my_uring_prep_rw(int op, struct io_uring_sqe *sqe, int fd, const void *addr, unsigned len, off_t offset) { // Prepare a read/write operation without clearing user_data @@ -62,6 +66,12 @@ static inline void my_uring_prep_sendmsg(struct io_uring_sqe *sqe, int fd, const sqe->msg_flags = flags; } +static inline void my_uring_prep_sendmsg_zc(struct io_uring_sqe *sqe, int fd, const struct msghdr *msg, unsigned flags) +{ + my_uring_prep_rw(IORING_OP_SENDMSG_ZC, sqe, fd, msg, 1, 0); + sqe->msg_flags = flags; +} + static inline void my_uring_prep_poll_add(struct io_uring_sqe *sqe, int fd, short poll_mask) { my_uring_prep_rw(IORING_OP_POLL_ADD, sqe, fd, NULL, 0, 0); @@ -112,6 +122,8 @@ struct ring_data_t { struct iovec iov; // for single-entry read/write operations int res; + bool prev: 1; + bool more: 1; std::function callback; }; @@ -133,6 +145,7 @@ class ring_loop_t bool loop_again; struct io_uring ring; int ring_eventfd = -1; + bool support_zc = false; public: ring_loop_t(int qd, bool multithreaded = false); ~ring_loop_t(); @@ -163,6 +176,10 @@ public: { return loop_again; } + inline bool has_sendmsg_zc() + { + return support_zc; + } void loop(); void wakeup();