]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: connection: add a function to calculate elastic streams limit
authorWilly Tarreau <w@1wt.eu>
Sat, 9 May 2026 14:37:25 +0000 (16:37 +0200)
committerWilly Tarreau <w@1wt.eu>
Sun, 10 May 2026 12:36:08 +0000 (14:36 +0200)
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.

doc/configuration.txt
include/haproxy/connection.h
include/haproxy/global-t.h
src/cfgparse-global.c

index 2c00c8680a541ea14889ed8f2619b50854d188ad..828e2cdad206eb9615d3cdc27f2fce2b4064919d 100644 (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
index 3fb5933c456b4feb64093b7f9a26823c3476165f..b4209caf60a1a96728dcb7895c7a043fb1b54f6e 100644 (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
index 2ffd8305f70385cbb0131ada765b5d33de3659d6..62144136c4814beda64536e382d9452427d6d289 100644 (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 {
index d16f9747e242ea8132baa3898e45fa7bf7affa92..882387c2cad7be37fbcf338a5c91c558e8bd36d1 100644 (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 },