experimental patch to make use of IO_URING to batch load certificates;
this drastically reduces the number of syscall and might benefit to
setup with large number of certificates.
it uses liburing in order to batch operation as follow:
for each certificate directory, we apply the same operations on each
file:
 - statx
 - openat
 - read
 - close
the results are stored in an ebtree. Then when we need to load them
with the SSL lib, instead of providing a path, we provide a buffer to
be consumed. The tree is freed after each directory.

for now it requires a quite large limit of file descriptors, as all
operations types are done one after another; so the limit of fd should
be set higher than the number of certificates you have to load. This
part is probably going to evolve very soon as IO_URING plans are to be
able to chain operations with a given pre-defined file descriptor.

on a setup with 25k certificates I was able to measure a minimum of 20%
gain on the init time when the filesystem cache is empty. My testing is
as follow; for each directory:
- put a timer before `ssl_sock_load_cert_list_file`, including
  `ssl_load_certiodir` in the case of io_uring case.
- measure the time at the end of those operations, just after
  `ssl_free_certiodir` in the case of io_uring.

some results with my old ssd laptop, the average certificate init time
in ms, using 24686 certs:
▒▒▒▒▒▒▒▒▒▒▒▒▒▒           8554    io_uring_empty_cache
▒▒▒▒▒▒▒▒▒▒▒▒▒            8100    io_uring_full_cache
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒      11395    syscall_empty_cache
▒▒▒▒▒▒▒▒▒▒▒▒▒            8087    syscall_full_cache

The benefits are of course less obvious with following reloads on a
machine where you only have haproxy process managing its own memory, but
it can be interesting on a busier setup where the filesystem cache is
often invalidated. However a very quick overview of reload time on our
production seems to show that more than 50% of the time, the files are
not present in cache anymore while reloading (I hope I'm not too far
from the reality in my analysis); so I would expect this patch could
participate to flatten our reload time; I however did not tested it in a
production environment.

It remains linux-specific though; the operations used require a minimum
of kernel >= v5.6 even though some operations were supported in v5.4, I
did not change the code to adapt the behavior. All my tests were
performed on >= v5.7rc4 as the time of writing.

it introduces three build parameters:
- USE_IO_URING
- IO_URING_INC
- IO_URING_LIB

Signed-off-by: William Dauchy <w.dau...@criteo.com>
---
 Makefile                 |  10 +-
 doc/io_uring.txt         |  18 +++
 include/proto/ssl_load.h |  23 ++++
 include/types/ssl_load.h |  38 ++++++
 include/types/ssl_sock.h |   7 +
 src/ssl_load.c           | 274 +++++++++++++++++++++++++++++++++++++++
 src/ssl_sock.c           |  32 ++++-
 7 files changed, 395 insertions(+), 7 deletions(-)
 create mode 100644 doc/io_uring.txt
 create mode 100644 include/proto/ssl_load.h
 create mode 100644 include/types/ssl_load.h
 create mode 100644 src/ssl_load.c

diff --git a/Makefile b/Makefile
index 1e4213989..afba381c0 100644
--- a/Makefile
+++ b/Makefile
@@ -52,6 +52,7 @@
 #   USE_SYSTEMD          : enable sd_notify() support.
 #   USE_OBSOLETE_LINKER  : use when the linker fails to emit 
__start_init/__stop_init
 #   USE_THREAD_DUMP      : use the more advanced thread state dump system. 
Automatic.
+#   USE_IO_URING         : use IO_URING advanced async features
 #
 # Options can be forced by specifying "USE_xxx=1" or can be disabled by using
 # "USE_xxx=" (empty string). The list of enabled and disabled options for a
@@ -293,7 +294,8 @@ use_opts = USE_EPOLL USE_KQUEUE USE_NETFILTER               
                  \
            USE_GETADDRINFO USE_OPENSSL USE_LUA USE_FUTEX USE_ACCEPT4          \
            USE_ZLIB USE_SLZ USE_CPU_AFFINITY USE_TFO USE_NS                   \
            USE_DL USE_RT USE_DEVICEATLAS USE_51DEGREES USE_WURFL USE_SYSTEMD  \
-           USE_OBSOLETE_LINKER USE_PRCTL USE_THREAD_DUMP USE_EVPORTS
+           USE_OBSOLETE_LINKER USE_PRCTL USE_THREAD_DUMP USE_EVPORTS          \
+           USE_IO_URING
 
 #### Target system options
 # Depending on the target platform, some options are set, as well as some
@@ -531,6 +533,12 @@ ifneq ($(USE_BACKTRACE),)
 OPTIONS_LDFLAGS += -Wl,$(if $(EXPORT_SYMBOL),$(EXPORT_SYMBOL),--export-dynamic)
 endif
 
+ifneq ($(USE_IO_URING),)
+OPTIONS_OBJS  += src/ssl_load.o
+OPTIONS_CFLAGS  += $(if $(IO_URING_INC),-I$(IO_URING_INC))
+OPTIONS_LDFLAGS += $(if $(IO_URING_LIB),-L$(IO_URING_LIB)) -luring
+endif
+
 ifneq ($(USE_OPENSSL),)
 # OpenSSL is packaged in various forms and with various dependencies.
 # In general -lssl is enough, but on some platforms, -lcrypto may be needed,
diff --git a/doc/io_uring.txt b/doc/io_uring.txt
new file mode 100644
index 000000000..aa290dd0d
--- /dev/null
+++ b/doc/io_uring.txt
@@ -0,0 +1,18 @@
+Linux IO_URING support for HAProxy
+==================================
+
+0. Build
+--------
+
+Add the following parameters to your make options:
+
+- USE_IO_URING=1
+- IO_URING_INC='/path/to/liburing/src/include/'
+- IO_URING_LIB='/path/to/liburing/src/'
+
+1. Certificates loading
+-----------------------
+
+HAProxy can make use of IO_URING in order to reduce the number of syscall used
+to load the certificates. It requires Linux kernel >= v5.6 according to the
+operations used (statx, open, read, close).
diff --git a/include/proto/ssl_load.h b/include/proto/ssl_load.h
new file mode 100644
index 000000000..b1b5ba07d
--- /dev/null
+++ b/include/proto/ssl_load.h
@@ -0,0 +1,23 @@
+/*
+ * load certificates in batch with liburing
+ *
+ * Copyright 2020 William Dauchy <wdau...@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version
+ * 2 of the License, or (at your option) any later version.
+ */
+
+#ifndef _PROTO_SSL_LOAD_H
+#define _PROTO_SSL_LOAD_H
+
+#ifdef USE_IO_URING
+
+#include <ebpttree.h>
+
+int ssl_load_certiodir(const char *filedir, struct eb_root *file_tree);
+void ssl_free_certiodir(const char *filedir, struct eb_root *file_tree);
+
+#endif /* USE_IO_URING */
+#endif /* _PROTO_SSL_LOAD_H */
diff --git a/include/types/ssl_load.h b/include/types/ssl_load.h
new file mode 100644
index 000000000..67384ee65
--- /dev/null
+++ b/include/types/ssl_load.h
@@ -0,0 +1,38 @@
+/*
+ * load certificates in batch with liburing
+ *
+ * Copyright 2020 William Dauchy <wdau...@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version
+ * 2 of the License, or (at your option) any later version.
+ */
+
+#ifndef _TYPES_SSL_LOAD_H
+#define _TYPES_SSL_LOAD_H
+
+#ifdef USE_IO_URING
+
+#include <liburing.h>
+#include <ebpttree.h>
+
+#define QD     4096
+
+struct cert_iobuf {
+       int fd;
+       char *buf;
+       off_t filesize;
+       struct statx stx;
+       struct ebmb_node node;
+       char filepath[0];
+};
+
+struct io_op {
+       char name[6];                                               /* 
operation name for logging */
+       int (*queue)(struct io_uring *, struct cert_iobuf *, int);
+       void (*handle)(struct cert_iobuf *, struct io_uring_cqe *);
+};
+
+#endif /* USE_IO_URING */
+#endif /* _TYPES_SSL_LOAD_H */
diff --git a/include/types/ssl_sock.h b/include/types/ssl_sock.h
index dbfa3d726..1f38c470e 100644
--- a/include/types/ssl_sock.h
+++ b/include/types/ssl_sock.h
@@ -31,6 +31,13 @@
 #include <common/mini-clist.h>
 #include <common/openssl-compat.h>
 
+#if HA_OPENSSL_VERSION_NUMBER >= 0x1000200fL
+extern const char *SSL_SOCK_KEYTYPE_NAMES[];
+#define SSL_SOCK_NUM_KEYTYPES 3
+#else
+#define SSL_SOCK_NUM_KEYTYPES 1
+#endif
+
 struct pkey_info {
        uint8_t sig;          /* TLSEXT_signature_[rsa,ecdsa,...] */
        uint16_t bits;        /* key size in bits */
diff --git a/src/ssl_load.c b/src/ssl_load.c
new file mode 100644
index 000000000..f3b4b9c19
--- /dev/null
+++ b/src/ssl_load.c
@@ -0,0 +1,274 @@
+/*
+ * load certificates in batch with liburing
+ *
+ * Copyright 2020 William Dauchy <wdau...@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version
+ * 2 of the License, or (at your option) any later version.
+ */
+
+#define _GNU_SOURCE
+
+#include <fcntl.h>
+#include <dirent.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <string.h>
+
+#include <liburing.h>
+
+#include <ebpttree.h>
+#include <ebsttree.h>
+
+#include <types/ssl_sock.h>
+#include <types/ssl_load.h>
+
+#include <proto/log.h>
+
+static int setup_context(unsigned entries, struct io_uring *ring)
+{
+       int ret;
+
+       ret = io_uring_queue_init(entries, ring, 0);
+       if (ret < 0) {
+               ha_alert("queue_init: %s\n", strerror(-ret));
+               return ERR_ALERT;
+       }
+       return 0;
+}
+
+static int do_io_op(struct io_uring *ring, struct eb_root *cert_iobuf_tree,
+                   struct io_op *op, int dirfd)
+{
+       struct cert_iobuf *cert_io;
+       struct eb_node *node, *next;
+       struct io_uring_cqe *cqe;
+       unsigned int i;
+       int inqueue;
+       int pending;
+       int ret;
+
+       inqueue = 0;
+       node = eb_first(cert_iobuf_tree);
+       while (node) {
+               for (; node && inqueue < QD; inqueue++) {
+                       next = eb_next(node);
+                       cert_io = ebmb_entry(node, struct cert_iobuf, node);
+                       ret = op->queue(ring, cert_io, dirfd);
+                       if (ret)
+                               ha_warning("ssl_load: queue error\n");
+                       node = next;
+               }
+               ret = io_uring_submit(ring);
+               if (ret < 0) {
+                       ha_warning("ssl_load, %s: io_uring_submit: %s\n",
+                                  op->name, strerror(-ret));
+                       return ERR_ALERT;
+               }
+               pending = ret;
+               for (i = 0; i < pending; i++) {
+                       ret = io_uring_wait_cqe(ring, &cqe);
+                       if (ret < 0) {
+                               ha_warning("ssl_load, %s: io_uring_wait_cqe: 
%s\n",
+                                          op->name, strerror(-ret));
+                               continue;
+                       }
+                       if (!cqe)
+                               continue;
+                       cert_io = io_uring_cqe_get_data(cqe);
+                       if (cqe->res < 0)
+                               ha_warning("ssl_load, %s: cqe failed: %s 
(%s)\n",
+                                          op->name, strerror(-cqe->res), 
cert_io->filepath);
+                       else
+                               op->handle(cert_io, cqe);
+                       io_uring_cqe_seen(ring, cqe);
+                       inqueue--;
+               }
+       }
+       if (inqueue > 0) {
+               ha_warning("ssl_load, %s: inqueue error\n", op->name);
+               return ERR_ALERT;
+       }
+       return 0;
+}
+
+static int queue_statx(struct io_uring *ring, struct cert_iobuf *cert_io, int 
dirfd)
+{
+       struct io_uring_sqe *sqe;
+
+       sqe = io_uring_get_sqe(ring);
+       if (!sqe)
+               return ERR_ALERT;
+
+       io_uring_prep_statx(sqe, dirfd, cert_io->filepath, 0, STATX_SIZE, 
&(cert_io->stx));
+       io_uring_sqe_set_data(sqe, cert_io);
+       return 0;
+}
+
+static void handle_statx(struct cert_iobuf *cert_io, struct io_uring_cqe *cqe)
+{
+       cert_io->filesize = cert_io->stx.stx_size;
+}
+
+static int queue_open(struct io_uring *ring, struct cert_iobuf *cert_io, int 
dirfd)
+{
+       struct io_uring_sqe *sqe;
+
+       sqe = io_uring_get_sqe(ring);
+       if (!sqe)
+               return ERR_ALERT;
+
+       io_uring_prep_openat(sqe, dirfd, cert_io->filepath, O_RDONLY, 0);
+       io_uring_sqe_set_data(sqe, cert_io);
+       return 0;
+}
+
+static void handle_open(struct cert_iobuf *cert_io, struct io_uring_cqe *cqe)
+{
+       cert_io->fd = cqe->res;
+}
+
+static int queue_read(struct io_uring *ring, struct cert_iobuf *cert_io, int 
dirfd)
+{
+       struct io_uring_sqe *sqe;
+
+       sqe = io_uring_get_sqe(ring);
+       if (!sqe)
+               return ERR_ALERT;
+
+       cert_io->buf = malloc(sizeof(*(cert_io->buf)) * cert_io->filesize + 1);
+       if (!cert_io->buf) {
+               ha_alert("ssl_load: out of memory in buffer allocation\n");
+               return ERR_ALERT;
+       }
+
+       io_uring_prep_read(sqe, cert_io->fd, cert_io->buf, cert_io->filesize, 
0);
+       io_uring_sqe_set_data(sqe, cert_io);
+       return 0;
+}
+
+void handle_read(struct cert_iobuf *cert_io, struct io_uring_cqe *cqe)
+{
+       cert_io->buf[cert_io->filesize] = 0;
+}
+
+static int queue_close(struct io_uring *ring, struct cert_iobuf *cert_io, int 
dirfd)
+{
+       struct io_uring_sqe *sqe;
+
+       sqe = io_uring_get_sqe(ring);
+       if (!sqe)
+               return ERR_ALERT;
+
+       io_uring_prep_close(sqe, cert_io->fd);
+       return 0;
+}
+
+static void handle_close(struct cert_iobuf *cert_io, struct io_uring_cqe *cqe)
+{
+}
+
+static int ssl_load_preparetree(const char *filedir, struct eb_root 
*cert_iobuf_tree)
+{
+       struct cert_iobuf *cert_io;
+       struct dirent **de_list;
+       struct dirent *de;
+       int path_len;
+       int nb_file;
+       int i, j;
+       char *end;
+
+       nb_file = scandir(filedir, &de_list, 0, alphasort);
+       if (nb_file < 0) {
+               ha_warning("ssl_load: unable to scan directory\n");
+               return ERR_ALERT;
+       }
+       for (i = 0; i < nb_file; i++) {
+               de = de_list[i];
+               end = strrchr(de->d_name, '.');
+               for (j = 0; j < SSL_SOCK_NUM_KEYTYPES; j++)
+                       if (!strcmp(end + 1, SSL_SOCK_KEYTYPE_NAMES[j]))
+                               goto load_entry;
+               goto skip_entry;
+load_entry:
+               /* filedir + slash + name + \0 */
+               path_len = strlen(filedir) + strlen(de->d_name) + 2;
+               cert_io = malloc(sizeof(*cert_io) + path_len);
+               if (cert_io == NULL) {
+                       ha_alert("ssl_load: out of memory in tree 
allocation\n");
+                       return ERR_ALERT;
+               }
+               snprintf((char *) cert_io->node.key, path_len, "%s/%s",
+                        filedir, de->d_name);
+               cert_io->filesize = 0;
+               cert_io->buf = NULL;
+               ebst_insert(cert_iobuf_tree, &cert_io->node);
+skip_entry:
+               free(de);
+       }
+       free(de_list);
+       return 0;
+}
+
+int ssl_load_certiodir(const char *filedir, struct eb_root *cert_iobuf_tree)
+{
+       struct io_uring ring;
+       struct io_op op;
+       int dirfd;
+       int ret = 0;
+
+       ret = ssl_load_preparetree(filedir, cert_iobuf_tree);
+       if (ret)
+               return ret;
+
+       dirfd = open(filedir, 0);
+       if (dirfd < 0)
+               return ERR_ALERT;
+       if (setup_context(QD, &ring))
+               return ERR_ALERT;
+       memcpy(op.name, "statx", strlen("statx") + 1);
+       op.queue = queue_statx;
+       op.handle = handle_statx;
+       ret = do_io_op(&ring, cert_iobuf_tree, &op, dirfd);
+       if (ret)
+               goto exit;
+       memcpy(op.name, "open", strlen("open") + 1);
+       op.queue = queue_open;
+       op.handle = handle_open;
+       ret = do_io_op(&ring, cert_iobuf_tree, &op, dirfd);
+       if (ret)
+               goto exit;
+       memcpy(op.name, "read", strlen("open") + 1);
+       op.queue = queue_read;
+       op.handle = handle_read;
+       ret = do_io_op(&ring, cert_iobuf_tree, &op, dirfd);
+       if (ret)
+               goto exit;
+       memcpy(op.name, "close", strlen("open") + 1);
+       op.queue = queue_close;
+       op.handle = handle_close;
+       ret = do_io_op(&ring, cert_iobuf_tree, &op, dirfd);
+       if (ret)
+               goto exit;
+exit:
+       io_uring_queue_exit(&ring);
+       return ret;
+}
+
+void ssl_free_certiodir(const char *filedir, struct eb_root *cert_iobuf_tree)
+{
+       struct eb_node *node, *next;
+       struct cert_iobuf *cert_io;
+
+       node = eb_first(cert_iobuf_tree);
+       while (node) {
+               next = eb_next(node);
+               eb_delete(node);
+               cert_io = ebmb_entry(node, struct cert_iobuf, node);
+               free(cert_io->buf);
+               free(cert_io);
+               node = next;
+       }
+}
diff --git a/src/ssl_sock.c b/src/ssl_sock.c
index 78d6da303..1f4fa1599 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -64,6 +64,7 @@
 #include <types/cli.h>
 #include <types/global.h>
 #include <types/ssl_sock.h>
+#include <types/ssl_load.h>
 #include <types/stats.h>
 
 #include <proto/acl.h>
@@ -85,6 +86,7 @@
 #include <proto/proxy.h>
 #include <proto/shctx.h>
 #include <proto/ssl_sock.h>
+#include <proto/ssl_load.h>
 #include <proto/stream.h>
 #include <proto/task.h>
 #include <proto/vars.h>
@@ -163,6 +165,10 @@ int nb_engines = 0;
 static struct eb_root cert_issuer_tree = EB_ROOT; /* issuers tree from 
"issuers-chain-path" */
 static struct issuer_chain* ssl_get0_issuer_chain(X509 *cert);
 
+#ifdef USE_IO_URING
+static struct eb_root cert_iobuf_tree = EB_ROOT_UNIQUE; /* IO_URING 
certificates buffer */
+#endif
+
 static struct {
        char *crt_base;             /* base directory path for certificates */
        char *ca_base;              /* base directory path for CAs and CRLs */
@@ -613,9 +619,6 @@ const char *SSL_SOCK_KEYTYPE_NAMES[] = {
        "ecdsa",
        "rsa"
 };
-#define SSL_SOCK_NUM_KEYTYPES 3
-#else
-#define SSL_SOCK_NUM_KEYTYPES 1
 #endif
 
 static struct shared_context *ssl_shctx = NULL; /* ssl shared session cache */
@@ -3680,12 +3683,22 @@ end:
  */
 static int ssl_sock_load_files_into_ckch(const char *path, struct 
cert_key_and_chain *ckch, char **err)
 {
+       char *buf = NULL;
        int ret = 1;
+#ifdef USE_IO_URING
+       struct ebmb_node *eb = NULL;
+       struct cert_iobuf *cert_io;
+
+       eb = ebst_lookup(&cert_iobuf_tree, path);
+       if (eb) {
+               cert_io = ebmb_entry(eb, struct cert_iobuf, node);
+               buf = cert_io->buf;
+       }
+#endif
 
        /* try to load the PEM */
-       if (ssl_sock_load_pem_into_ckch(path, NULL, ckch , err) != 0) {
+       if (ssl_sock_load_pem_into_ckch(path, buf, ckch , err) != 0)
                goto end;
-       }
 
        /* try to load an external private key if it wasn't in the PEM */
        if ((ckch->key == NULL) && (global_ssl.extra_files & SSL_GF_KEY)) {
@@ -5072,7 +5085,14 @@ int ssl_sock_load_cert(char *path, struct bind_conf 
*bind_conf, char **err)
 
                        return ssl_sock_load_ckchs(path, ckchs, bind_conf, 
NULL, NULL, 0, &ckch_inst, err);
                } else {
-                       return ssl_sock_load_cert_list_file(path, 1, bind_conf, 
bind_conf->frontend, err);
+#ifdef USE_IO_URING
+                       cfgerr = ssl_load_certiodir(path, &cert_iobuf_tree);
+#endif
+                       cfgerr |= ssl_sock_load_cert_list_file(path, 1, 
bind_conf, bind_conf->frontend, err);
+#ifdef USE_IO_URING
+                       ssl_free_certiodir(path, &cert_iobuf_tree);
+#endif
+                       return cfgerr;
                }
        } else {
                /* stat failed, could be a bundle */
-- 
2.26.2


Reply via email to