MEDIUM: listener: add support for limiting the session rate in addition to the connection rate

It's sometimes useful to be able to limit the connection rate on a machine
running many haproxy instances (eg: per customer) but it removes the ability
for that machine to defend itself against a DoS. Thus, better also provide a
limit on the session rate, which does not include the connections rejected by
"tcp-request connection" rules. This permits to have much higher limits on
the connection rate without having to raise the session rate limit to insane
values.

The limit can be changed on the CLI using "set rate-limit sessions global",
or in the global section using "maxsessrate".
This commit is contained in:
Willy Tarreau 2013-10-07 18:51:07 +02:00
parent 71b734c307
commit 93e7c006c1
5 changed files with 103 additions and 4 deletions

View File

@ -465,6 +465,7 @@ The following keywords are supported in the "global" section :
- maxcomprate
- maxcompcpuusage
- maxpipes
- maxsessrate
- maxsslconn
- noepoll
- nokqueue
@ -733,6 +734,16 @@ maxpipes <number>
The splice code dynamically allocates and releases pipes, and can fall back
to standard copy, so setting this value too low may only impact performance.
maxsessrate <number>
Sets the maximum per-process number of sessions per second to <number>.
Proxies will stop accepting connections when this limit is reached. It can be
used to limit the global capacity regardless of each frontend capacity. It is
important to note that this can only be used as a service protection measure,
as there will not necessarily be a fair share between frontends when the
limit is reached, so it's a good idea to also limit each frontend to some
value close to its expected share. Also, lowering tune.maxaccept can improve
fairness.
maxsslconn <number>
Sets the maximum per-process number of concurrent SSL connections to
<number>. By default there is no SSL-specific limit, which means that the
@ -12598,6 +12609,12 @@ set rate-limit http-compression global <value>
passed in number of kilobytes per second. The value is available in the "show
info" on the line "CompressBpsRateLim" in bytes.
set rate-limit sessions global <value>
Change the process-wide session rate limit, which is set by the global
'maxsessrate' setting. A value of zero disables the limitation. This limit
applies to all frontends and the change has an immediate effect. The value
is passed in number of sessions per second.
set table <table> key <key> [data.<data_type> <value>]*
Create or update a stick-table entry in the table. If the key is not present,
an entry is inserted. See stick-table in section 4.2 to find all possible

View File

@ -80,9 +80,11 @@ struct global {
char *connect_default_ciphers;
#endif
struct freq_ctr conn_per_sec;
struct freq_ctr sess_per_sec;
struct freq_ctr comp_bps_in; /* bytes per second, before http compression */
struct freq_ctr comp_bps_out; /* bytes per second, after http compression */
int cps_lim, cps_max;
int sps_lim, sps_max;
int comp_rate_lim; /* HTTP compression rate limit */
int maxpipes; /* max # of pipes */
int maxsock; /* max # of sockets */

View File

@ -875,6 +875,19 @@ int cfg_parse_global(const char *file, int linenum, char **args, int kwm)
}
global.cps_lim = atol(args[1]);
}
else if (!strcmp(args[0], "maxsessrate")) {
if (global.sps_lim != 0) {
Alert("parsing [%s:%d] : '%s' already specified. Continuing.\n", file, linenum, args[0]);
err_code |= ERR_ALERT;
goto out;
}
if (*(args[1]) == 0) {
Alert("parsing [%s:%d] : '%s' expects an integer argument.\n", file, linenum, args[0]);
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
}
global.sps_lim = atol(args[1]);
}
else if (!strcmp(args[0], "maxcomprate")) {
if (*(args[1]) == 0) {
Alert("parsing [%s:%d] : '%s' expects an integer argument in kb/s.\n", file, linenum, args[0]);

View File

@ -1146,6 +1146,7 @@ static int stats_sock_parse_request(struct stream_interface *si, char *line)
}
global.cps_max = 0;
global.sps_max = 0;
return 1;
}
else if (strcmp(args[1], "table") == 0) {
@ -1424,6 +1425,43 @@ static int stats_sock_parse_request(struct stream_interface *si, char *line)
return 1;
}
}
else if (strcmp(args[2], "sessions") == 0) {
if (strcmp(args[3], "global") == 0) {
int v;
if (s->listener->bind_conf->level < ACCESS_LVL_ADMIN) {
appctx->ctx.cli.msg = stats_permission_denied_msg;
appctx->st0 = STAT_CLI_PRINT;
return 1;
}
if (!*args[4]) {
appctx->ctx.cli.msg = "Expects an integer value.\n";
appctx->st0 = STAT_CLI_PRINT;
return 1;
}
v = atoi(args[4]);
if (v < 0) {
appctx->ctx.cli.msg = "Value out of range.\n";
appctx->st0 = STAT_CLI_PRINT;
return 1;
}
global.sps_lim = v;
/* Dequeues all of the listeners waiting for a resource */
if (!LIST_ISEMPTY(&global_listener_queue))
dequeue_all_listeners(&global_listener_queue);
return 1;
}
else {
appctx->ctx.cli.msg = "'set rate-limit sessions' only supports 'global'.\n";
appctx->st0 = STAT_CLI_PRINT;
return 1;
}
}
else if (strcmp(args[2], "http-compression") == 0) {
if (strcmp(args[3], "global") == 0) {
int v;
@ -1444,7 +1482,7 @@ static int stats_sock_parse_request(struct stream_interface *si, char *line)
}
}
else {
appctx->ctx.cli.msg = "'set rate-limit' supports 'connections' and 'http-compression'.\n";
appctx->ctx.cli.msg = "'set rate-limit' supports 'connections', 'sessions', and 'http-compression'.\n";
appctx->st0 = STAT_CLI_PRINT;
return 1;
}
@ -2182,6 +2220,9 @@ static int stats_dump_info_to_buffer(struct stream_interface *si)
"ConnRate: %d\n"
"ConnRateLimit: %d\n"
"MaxConnRate: %d\n"
"SessRate: %d\n"
"SessRateLimit: %d\n"
"MaxSessRate: %d\n"
"CompressBpsIn: %u\n"
"CompressBpsOut: %u\n"
"CompressBpsRateLim: %u\n"
@ -2209,6 +2250,7 @@ static int stats_dump_info_to_buffer(struct stream_interface *si)
#endif
global.maxpipes, pipes_used, pipes_free,
read_freq_ctr(&global.conn_per_sec), global.cps_lim, global.cps_max,
read_freq_ctr(&global.sess_per_sec), global.sps_lim, global.sps_max,
read_freq_ctr(&global.comp_bps_in), read_freq_ctr(&global.comp_bps_out),
global.comp_rate_lim,
#ifdef USE_ZLIB

View File

@ -263,13 +263,31 @@ void listener_accept(int fd)
return;
}
if (global.cps_lim && !(l->options & LI_O_UNLIMITED)) {
int max = freq_ctr_remain(&global.conn_per_sec, global.cps_lim, 0);
if (!(l->options & LI_O_UNLIMITED) && global.sps_lim) {
int max = freq_ctr_remain(&global.sess_per_sec, global.sps_lim, 0);
int expire;
if (unlikely(!max)) {
/* frontend accept rate limit was reached */
limit_listener(l, &global_listener_queue);
task_schedule(global_listener_queue_task, tick_add(now_ms, next_event_delay(&global.conn_per_sec, global.cps_lim, 0)));
expire = tick_add(now_ms, next_event_delay(&global.sess_per_sec, global.sps_lim, 0));
task_schedule(global_listener_queue_task, tick_first(expire, global_listener_queue_task->expire));
return;
}
if (max_accept > max)
max_accept = max;
}
if (!(l->options & LI_O_UNLIMITED) && global.cps_lim) {
int max = freq_ctr_remain(&global.conn_per_sec, global.cps_lim, 0);
int expire;
if (unlikely(!max)) {
/* frontend accept rate limit was reached */
limit_listener(l, &global_listener_queue);
expire = tick_add(now_ms, next_event_delay(&global.conn_per_sec, global.cps_lim, 0));
task_schedule(global_listener_queue_task, tick_first(expire, global_listener_queue_task->expire));
return;
}
@ -411,6 +429,13 @@ void listener_accept(int fd)
return;
}
/* increase the per-process number of cumulated connections */
if (!(l->options & LI_O_UNLIMITED)) {
update_freq_ctr(&global.sess_per_sec, 1);
if (global.sess_per_sec.curr_ctr > global.sps_max)
global.sps_max = global.sess_per_sec.curr_ctr;
}
} /* end of while (max_accept--) */
/* we've exhausted max_accept, so there is no need to poll again */