LoginSignup
0
0

More than 5 years have passed since last update.

nginx Upgrading Executable on the Flyの解説

Posted at

はじめに

nginxのオンライン制御方法は本家ドキュメント にいろいろありますが特徴的なものにオンラインでのバイナリ入れ替え(バージョンアップ)があります。
珍しい機能なのでその仕組みを追いかけてみます。

前提知識

  • fd(file descriptor)
    日本語だとファイル記述子。
    ファイルやsocketにアサインされたIDで、データの読み書きの対象をこの番号を元で指定します。

  • fork()
    自身のプロセスのコピーを生成するシステムコール。
    fdを引き継ぐので、親のsocketを子が拾うことが出来る。

  • exec()
    実行中の自身を他のプログラムと置き換えるシステムコール。
    呼び出し元のfdやpid、メモリ空間を引き継ぎます。

fork()、exec()は仲間がいるので詳しくはman fork/execしてみてください。

概要

操作手順はドキュメント(上の"Upgrading Executable on the Fly")にあるように

  1. 旧Master(今動いてるMaster)にSIGUSR2を発行し新Master&新Workerを立ち上げる
  2. 旧MasterへSIGWINCH発行。旧Workerを停止し順次新Workerへ移行
  3. 旧MasterへSIGQUIT発行。旧Master停止

となっています。これを頭に入れつつ動作とコードを追っていきます 。
ソースはnginx-1.14.0

動作詳細

定義とか

は今動いているバイナリ/プロセス、はUpgrade先のバイナリ/プロセスを表します。
バイナリファイルは置き換わってることとします。

SIGUSR2の発行

現行MasterにUSR2を送ります(kill -USR2 pid)

src/os/unix/ngx_process.c#ngx_signal_handler

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のフラグを立てる。

src/os/unix/ngx_process_cycle.c#ngx_master_process_cycle

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)
実際の起動処理は以下で行われます。

src/core/nginx.c#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ファイルを元に戻します。

src/os/unix/ngx_process.c
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が次の次に呼び出される関数です。

src/os/unix/ngx_process.c#ngx_spawn_process
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)としての動作になります。

src/os/unix/ngx_process.c#ngx_execute_proc
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のイベントループ内で生成されます。

src/os/unix/ngx_process_cycle.c
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)

src/os/unix/ngx_process.c#ngx_signal_handler
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の新規受け付けを停止を指示します。

src/os/unix/ngx_process_cycle.c#ngx_master_process_cycle
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)

src/os/unix/ngx_process.c#ngx_signal_handler
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に設定し終了処理を指示します。

src/os/unix/ngx_process_cycle.c#ngx_master_process_cycle
 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()で何が引き継がれて何が個別になるのかを知っているとコードが読みやすいかと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0