From 24cea66e072a320ca45db3cded7e3ce64facae04 Mon Sep 17 00:00:00 2001 From: Amaury Denoyelle Date: Mon, 18 Nov 2024 11:05:28 +0100 Subject: [PATCH] MEDIUM: quic: define cubic-pacing congestion algorithm Define a new QUIC congestion algorithm token 'cubic-pacing' for quic-cc-algo bind keyword. This is identical to default cubic implementation, except that pacing is used for STREAM frames emission. This algorithm supports an extra argument to specify a burst size. This is stored into a new bind_conf member named quic_pacing_burst which can be reuse to initialize quic path. Pacing support is still considered experimental. As such, 'cubic-pacing' can only be used with expose-experimental-directives set. --- doc/configuration.txt | 14 +++++++- include/haproxy/listener-t.h | 1 + include/haproxy/quic_cc.h | 4 ++- src/cfgparse-quic.c | 62 +++++++++++++++++++++++++++++++++++- src/listener.c | 1 + src/quic_conn.c | 3 +- 6 files changed, 81 insertions(+), 4 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index 8fbd48057..b35a2562b 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -17035,25 +17035,37 @@ proto instance, it is possible to force the http/2 on clear TCP by specifying "proto h2" on the bind line. -quic-cc-algo { cubic | newreno | nocc }[()] +quic-cc-algo { cubic[-pacing] | newreno | nocc }[()] This is a QUIC specific setting to select the congestion control algorithm for any connection attempts to the configured QUIC listeners. They are similar to those used by TCP. Default value: cubic + It is possible to active pacing if the algorithm is compatible. This is done + by using the suffix "-pacing" after the algorithm name. Pacing purpose is to + smooth emission of data without burst to reduce network loss. In some + scenario, it can significantly improve network throughput. However, it can + also increase CPU usage if haproxy is forced to wait too long between each + emission. Pacing support is still experimental, as such it requires + "expose-experimental-directives". + For further customization, a list of parameters can be specified after the algorithm token. It must be written between parenthesis, separated by a comma operator. Each argument is optional and can be empty if needed. Here is the mandatory order of each parameters : - maximum window size in bytes. It must be greater than 10k and smaller than 4g. By default "tune.quic.frontend.default-max-window-size" value is used. + - count of datagrams to emit in a burst if pacing is activated. It must be + between the default value of 1 and 1024. Example: # newreno congestion control algorithm quic-cc-algo newreno # cubic congestion control algorithm with one megabytes as window quic-cc-algo cubic(1m) + # cubic with pacing on top of it, with burst limited to 12 datagrams + quic-cc-algo cubic-pacing(,12) A special value "nocc" may be used to force a fixed congestion window always set at the maximum size. It is reserved for debugging scenarios to remove any diff --git a/include/haproxy/listener-t.h b/include/haproxy/listener-t.h index becc83b01..ceb19b382 100644 --- a/include/haproxy/listener-t.h +++ b/include/haproxy/listener-t.h @@ -185,6 +185,7 @@ struct bind_conf { struct quic_cc_algo *quic_cc_algo; /* QUIC control congestion algorithm */ size_t max_cwnd; /* QUIC maximumu congestion control window size (kB) */ enum quic_sock_mode quic_mode; /* QUIC socket allocation strategy */ + int quic_pacing_burst; /* QUIC allowed pacing burst size */ #endif struct proxy *frontend; /* the frontend all these listeners belong to, or NULL */ const struct mux_proto_list *mux_proto; /* the mux to use for all incoming connections (specified by the "proto" keyword) */ diff --git a/include/haproxy/quic_cc.h b/include/haproxy/quic_cc.h index 471bf57da..8003878fd 100644 --- a/include/haproxy/quic_cc.h +++ b/include/haproxy/quic_cc.h @@ -82,7 +82,8 @@ static inline void *quic_cc_priv(const struct quic_cc *cc) * which is true for an IPv4 path, if not false for an IPv6 path. */ static inline void quic_cc_path_init(struct quic_cc_path *path, int ipv4, unsigned long max_cwnd, - struct quic_cc_algo *algo, struct quic_conn *qc) + struct quic_cc_algo *algo, int burst, + struct quic_conn *qc) { unsigned int max_dgram_sz; @@ -96,6 +97,7 @@ static inline void quic_cc_path_init(struct quic_cc_path *path, int ipv4, unsign path->prep_in_flight = 0; path->in_flight = 0; path->ifae_pkts = 0; + path->pacing_burst = burst; quic_cc_init(&path->cc, algo, qc); } diff --git a/src/cfgparse-quic.c b/src/cfgparse-quic.c index c7ff2b7ca..0b1b4596d 100644 --- a/src/cfgparse-quic.c +++ b/src/cfgparse-quic.c @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include @@ -69,11 +69,38 @@ static unsigned long parse_window_size(const char *kw, char *value, return 0; } +/* Parse as a number of datagrams allowed for burst. + * + * Returns the parsed value or 0 on error. + */ +static uint parse_burst(const char *kw, char *value, char **end_opt, char **err) +{ + uint burst; + + errno = 0; + burst = strtoul(value, end_opt, 0); + if (*end_opt == value || errno != 0) { + memprintf(err, "'%s' : could not parse burst value", kw); + goto fail; + } + + if (!burst || burst > 1024) { + memprintf(err, "'%s' : pacing burst value must be between 1 and 1024", kw); + goto fail; + } + + return burst; + + fail: + return 0; +} + /* parse "quic-cc-algo" bind keyword */ static int bind_parse_quic_cc_algo(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err) { struct quic_cc_algo *cc_algo = NULL; + const char *str_pacing = "-pacing"; const char *algo = NULL; char *arg; @@ -100,6 +127,19 @@ static int bind_parse_quic_cc_algo(char **args, int cur_arg, struct proxy *px, algo = QUIC_CC_CUBIC_STR; *cc_algo = quic_cc_algo_cubic; arg += strlen(QUIC_CC_CUBIC_STR); + + if (strncmp(arg, str_pacing, strlen(str_pacing)) == 0) { + if (!experimental_directives_allowed) { + memprintf(err, "'%s' : support for pacing is experimental, must be allowed via a global " + "'expose-experimental-directives'\n", args[cur_arg]); + goto fail; + } + + cc_algo->pacing_rate = quic_cc_default_pacing_rate; + cc_algo->pacing_burst = quic_cc_default_pacing_burst; + conf->quic_pacing_burst = 1; + arg += strlen(str_pacing); + } } else if (strncmp(arg, QUIC_CC_NO_CC_STR, strlen(QUIC_CC_NO_CC_STR)) == 0) { /* nocc */ @@ -141,6 +181,26 @@ static int bind_parse_quic_cc_algo(char **args, int cur_arg, struct proxy *px, arg = end_opt; } + if (*++arg == ')') + goto out; + + if (*arg != ',') { + uint burst = parse_burst(args[cur_arg], arg, &end_opt, err); + if (!burst) + goto fail; + + conf->quic_pacing_burst = burst; + + if (*end_opt == ')') { + goto out; + } + else if (*end_opt != ',') { + memprintf(err, "'%s' : cannot parse burst argument for '%s' algorithm", args[cur_arg], algo); + goto fail; + } + arg = end_opt; + } + if (*++arg != ')') { memprintf(err, "'%s' : too many argument for '%s' algorithm", args[cur_arg], algo); goto fail; diff --git a/src/listener.c b/src/listener.c index ec105c310..7518f34ad 100644 --- a/src/listener.c +++ b/src/listener.c @@ -2040,6 +2040,7 @@ struct bind_conf *bind_conf_alloc(struct proxy *fe, const char *file, /* Use connection socket for QUIC by default. */ bind_conf->quic_mode = QUIC_SOCK_MODE_CONN; bind_conf->max_cwnd = global.tune.quic_frontend_max_window_size; + bind_conf->quic_pacing_burst = 0; #endif LIST_INIT(&bind_conf->listeners); diff --git a/src/quic_conn.c b/src/quic_conn.c index dd2df5307..c5c2a74ff 100644 --- a/src/quic_conn.c +++ b/src/quic_conn.c @@ -1224,7 +1224,8 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4, /* Only one path at this time (multipath not supported) */ qc->path = &qc->paths[0]; quic_cc_path_init(qc->path, ipv4, server ? l->bind_conf->max_cwnd : 0, - cc_algo ? cc_algo : default_quic_cc_algo, qc); + cc_algo ? cc_algo : default_quic_cc_algo, + l->bind_conf->quic_pacing_burst, qc); memcpy(&qc->local_addr, local_addr, sizeof(qc->local_addr)); memcpy(&qc->peer_addr, peer_addr, sizeof qc->peer_addr);