From: Willy Tarreau Date: Sat, 9 May 2026 14:37:25 +0000 (+0200) Subject: MINOR: connection: add a function to calculate elastic streams limit X-Git-Url: http://www.kaiwu.me/postgresql/commit/?a=commitdiff_plain;h=dd36c84a7b21fa86f8db57e3f832e3122a52aeef;p=haproxy.git MINOR: connection: add a function to calculate elastic streams limit This adds a new tune.streams-elasticity parameter. This parameter indicates, as a percentage, the average number of streams per connection at full load. It is used to calculate limits of the number of streams to advertise on new connections. 0 means that no such limit is set. When a limit is set, the new function conn_calc_max_streams() determines the optimal number of streams to allow on a connection. It will assign at least the ratio of streams left to connections left, and at least a fair share of what's left times the number of desired streams. It will always ensure that each connection gets at least 1 stream, and everything beyond this will be evenly distributed. For now the function is not used. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 2c00c8680..828e2cdad 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -2001,6 +2001,7 @@ The following keywords are supported in the "global" section : - tune.sndbuf.client - tune.sndbuf.frontend - tune.sndbuf.server + - tune.streams-elasticity - tune.stick-counters - tune.ssl.cachesize - tune.ssl.capture-buffer-size @@ -5689,6 +5690,39 @@ tune.ssl.ssl-ctx-cache-size dynamically is expensive, they are cached. The default cache size is set to 1000 entries. +tune.streams-elasticity + Defines a target percentage of streams per frontend connection relative to + the maximum number of concurrent connections (maxconn) when all connections + are established. This metric applies to multiplexed protocols like HTTP/2 or + QUIC, where each connection may receive multiple streams. At least one is + always guaranteed, so the percentage must be at least 100%. During connection + setup, HAProxy dynamically advertises additional streams up to the configured + limit, maintaining the target ratio. At connection establishment, every + frontend connection receives at least one stream; extra streams are assigned + based on the target percentage and configured stream limits. This ensures + efficient stream allocation under varying load conditions (more streams at + low loads, fewer at high loads). + + Highly dynamic sites with many objects per page benefit from high ratios, + enabling many streams per connection. Sites using fewer streams on average + (WebSocket, application code) may prefer small ratios closer to 120 or 150 + (20 to 50% more streams than connections) preventing excessive stream counts + under sustained loads. + + The default value is 0, meaning no enforcement at this level, so only H2 and + QUIC configurations apply (with the default setting of 100 streams per + connection, this corresponds to 10000%). This remains the recommended setting + for small deployments (maxconn around a thousand). Moderately sized setups + (few thousands to tens of thousands connections) typically set the ratio + between 1000 and 5000, allowing 10 to 50 streams per connection at full load. + Large-scale deployments (hundreds of thousands to millions connections) might + use lower values (120 to 200) to support 1.2 to 2 streams per connection on + average at full load. + + Monitoring the total number of active streams on backends, including queues, + provides a practical indicator of a sustainable target load and helps avoid + over-provisioning. + tune.stick-counters Sets the number of stick-counters that may be tracked at the same time by a connection or a request via "track-sc*" actions in "tcp-request" or diff --git a/include/haproxy/connection.h b/include/haproxy/connection.h index 3fb5933c4..b4209caf6 100644 --- a/include/haproxy/connection.h +++ b/include/haproxy/connection.h @@ -496,6 +496,64 @@ static inline int conn_install_mux(struct connection *conn, const struct mux_ops return ret; } +/* Calculates the approximate number of streams permitted for an already + * established frontend connection based on the number of active connections + * (including this one), the number of already committed streams in the current + * thread group, the limit, and the desired limit (a ratio of which will be + * applied as the budget permits). May return 0 for no limit. The minimum value + * when a limit is set will be 1 as a minimum. + */ +static inline uint conn_calc_max_streams(uint desired) +{ + uint per_conn_left; + uint avg_per_conn; + uint conn_curr; + int conn_left; + uint extra; + uint curr; + + /* check for infinite */ + if (!global.tune.streams_elasticity) + return 0; + + /* check for none (0% overcommit) */ + if (global.tune.streams_elasticity == 100) + return 1; + + if (desired <= 1) + return 1; + + conn_curr = _HA_ATOMIC_LOAD(&actconn) - 1; + conn_left = global.hardmaxconn - conn_curr; + if (conn_left <= 0) + return 1; + + /* the limit is per process, we're working per group. Since we're + * counting extra streams max, we subtract 100% from elasticity. + */ + extra = (((ullong)global.hardmaxconn * (global.tune.streams_elasticity - 100) / 100)); + curr = _HA_ATOMIC_LOAD(&tg_ctx->committed_extra_streams) * global.nbtgroups; + if (curr >= extra) + return 1; + + /* this is the average per conn left that we can allocate */ + per_conn_left = ((extra - curr) + conn_left - 1) / conn_left; + + /* OK so we know we can still allocate (extra - curr) streams per + * tgroup, that will be shared across conn_left connections, but ought + * to be fairly shared between all conn_curr ones. This allows to + * provide at least up to as long as we leave enough for all + * remaining connections left. + */ + avg_per_conn = ((ullong)(extra - curr) * (desired - 1)) / extra; + + /* both values are permitted since they respect the global limit, + * so let's deliver the best option to better serve first conns + * so that the limit degrades smoothly with the number of conns. + */ + return 1 + MAX(per_conn_left, avg_per_conn); +} + /* Retrieves any valid stream connector from this connection, preferably the first * valid one. The purpose is to be able to figure one other end of a private * connection for purposes like source binding or proxy protocol header diff --git a/include/haproxy/global-t.h b/include/haproxy/global-t.h index 2ffd8305f..62144136c 100644 --- a/include/haproxy/global-t.h +++ b/include/haproxy/global-t.h @@ -216,6 +216,7 @@ struct global { uint max_checks_per_thread; /* if >0, no more than this concurrent checks per thread */ uint ring_queues; /* if >0, #ring queues, otherwise equals #thread groups */ uint cli_max_payload_sz; /* The max payload size for the CLI */ + int streams_elasticity; /* percent of advertised streams to connection; 0=no limit */ enum threadgroup_takeover tg_takeover; /* Policy for threadgroup takeover */ } tune; struct { diff --git a/src/cfgparse-global.c b/src/cfgparse-global.c index d16f9747e..882387c2c 100644 --- a/src/cfgparse-global.c +++ b/src/cfgparse-global.c @@ -1444,6 +1444,16 @@ static int cfg_parse_global_tune_opts(char **args, int section_type, return -1; } } + else if (strcmp(args[0], "tune.streams-elasticity") == 0) { + char *stop; + + global.tune.streams_elasticity = strtol(args[1], &stop, 10); + if (!*args[1] || *stop || + (global.tune.streams_elasticity && global.tune.streams_elasticity < 100)) { + memprintf(err, "'%s' expects 0 or a positive percentage value of 100 or above", args[0]); + return -1; + } + } else if (strcmp(args[0], "tune.takeover-other-tg-connections") == 0) { if (*(args[1]) == 0) { memprintf(err, "'%s' expects 'none', 'restricted', or 'full'", args[0]); @@ -1903,6 +1913,7 @@ static struct cfg_kw_list cfg_kws = {ILH, { { CFG_GLOBAL, "tune.runqueue-depth", cfg_parse_global_tune_opts }, { CFG_GLOBAL, "tune.sndbuf.client", cfg_parse_global_tune_opts }, { CFG_GLOBAL, "tune.sndbuf.server", cfg_parse_global_tune_opts }, + { CFG_GLOBAL, "tune.streams-elasticity", cfg_parse_global_tune_opts }, { CFG_GLOBAL, "tune.takeover-other-tg-connections", cfg_parse_global_tune_opts }, { CFG_GLOBAL, "unsetenv", cfg_parse_global_env_opts, KWF_DISCOVERY }, { CFG_GLOBAL, "zero-warning", cfg_parse_global_mode, KWF_DISCOVERY },