[PATCH 2 of 2] Added "max_shutdown_workers" directive
Maxim Dounin
mdounin at mdounin.ru
Mon Sep 25 19:28:12 UTC 2023
Hello!
On Mon, Aug 28, 2023 at 04:30:44PM +0400, Roman Arutyunyan wrote:
> # HG changeset patch
> # User Roman Arutyunyan <arut at nginx.com>
> # Date 1693218974 -14400
> # Mon Aug 28 14:36:14 2023 +0400
> # Node ID 42c3166b675a6a7624bdf3d78c0d4685ce8172e3
> # Parent fdad00808cb485ab83e919be59e9211ef06ac56a
> Added "max_shutdown_workers" directive.
>
> The directive sets maximum number of workers in shutdown state while reloading
> nginx configuration. Upon reaching this value, workers restart is postponed
> until shutdown workers start exiting. This allows for lower peak resource
> consumption at the cost of slower reconfiguration.
I can't say I like the name, as the preferred term is "worker
processes", not "workers".
max_shutdown_processes?
Also note that this doesn't really limit the total resource usage
during configuration reloads, since cache manager and cache loader
processes are exempt from the limit, but new processes are started
on each configuration reload. It would be nice to actually
provide a limit which makes multiple/frequent configuration reloads
completely safe from the resource usage point of view, that is, to
limit all processes spawned during configuration reloads, and not
just worker processes.
>
> diff --git a/src/core/nginx.c b/src/core/nginx.c
> --- a/src/core/nginx.c
> +++ b/src/core/nginx.c
> @@ -83,6 +83,13 @@ static ngx_command_t ngx_core_commands[
> 0,
> NULL },
>
> + { ngx_string("max_shutdown_workers"),
> + NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1,
> + ngx_conf_set_num_slot,
> + 0,
> + offsetof(ngx_core_conf_t, max_shutdown_workers),
> + NULL },
> +
> { ngx_string("debug_points"),
> NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1,
> ngx_conf_set_enum_slot,
> @@ -1117,6 +1124,7 @@ ngx_core_module_create_conf(ngx_cycle_t
> ccf->shutdown_timeout = NGX_CONF_UNSET_MSEC;
>
> ccf->worker_processes = NGX_CONF_UNSET;
> + ccf->max_shutdown_workers = NGX_CONF_UNSET;
> ccf->debug_points = NGX_CONF_UNSET;
>
> ccf->rlimit_nofile = NGX_CONF_UNSET;
> @@ -1146,6 +1154,7 @@ ngx_core_module_init_conf(ngx_cycle_t *c
> ngx_conf_init_msec_value(ccf->shutdown_timeout, 0);
>
> ngx_conf_init_value(ccf->worker_processes, 1);
> + ngx_conf_init_value(ccf->max_shutdown_workers, 0);
> ngx_conf_init_value(ccf->debug_points, 0);
>
> #if (NGX_HAVE_CPU_AFFINITY)
> diff --git a/src/core/ngx_cycle.h b/src/core/ngx_cycle.h
> --- a/src/core/ngx_cycle.h
> +++ b/src/core/ngx_cycle.h
> @@ -94,6 +94,7 @@ typedef struct {
> ngx_msec_t shutdown_timeout;
>
> ngx_int_t worker_processes;
> + ngx_int_t max_shutdown_workers;
> ngx_int_t debug_points;
>
> ngx_int_t rlimit_nofile;
> diff --git a/src/os/unix/ngx_process.c b/src/os/unix/ngx_process.c
> --- a/src/os/unix/ngx_process.c
> +++ b/src/os/unix/ngx_process.c
> @@ -216,6 +216,7 @@ ngx_spawn_process(ngx_cycle_t *cycle, ng
> ngx_processes[s].data = data;
> ngx_processes[s].name = name;
> ngx_processes[s].exiting = 0;
> + ngx_processes[s].old = 0;
>
> switch (respawn) {
>
> diff --git a/src/os/unix/ngx_process.h b/src/os/unix/ngx_process.h
> --- a/src/os/unix/ngx_process.h
> +++ b/src/os/unix/ngx_process.h
> @@ -33,6 +33,7 @@ typedef struct {
> unsigned detached:1;
> unsigned exiting:1;
> unsigned exited:1;
> + unsigned old:1;
> } ngx_process_t;
>
>
> diff --git a/src/os/unix/ngx_process_cycle.c b/src/os/unix/ngx_process_cycle.c
> --- a/src/os/unix/ngx_process_cycle.c
> +++ b/src/os/unix/ngx_process_cycle.c
> @@ -15,6 +15,7 @@ static void ngx_start_worker_processes(n
> ngx_int_t type);
> static void ngx_start_worker_process(ngx_cycle_t *cycle, ngx_int_t i,
> ngx_int_t type);
> +static void ngx_restart_worker_processes(ngx_cycle_t *cycle);
> static void ngx_start_cache_manager_processes(ngx_cycle_t *cycle,
> ngx_uint_t respawn);
> static void ngx_pass_open_channel(ngx_cycle_t *cycle);
> @@ -22,6 +23,7 @@ static void ngx_signal_worker_processes(
> static void ngx_signal_worker_process(ngx_cycle_t *cycle, ngx_int_t i,
> int signo);
> static ngx_uint_t ngx_reap_children(ngx_cycle_t *cycle);
> +static ngx_uint_t ngx_restart_worker_process(ngx_cycle_t *cycle);
> static void ngx_master_process_exit(ngx_cycle_t *cycle);
> static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data);
> static void ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker);
> @@ -235,8 +237,8 @@ ngx_master_process_cycle(ngx_cycle_t *cy
> ngx_cycle = cycle;
> ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
> ngx_core_module);
> - ngx_start_worker_processes(cycle, ccf->worker_processes,
> - NGX_PROCESS_JUST_RESPAWN);
> + ngx_restart_worker_processes(cycle);
> +
> ngx_start_cache_manager_processes(cycle, 1);
>
> /* allow new processes to start */
> @@ -360,6 +362,70 @@ ngx_start_worker_process(ngx_cycle_t *cy
>
>
> static void
> +ngx_restart_worker_processes(ngx_cycle_t *cycle)
> +{
> + ngx_int_t i, n, m, worker;
> + ngx_core_conf_t *ccf;
> +
> + ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
> +
> + n = 0;
> + m = ccf->max_shutdown_workers;
> +
> + if (m == 0) {
> + goto start;
> + }
> +
> + for (i = 0; i < ngx_last_process; i++) {
> +
> + if (ngx_processes[i].pid == -1
> + || ngx_processes[i].proc != ngx_worker_process_cycle)
> + {
Checking "proc" here looks like a layering violation. For
example, ngx_signal_worker_process() does try to depend on the
process entry point, and instead relies on the "detached" flag to
decide which processes need to be signalled.
> + continue;
> + }
> +
> + ngx_processes[i].old = 0;
> + worker = (intptr_t) ngx_processes[i].data;
> +
> + if (!ngx_processes[i].exiting && worker < ccf->worker_processes) {
> + n++;
It is not clear what we are counting here, and why. My best guess
it that it's an attempt to count worker processes we are going to
restart (and not just stop).
It is not clear why we are doing this though: the limit as defined
does not differentiate between shutting down processes being
stopped and processes being restarted.
Further, I don't think it's done correctly: since "worker" is an
index number of the worker process, there can be at least gaps (if
worker process failed to start and cannot be respawn).
> +
> + } else if (m > 0) {
> + m--;
> + }
> + }
> +
> + n = (n > m) ? n - m : 0;
n = ngx_max(n - m, 0);
?
Also, some better names for the variables and/or some comments
explaining what is being done might worth the effort.
(As far as I can tell, "n" here is the number of processes we want
to restart but not allowed to.)
> +
> + if (n == 0) {
> + goto start;
> + }
> +
> + for (i = 0; i < ngx_last_process; i++) {
> +
> + if (ngx_processes[i].pid == -1
> + || ngx_processes[i].proc != ngx_worker_process_cycle)
> + {
> + continue;
> + }
> +
> + ngx_processes[i].old = 1;
> + worker = (intptr_t) ngx_processes[i].data;
> +
> + if (!ngx_processes[i].exiting && worker < n) {
Here "n" is the number of processes we are not allowed to restart
right now, while "worker" is the index number of the worker
process in question. For a given "n", number of worker processes
matching this condition might be at least less than "n": if some
worker processes failed to start.
(Just in case, the rule of thumb is: if you are using worker
process index number for anything, most likely you are doing it
wrong.)
> + ngx_processes[i].just_spawn = 1;
The "just_spawn" flag implies the process was just started, and
so it is ignored by the following ngx_signal_worker_process()
call. Abusing it to ignore processes during shutdown might work,
but I can't say I like this approach. Using a separate flag might
be better.
> + }
> + }
> +
> +start:
> +
> + for (i = n; i < ccf->worker_processes; i++) {
> + ngx_start_worker_process(cycle, i, NGX_PROCESS_JUST_RESPAWN);
> + }
The order of processes being started looks somewhat
counter-intuitive here.
> +}
> +
> +
> +static void
> ngx_start_cache_manager_processes(ngx_cycle_t *cycle, ngx_uint_t respawn)
> {
> ngx_uint_t i, manager, loader;
> @@ -486,15 +552,16 @@ ngx_signal_worker_process(ngx_cycle_t *c
> ch.fd = -1;
>
>
> - ngx_log_debug7(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
> - "child: %i %P e:%d t:%d d:%d r:%d j:%d",
> + ngx_log_debug8(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
> + "child: %i %P e:%d t:%d d:%d r:%d j:%d o:%d",
> i,
> ngx_processes[i].pid,
> ngx_processes[i].exiting,
> ngx_processes[i].exited,
> ngx_processes[i].detached,
> ngx_processes[i].respawn,
> - ngx_processes[i].just_spawn);
> + ngx_processes[i].just_spawn,
> + ngx_processes[i].old);
>
> if (ngx_processes[i].detached || ngx_processes[i].pid == -1) {
> return;
> @@ -563,15 +630,16 @@ ngx_reap_children(ngx_cycle_t *cycle)
> live = 0;
> for (i = 0; i < ngx_last_process; i++) {
>
> - ngx_log_debug7(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
> - "child: %i %P e:%d t:%d d:%d r:%d j:%d",
> + ngx_log_debug8(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
> + "child: %i %P e:%d t:%d d:%d r:%d j:%d o:%d",
> i,
> ngx_processes[i].pid,
> ngx_processes[i].exiting,
> ngx_processes[i].exited,
> ngx_processes[i].detached,
> ngx_processes[i].respawn,
> - ngx_processes[i].just_spawn);
> + ngx_processes[i].just_spawn,
> + ngx_processes[i].old);
>
> if (ngx_processes[i].pid == -1) {
> continue;
> @@ -660,6 +728,16 @@ ngx_reap_children(ngx_cycle_t *cycle)
> ngx_processes[i].pid = -1;
> }
>
Note: when a process dies, it is restarted if the "respawn" flag
is set, regardless of the "old" flag. The "old" flag is preserved
even if it is no longer relevant after the respawn: the process is
restarted with the new configuration, and there is no need to
restart it again.
> + if (ngx_processes[i].old
> + && ngx_processes[i].exiting
> + && !ngx_terminate
> + && !ngx_quit)
> + {
> + if (ngx_restart_worker_process(cycle)) {
> + live = 1;
> + }
> + }
> +
> } else if (ngx_processes[i].exiting || !ngx_processes[i].detached) {
> live = 1;
> }
> @@ -669,6 +747,53 @@ ngx_reap_children(ngx_cycle_t *cycle)
> }
>
>
> +static ngx_uint_t
> +ngx_restart_worker_process(ngx_cycle_t *cycle)
> +{
> + ngx_int_t i, j, m;
> + ngx_core_conf_t *ccf;
> +
> + ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
> +
> + j = -1;
> + m = ccf->max_shutdown_workers;
> +
> + if (m == 0) {
> + return 0;
> + }
> +
> + for (i = 0; i < ngx_last_process; i++) {
> +
> + if (ngx_processes[i].pid == -1 || !ngx_processes[i].old) {
> + continue;
> + }
> +
> + if (!ngx_processes[i].exiting && !ngx_processes[i].exited) {
The "exited" check here is not needed to catch the processes we
were called for, or other processes being shut down - they already
have "exiting" flag set.
On the other hand, this can catch unexpectedly died processes
(the ones which are still waiting for restart). As per the code,
such processes will be counted against max_shutdown_workers as if
they are shutting down - this looks wrong and can cause reload
hang.
> + j = i;
> + continue;
> + }
> +
> + if (--m == 0) {
> + return 0;
> + }
> + }
> +
> + if (j == -1) {
> + return 0;
> + }
> +
> + ngx_start_worker_process(cycle, (intptr_t) ngx_processes[j].data,
> + NGX_PROCESS_RESPAWN);
> +
> + ngx_msleep(100);
> +
> + ngx_signal_worker_process(cycle, j,
> + ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
Also note that the "exited" check above implies that there can be
multiple exited processes at the same time, but we only restart
just one process here.
It might be a good idea to restart multiple processes at the same
time: sleeping for 100ms for each process means that if, for
example, 64 processes will exit at the same time after shutdown
timeout expiration, master process will be unresponsive for 6
seconds.
Also, this looks very close to ngx_restart_worker_processes(). It
might be a good idea to rework things to ensure unified approach
to restarting processes instead of providing multiple duplicate
ways.
> +
> + return 1;
> +}
> +
> +
> static void
> ngx_master_process_exit(ngx_cycle_t *cycle)
> {
Overall, I tend to think that the suggested code is very fragile,
and I would rather consider re-thinking the approach.
--
Maxim Dounin
http://mdounin.ru/
More information about the nginx-devel
mailing list