はじめに
nginxのオンライン制御方法は本家ドキュメント にいろいろありますが特徴的なものにオンラインでのバイナリ入れ替え(バージョンアップ)があります。
珍しい機能なのでその仕組みを追いかけてみます。
前提知識
fd(file descriptor)
日本語だとファイル記述子。
ファイルやsocketにアサインされたIDで、データの読み書きの対象をこの番号を元で指定します。fork()
自身のプロセスのコピーを生成するシステムコール。
fdを引き継ぐので、親のsocketを子が拾うことが出来る。exec()
実行中の自身を他のプログラムと置き換えるシステムコール。
呼び出し元のfdやpid、メモリ空間を引き継ぎます。
fork()、exec()は仲間がいるので詳しくはman fork/execしてみてください。
概要
操作手順はドキュメント(上の"Upgrading Executable on the Fly")にあるように
- 旧Master(今動いてるMaster)にSIGUSR2を発行し新Master&新Workerを立ち上げる
- 旧MasterへSIGWINCH発行。旧Workerを停止し順次新Workerへ移行
- 旧MasterへSIGQUIT発行。旧Master停止
となっています。これを頭に入れつつ動作とコードを追っていきます 。
ソースはnginx-1.14.0
動作詳細
定義とか
旧は今動いているバイナリ/プロセス、新はUpgrade先のバイナリ/プロセスを表します。
バイナリファイルは置き換わってることとします。
SIGUSR2の発行
現行MasterにUSR2を送ります(kill -USR2 pid)
373
374 case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):
375 if (ngx_getppid() == ngx_parent || ngx_new_binary > 0) {
376
377 /*
378 * Ignore the signal in the new binary if its parent is
379 * not changed, i.e. the old binary's process is still
380 * running. Or ignore the signal in the old binary's
381 * process if the new binary's process is already running.
382 */
383
384 action = ", ignoring";
385 ignore = 1;
386 break;
387 }
388
389 ngx_change_binary = 1;
390 action = ", changing binary";
391 break;
392
※NGX_CHANGEBIN_SIGNALはSIGUSR2のalias
自身が親且つ新masterが存在しないならばngx_change_binaryを1に設定しupgradingのフラグを立てる。
270 }
271
272 if (ngx_change_binary) {
273 ngx_change_binary = 0;
274 ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "changing binary");
275 ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
276 }
277
旧Masterのイベントループ内で新Masterを起動(ngx_exec_new_binary)
実際の起動処理は以下で行われます。
709 if (ngx_rename_file(ccf->pid.data, ccf->oldpid.data) == NGX_FILE_ERROR) {
710 ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
711 ngx_rename_file_n " %s to %s failed "
712 "before executing new binary process \"%s\"",
713 ccf->pid.data, ccf->oldpid.data, argv[0]);
714
715 ngx_free(env);
716 ngx_free(var);
717
718 return NGX_INVALID_PID;
719 }
720
721 pid = ngx_execute(cycle, &ctx);
722
723 if (pid == NGX_INVALID_PID) {
724 if (ngx_rename_file(ccf->oldpid.data, ccf->pid.data)
725 == NGX_FILE_ERROR)
726 {
709行目のngx_rename_file()でpidファイルをrename、 721行目のngx_execute()で新Masterを立ち上げます。
失敗したら旧pidファイルを元に戻します。
261 ngx_pid_t
262 ngx_execute(ngx_cycle_t *cycle, ngx_exec_ctx_t *ctx)
263 {
264 return ngx_spawn_process(cycle, ngx_execute_proc, ctx, ctx->name,
265 NGX_PROCESS_DETACHED);
266 }
263行目でngx_spawn_proces()を呼び出します。
第2引数のngx_execute_procが次の次に呼び出される関数です。
183 ngx_process_slot = s;
184
185
186 pid = fork();
187
188 switch (pid) {
189
190 case -1:
191 ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
192 "fork() failed while spawning \"%s\"", name);
193 ngx_close_channel(ngx_processes[s].channel, cycle->log);
194 return NGX_INVALID_PID;
195
196 case 0:
197 ngx_parent = ngx_pid;
198 ngx_pid = ngx_getpid();
199 proc(cycle, data);
200 break;
201
202 default:
203 break;
204 }
205
186行目でfork()し成功の場合は199行目でproc(呼び元の第2引数でngx_execute_proc)を呼び出します。
親(旧Master)はSIGNAL処理はこれで完了し通常動作に戻ります。
以下は子(新Master)としての動作になります。
268 static void
269 ngx_execute_proc(ngx_cycle_t *cycle, void *data)
270 {
271 ngx_exec_ctx_t *ctx = data;
272
273 if (execve(ctx->path, ctx->argv, ctx->envp) == -1) {
274 ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
275 "execve() failed while executing %s \"%s\"",
276 ctx->name, ctx->path);
277 }
278
279 exit(1);
280 }
273行目のexec()の仲間のexecve()で同名のnginxバイナリに入れ替えます。
fdを引き継ぐので新Masterでも旧Masterのsocketへのアクセスが可能になります。
なお新MasterのWorkerは新Masterのイベントループ内で生成されます。
233 if (ngx_new_binary) {
234 ngx_start_worker_processes(cycle, ccf->worker_processes,
235 NGX_PROCESS_RESPAWN);
236 ngx_start_cache_manager_processes(cycle, 0);
237 ngx_noaccepting = 0;
238
239 continue;
240 }
これでドキュメントの
PID PPID USER %CPU VSZ WCHAN COMMAND
33126 1 root 0.0 1164 pause nginx: master process /usr/local/nginx/sbin/nginx
33134 33126 nobody 0.0 1368 kqread nginx: worker process (nginx)
33135 33126 nobody 0.0 1380 kqread nginx: worker process (nginx)
33136 33126 nobody 0.0 1368 kqread nginx: worker process (nginx)
36264 33126 root 0.0 1148 pause nginx: master process /usr/local/nginx/sbin/nginx
36265 36264 nobody 0.0 1364 kqread nginx: worker process (nginx)
36266 36264 nobody 0.0 1364 kqread nginx: worker process (nginx)
36267 36264 nobody 0.0 1364 kqread nginx: worker process (nginx)
というところまでできました。
旧Master(PID33126)と新Master(PID36264)が同時に起動しています。
この時点で新旧Workerがリクエストを処理しています。
SIGWINCH
旧バージョンでの受付を停止させるため旧MasterへWINCHを発行します
(kill -WINCH oldpid)
357 case ngx_signal_value(NGX_NOACCEPT_SIGNAL):
358 if (ngx_daemonized) {
359 ngx_noaccept = 1;
360 action = ", stop accepting connections";
361 }
362 break;
※NGX_NOACCEPT_SIGNALはWINCHのalias
ngx_noacceptを1に設定して旧Workerの新規受け付けを停止を指示します。
277
278 if (ngx_noaccept) {
279 ngx_noaccept = 0;
280 ngx_noaccepting = 1;
281 ngx_signal_worker_processes(cycle,
282 ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
283 }
旧Workerを(graceful)shutdownします。
この時点でドキュメントの三つめの状態になります。
PID PPID USER %CPU VSZ WCHAN COMMAND
33126 1 root 0.0 1164 pause nginx: master process /usr/local/nginx/sbin/nginx
36264 33126 root 0.0 1148 pause nginx: master process /usr/local/nginx/sbin/nginx
36265 36264 nobody 0.0 1364 kqread nginx: worker process (nginx)
36266 36264 nobody 0.0 1364 kqread nginx: worker process (nginx)
36267 36264 nobody 0.0 1364 kqread nginx: worker process (nginx)
この時点で起動中のプロセスは
- 旧Master(33126)
- 新Master(36264)
- 新Worker(36265-36267)
の3つです。
元に戻したいときは新MasterへTERMを発行すれば、新Worker停止後旧Masterが旧Workerを立ち上げ元の状態に戻ります。
SIGQUIT
後処理として旧Masterを停止させます。
旧MasterへQUITを発行します(kill -QUIT oldpid)
342 case NGX_PROCESS_MASTER:
343 case NGX_PROCESS_SINGLE:
344 switch (signo) {
345
346 case ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
347 ngx_quit = 1;
348 action = ", shutting down";
349 break;
350
※NGX_SHUTDOWN_SIGNALはQUITのalias
ngx_quitを1に設定し終了処理を指示します。
177
178 if (!live && (ngx_terminate || ngx_quit)) {
179 ngx_master_process_exit(cycle);
180 }
181
203
204 if (ngx_quit) {
205 ngx_signal_worker_processes(cycle,
206 ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
207
208 ls = cycle->listening.elts;
179行目がMasterの終了処理起動箇所です。
liveはWorkerの有無で、もしあれば213行目からの処理で先に旧Workerの終了処理を実施します。
これで切り替え完了です。
PID PPID USER %CPU VSZ WCHAN COMMAND
36264 1 root 0.0 1148 pause nginx: master process /usr/local/nginx/sbin/nginx
36265 36264 nobody 0.0 1364 kqread nginx: worker process (nginx)
36266 36264 nobody 0.0 1364 kqread nginx: worker process (nginx)
36267 36264 nobody 0.0 1364 kqread nginx: worker process (nginx)
ポイント
二つのシステムコールがポイントです。
fork()
今動いている自プロセスをメモリ空間ごとコピーして新規プロセスを起動する。
メモリ上に載っているものの複製なので元のバイナリが変わろうとも今動いてるものと同じものが複製される。fdも複製される。exec()
今動いてる自プロセスに代わって新たにプロセスを起動する。
新たに起動されるのでバイナリが変更されていれば変更後のバイナリが起動される。
fdは引き継がれるがメモリ上のデータ(フラグなど)は引き継がれない。
nginxの実装では
旧Master/旧バイナリ ⇒ fork()で新Master/旧バイナリを生成
メモリ(各種フラグ)やsocket(fd)を引き継ぎ、フラグによりMasterに自分の役割(新旧)を認識させています。新Master/旧バイナリ ⇒ exec()で新Master/新バイナリに変身
この時フラグ(メモリ空間)は不要なのと新たなバイナリで起動させたいためfork()ではなくexec()を用います。
exec()でもfdは引き継がれるので旧Masterが開いたsocketは新バイナリでも利用できます。
となっていて、クラウドのブルーグリーンデプロイメントのイメージが近いかと思います。
おわりに
nginxのバイナリ入れ替えのコードを追ってみました。
fork()やexec()で何が引き継がれて何が個別になるのかを知っているとコードが読みやすいかと思います。