]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: mux-h2: permit to moderate the advertised streams limit depending on load
authorWilly Tarreau <w@1wt.eu>
Wed, 18 Mar 2026 21:32:05 +0000 (22:32 +0100)
committerWilly Tarreau <w@1wt.eu>
Thu, 19 Mar 2026 15:24:31 +0000 (16:24 +0100)
Global setting tune.h2.fe.max-concurrent-streams now supports an optional
"rq-load" option to pass either a target load, or a keyword among "auto"
and "ignore". These are used to quadratically reduce the advertised streams
limit when the thread's run queue size goes beyong the configured value,
and automatically reduce the load on the process from new connections.
With "auto", instead of taking an explicit value, it uses as a target the
"tune.runqueue-depth" setting (which might be automatic). Tests have shown
that values between 50 and 100 are already very effective at reducing the
loads during attacks from 100000 to around 1500. By default, "ignore"
is in effect, which means that the dynamic tuning is not enabled.

doc/configuration.txt
src/mux_h2.c

index b0ec3fe5f9228ef2dfc1eda2a68e7093dc167076..c774aface0c733b5eb6adf3e7da50a42b153f745 100644 (file)
@@ -4418,7 +4418,7 @@ tune.h2.fe.initial-window-size <number>
 
   See also: tune.h2.initial-window-size.
 
-tune.h2.fe.max-concurrent-streams <number>
+tune.h2.fe.max-concurrent-streams <number> [args...]
   Sets the HTTP/2 maximum number of concurrent streams per incoming connection
   (i.e. the number of outstanding requests on a single connection from a
   client). When not set, the default set by tune.h2.max-concurrent-streams
@@ -4426,7 +4426,26 @@ tune.h2.fe.max-concurrent-streams <number>
   the page load time for complex sites with lots of small objects over high
   latency networks but can also result in using more memory by allowing a
   client to allocate more resources at once. The default value of 100 is
-  generally good and it is recommended not to change this value.
+  generally good and it is recommended not to change this value. A larger
+  concurrency also has an impact on the processing load and latency when
+  dealing with large numbers of connections which are themselves using many
+  streams, and it may lower the barrier to denial of service attacks. The
+  command supports the following optional arguments after the number:
+
+  - rq-load { <number> | auto | ignore }:
+    The optional argument "rq-load" permits to dynamically adjust the
+    advertised concurrency based on the executing thread's run-queue load:
+    as long as the thread's load remains below the indicated threshold, the
+    configured streams limit will be advertised. When the thread's load
+    increases beyond the configured limit, the advertised streams limit will be
+    decreased proportionally to the square of the excess ratio. Target load
+    levels between 50 and 100 generally show very good moderation under heavy
+    loads. Alternately, instead of specifying an explicit number, the keyword
+    accepts "ignore", which is the default and means that the thread's
+    run-queue load will not be considered to moderate the advertised streams
+    limit, and "auto", which sets the limit to the "tune.runqueue-depth"
+    value, which generally provides good results without having to tweak
+    the configuration any further.
 
 tune.h2.fe.max-total-streams <number>
   Sets the HTTP/2 maximum number of total streams processed per incoming
index 00a33d7ae5d94e22821e996d2dbb79ae526cd811..302c4fe9931c548c3dcfa9aaa70ea3d32b7743ae 100644 (file)
@@ -492,6 +492,7 @@ static uint h2_fe_rxbuf                       =     0; /* frontend's default tot
 static unsigned int h2_settings_max_concurrent_streams    = 100; /* default value */
 static unsigned int h2_be_settings_max_concurrent_streams =   0; /* backend value */
 static unsigned int h2_fe_settings_max_concurrent_streams =   0; /* frontend value */
+static unsigned int h2_fe_max_rq_load         = ~0;    /* max rq for FE dynamic MCS sizing. 0=def rq */
 static int h2_settings_max_frame_size         = 0;     /* unset */
 static int h2_settings_log_errors             = H2_ERR_LOG_ERR_STRM;
 
@@ -757,6 +758,21 @@ static inline int h2c_max_concurrent_streams(const struct h2c *h2c)
                h2_fe_settings_max_concurrent_streams;
 
        ret = ret ? ret : h2_settings_max_concurrent_streams;
+
+       /* if h2_fe_max_rq_load is set, adjust the max concurrent streams
+        * according to it and the current load.
+        */
+       if (!(h2c->flags & H2_CF_IS_BACK) && h2_fe_max_rq_load != ~0) {
+               uint limit = h2_fe_max_rq_load ? h2_fe_max_rq_load : global.tune.runqueue_depth;
+               uint load = MAX(swrate_avg(th_ctx->rq_tot_peak, RQ_LOAD_SAMPLES), th_ctx->rq_total);
+
+               /* divide limits by the square of the ratio of current load to
+                * the limit so as to react fast.
+                */
+               if (load > limit)
+                       ret = (uint64_t)ret * limit / load * limit / load;
+               ret = ret ? ret : 1;
+       }
        return ret;
 }
 
@@ -8709,19 +8725,42 @@ static int h2_parse_max_concurrent_streams(char **args, int section_type, struct
 {
        uint *vptr;
 
-       if (too_many_args(1, args, err, NULL))
-               return -1;
-
        /* backend/frontend/default */
        vptr = (args[0][8] == 'b') ? &h2_be_settings_max_concurrent_streams :
               (args[0][8] == 'f') ? &h2_fe_settings_max_concurrent_streams :
               &h2_settings_max_concurrent_streams;
 
+
+       if ((args[0][8] != 'f' && too_many_args(1, args, err, NULL)) ||
+           too_many_args(3, args, err, NULL))
+               return -1;
+
        *vptr = atoi(args[1]);
        if ((int)*vptr < 0) {
                memprintf(err, "'%s' expects a positive numeric value.", args[0]);
                return -1;
        }
+
+       if (args[0][8] != 'f')
+               goto leave;
+
+       /* tune.h2.fe. here */
+       if (strcmp(args[2], "rq-load") == 0) {
+               if (strcmp(args[3], "ignore") == 0)
+                       h2_fe_max_rq_load = ~0;
+               else if (strcmp(args[3], "auto") == 0)
+                       h2_fe_max_rq_load = 0;
+               else if (!*args[3] || (h2_fe_max_rq_load = atoi(args[3])) <= 0) {
+                       memprintf(err, "'%s' expects a strictly positive run-queue length, or 'auto' or 'ignore'.", args[0]);
+                       return -1;
+               }
+       }
+       else if (*args[2]) {
+               memprintf(err, "'%s' only supports 'rq-load' after the numeric value, but found '%s'.", args[0], args[2]);
+               return -1;
+       }
+
+ leave:
        return 0;
 }