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.
This commit is contained in:
Willy Tarreau 2026-05-09 16:37:25 +02:00
parent 7f17512d18
commit dd36c84a7b
4 changed files with 104 additions and 0 deletions

View File

@ -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 <number>
dynamically is expensive, they are cached. The default cache size is set to
1000 entries.
tune.streams-elasticity <number>
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 <number>
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

View File

@ -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 <desired> 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

View File

@ -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 {

View File

@ -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 },