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.
This commit is contained in:
Amaury Denoyelle 2024-11-18 11:05:28 +01:00
parent 6dfc8fbf1d
commit 24cea66e07
6 changed files with 81 additions and 4 deletions

View File

@ -17035,25 +17035,37 @@ proto <name>
instance, it is possible to force the http/2 on clear TCP by specifying "proto instance, it is possible to force the http/2 on clear TCP by specifying "proto
h2" on the bind line. h2" on the bind line.
quic-cc-algo { cubic | newreno | nocc }[(<args,...>)] quic-cc-algo { cubic[-pacing] | newreno | nocc }[(<args,...>)]
This is a QUIC specific setting to select the congestion control algorithm This is a QUIC specific setting to select the congestion control algorithm
for any connection attempts to the configured QUIC listeners. They are similar for any connection attempts to the configured QUIC listeners. They are similar
to those used by TCP. to those used by TCP.
Default value: cubic 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 For further customization, a list of parameters can be specified after the
algorithm token. It must be written between parenthesis, separated by a comma 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 operator. Each argument is optional and can be empty if needed. Here is the
mandatory order of each parameters : mandatory order of each parameters :
- maximum window size in bytes. It must be greater than 10k and smaller than - 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. 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: Example:
# newreno congestion control algorithm # newreno congestion control algorithm
quic-cc-algo newreno quic-cc-algo newreno
# cubic congestion control algorithm with one megabytes as window # cubic congestion control algorithm with one megabytes as window
quic-cc-algo cubic(1m) 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 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 set at the maximum size. It is reserved for debugging scenarios to remove any

View File

@ -185,6 +185,7 @@ struct bind_conf {
struct quic_cc_algo *quic_cc_algo; /* QUIC control congestion algorithm */ struct quic_cc_algo *quic_cc_algo; /* QUIC control congestion algorithm */
size_t max_cwnd; /* QUIC maximumu congestion control window size (kB) */ size_t max_cwnd; /* QUIC maximumu congestion control window size (kB) */
enum quic_sock_mode quic_mode; /* QUIC socket allocation strategy */ enum quic_sock_mode quic_mode; /* QUIC socket allocation strategy */
int quic_pacing_burst; /* QUIC allowed pacing burst size */
#endif #endif
struct proxy *frontend; /* the frontend all these listeners belong to, or NULL */ 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) */ const struct mux_proto_list *mux_proto; /* the mux to use for all incoming connections (specified by the "proto" keyword) */

View File

@ -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. * 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, 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; 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->prep_in_flight = 0;
path->in_flight = 0; path->in_flight = 0;
path->ifae_pkts = 0; path->ifae_pkts = 0;
path->pacing_burst = burst;
quic_cc_init(&path->cc, algo, qc); quic_cc_init(&path->cc, algo, qc);
} }

View File

@ -13,7 +13,7 @@
#include <haproxy/global.h> #include <haproxy/global.h>
#include <haproxy/listener.h> #include <haproxy/listener.h>
#include <haproxy/proxy.h> #include <haproxy/proxy.h>
#include <haproxy/quic_cc-t.h> #include <haproxy/quic_cc.h>
#include <haproxy/quic_rules.h> #include <haproxy/quic_rules.h>
#include <haproxy/tools.h> #include <haproxy/tools.h>
@ -69,11 +69,38 @@ static unsigned long parse_window_size(const char *kw, char *value,
return 0; return 0;
} }
/* Parse <value> 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 */ /* parse "quic-cc-algo" bind keyword */
static int bind_parse_quic_cc_algo(char **args, int cur_arg, struct proxy *px, static int bind_parse_quic_cc_algo(char **args, int cur_arg, struct proxy *px,
struct bind_conf *conf, char **err) struct bind_conf *conf, char **err)
{ {
struct quic_cc_algo *cc_algo = NULL; struct quic_cc_algo *cc_algo = NULL;
const char *str_pacing = "-pacing";
const char *algo = NULL; const char *algo = NULL;
char *arg; 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; algo = QUIC_CC_CUBIC_STR;
*cc_algo = quic_cc_algo_cubic; *cc_algo = quic_cc_algo_cubic;
arg += strlen(QUIC_CC_CUBIC_STR); 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) { else if (strncmp(arg, QUIC_CC_NO_CC_STR, strlen(QUIC_CC_NO_CC_STR)) == 0) {
/* nocc */ /* nocc */
@ -141,6 +181,26 @@ static int bind_parse_quic_cc_algo(char **args, int cur_arg, struct proxy *px,
arg = end_opt; 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 != ')') { if (*++arg != ')') {
memprintf(err, "'%s' : too many argument for '%s' algorithm", args[cur_arg], algo); memprintf(err, "'%s' : too many argument for '%s' algorithm", args[cur_arg], algo);
goto fail; goto fail;

View File

@ -2040,6 +2040,7 @@ struct bind_conf *bind_conf_alloc(struct proxy *fe, const char *file,
/* Use connection socket for QUIC by default. */ /* Use connection socket for QUIC by default. */
bind_conf->quic_mode = QUIC_SOCK_MODE_CONN; bind_conf->quic_mode = QUIC_SOCK_MODE_CONN;
bind_conf->max_cwnd = global.tune.quic_frontend_max_window_size; bind_conf->max_cwnd = global.tune.quic_frontend_max_window_size;
bind_conf->quic_pacing_burst = 0;
#endif #endif
LIST_INIT(&bind_conf->listeners); LIST_INIT(&bind_conf->listeners);

View File

@ -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) */ /* Only one path at this time (multipath not supported) */
qc->path = &qc->paths[0]; qc->path = &qc->paths[0];
quic_cc_path_init(qc->path, ipv4, server ? l->bind_conf->max_cwnd : 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->local_addr, local_addr, sizeof(qc->local_addr));
memcpy(&qc->peer_addr, peer_addr, sizeof qc->peer_addr); memcpy(&qc->peer_addr, peer_addr, sizeof qc->peer_addr);