Platform Team/Sys-Infra Unitの伊豆です。今回は、pt-online-schema-change
にnohup
をつけてバックグラウンドで実行してもSIGHUP
を受け取って終了する現象に遭遇したので、そのときに調査した内容を共有します。
調査は以下の環境で行いました。
背景
ReproではAurora MySQLを使っていて、レコード数の多いテーブルのスキーマを変更するときに、pt-online-schema-changeを使うことがあります 1 。pt-online-schema-change
を実行するときは、実行用のサーバーにSSHでログインして実行しています。
中には10億レコード以上あるテーブルもあり、pt-online-schema-change
が完了するまで数日かかる場合があります。そのため、実行用のサーバーとのSSH接続が切れてもpt-online-schema-change
の実行が続くように、nohup
をつけてバックグラウンドでpt-online-schema-change
を実行しています。
それにもかかわらず、SIGHUP
を受け取ってpt-online-schema-change
が終了してしまう現象が時々発生し、そのたびにpt-online-schema-change
をやり直していました。
Copying `repro`.`users`: 4% 4+15:40:44 remain Copying `repro`.`users`: 4% 4+15:29:30 remain Copying `repro`.`users`: 4% 4+15:18:51 remain Copying `repro`.`users`: 4% 4+15:07:47 remain Copying `repro`.`users`: 4% 4+14:57:25 remain Copying `repro`.`users`: 4% 4+14:47:09 remain # Exiting on SIGHUP. Not dropping triggers because the tool was interrupted. To drop the triggers, execute:
何回かこの現象に遭遇して、以下の2つのことが分かりました。
- SSHクライアントが起動しているPCのスリープなどによってSSH接続が切れると、
pt-online-schema-change
がSIGHUP
を受け取って終了する exit
で終了した場合は、pt-online-schema-change
はSIGHUP
を受け取らずに実行が継続される
なぜnohupをつけて実行したpt-online-schema-changeがSIGHUPを受け取ったときに終了するのか
結論からいうと、pt-online-schema-change
がSIGHUPを受け取ったら終了するようにシグナルハンドラーを設定しているからです 2 。normal-signalsはSIGHUP
、 SIGINT
、 SIGPIPE
、 SIGTERM
を表しています。
nohup
はSIGHUPを無視するシグナルハンドラーを設定して引数として渡されたコマンドを実行(execvp
)します。execve
のmanページによると、POSIXでは無視するように設定されたシグナルハンドラーはexec
後も引き継がれることになっています。
POSIX.1 specifies that the dispositions of any signals that are ignored or set to the default are left unchanged
これによって、nohup
で実行したコマンドはSIGHUP
を受け取ってもそれを無視するようになります。しかし、pt-online-schema-change
のようにSIGHUP
を受け取ったときに終了するシグナルハンドラーを設定していると、最後に設定したシグナルハンドラーが有効になり、たとえnohup
をつけて実行していてもSIGHUP
を受け取ると終了してしまいます。
なぜSSH接続が切れたときにSIGHUP
を受け取り、exit
で終了した場合はSIGHUP
を受け取らないのか
nohup
をつけて実行したpt-online-schema-change
がSIGHUP
を受け取ったときに終了する理由は分かったので、次に「SSH接続が切れたときにSIGHUP
を受け取り、exit
で終了した場合はSIGHUP
を受け取らない」理由を調査しました。
その理由は、技術/UNIX/なぜnohupをバックグランドジョブとして起動するのが定番なのか?(擬似端末, Pseudo Terminal, SIGHUP他) - Glamenv-Septzen.netにとても詳しく書いてありました。
まとめると以下のような理由で、pt-online-schema-change
にSIGHUP
が送られたり、送られなかったりしていました。
- SSH接続が切れた場合
bash
はSIGHUP
を受け取って終了する。このときbash
は全てのジョブ(プロセスグループ)にSIGHUP
を送る
exit
で終了する場合huponexit
がオンの場合、bash
は全てのジョブ(プロセスグループ)にSIGHUP
を送り、オフの場合は送らない。デフォルトはオフ。
SSHでログインした直後の状態
SSHでログインした直後のsshd
やbash
プロセスは以下のようになっています。
sshd
とbash
は疑似端末を通して通信し、疑似端末のmaster側(pty master
)がsshd
、slave側(pty slave
)がbash
につながっています。このとき、疑似端末のslave側がセッションの制御端末になりbash
は新しいセッションのセッションリーダーになります。
ここから、nohup
をつけてpt-online-schema-change
をバックグラウンドで実行すると以下のようになります。
バックグラウンドで実行しているpt-online-schema-change
は、bash
と同じセッションにいますがプロセスグループは異なっています。
SSH接続が切れた場合
SSH接続が切れた場合、bash
はカーネルからSIGHUP
を受け取り終了します。このときの流れのイメージは以下のようになっています。
sshd
はSSH接続が切れたことを検知すると、疑似端末のmaster側(pty master
)をクローズします。
sshd
が入力を待っているときにSSH接続が切れたことを検知すると、clean_exit
が実行されて疑似端末のmaster側(pty master
)がクローズされます。
void server_loop2(struct ssh *ssh, Authctxt *authctxt) { ~ for (;;) { process_buffered_input_packets(ssh); ~~ } ~~~ static void process_buffered_input_packets(struct ssh *ssh) { ssh_dispatch_run_fatal(ssh, DISPATCH_NONBLOCK, NULL); }
https://github.com/openssh/openssh-portable/blob/V_8_7_P1/serverloop.c#L362
int ssh_dispatch_run(struct ssh *ssh, int mode, volatile sig_atomic_t *done) { ~~ for (;;) { ~~ r = ssh_packet_read_poll_seqnr(ssh, &type, &seqnr); if (r != 0) return r; ~~ } void ssh_dispatch_run_fatal(struct ssh *ssh, int mode, volatile sig_atomic_t *done) { int r; if ((r = ssh_dispatch_run(ssh, mode, done)) != 0) sshpkt_fatal(ssh, r, "%s", __func__); }
https://github.com/openssh/openssh-portable/blob/V_8_7_P1/dispatch.c#L129
int ssh_packet_read_poll_seqnr(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p) { ~~ for (;;) { ~~ r = ssh_packet_read_poll2(ssh, typep, seqnr_p); ~~ switch (*typep) { ~~ case SSH2_MSG_DISCONNECT: ~~ do_log2(ssh->state->server_side && reason == SSH2_DISCONNECT_BY_APPLICATION ? SYSLOG_LEVEL_INFO : SYSLOG_LEVEL_ERROR, "Received disconnect from %s port %d:" "%u: %.400s", ssh_remote_ipaddr(ssh), ssh_remote_port(ssh), reason, msg); ~~ return SSH_ERR_DISCONNECTED; ~~ }
https://github.com/openssh/openssh-portable/blob/V_8_7_P1/packet.c#L1714
static void sshpkt_vfatal(struct ssh *ssh, int r, const char *fmt, va_list ap) { switch (r) { ~~ case SSH_ERR_DISCONNECTED: ssh_packet_clear_keys(ssh); logdie("Disconnected from %s", remote_id); ~~ } void sshpkt_fatal(struct ssh *ssh, int r, const char *fmt, ...) { va_list ap; va_start(ap, fmt); sshpkt_vfatal(ssh, r, fmt, ap); /* NOTREACHED */ va_end(ap); logdie_f("should have exited"); }
https://github.com/openssh/openssh-portable/blob/V_8_7_P1/packet.c#L1897C1-L1907C2
void sshlogdie(const char *file, const char *func, int line, int showfunc, LogLevel level, const char *suffix, const char *fmt, ...) { va_list args; va_start(args, fmt); sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_INFO, suffix, fmt, args); va_end(args); cleanup_exit(255); }
https://github.com/openssh/openssh-portable/blob/V_8_7_P1/log.c#L435
void cleanup_exit(int i) { if (the_active_state != NULL && the_authctxt != NULL) { do_cleanup(the_active_state, the_authctxt); ~~ }
https://github.com/openssh/openssh-portable/blob/V_8_7_P1/sshd.c#L2429
void session_pty_cleanup2(Session *s) { /* * Close the server side of the socket pairs. We must do this after * the pty cleanup, so that another process doesn't get this pty * while we're still cleaning up. */ if (s->ptymaster != -1 && close(s->ptymaster) == -1) error("close(s->ptymaster/%d): %s", s->ptymaster, strerror(errno)); ~~ } ~~ void do_cleanup(struct ssh *ssh, Authctxt *authctxt) { ~~ session_destroy_all(ssh, session_pty_cleanup2); }
https://github.com/openssh/openssh-portable/blob/V_8_7_P1/session.c#L2643
このとき、カーネルからセッションリーダーであるbash
にSIGHUP
が送られます。
擬似端末の場合、"master"側デバイスのファイル記述子が全てclose()されたのを"hangup"の合図として、端末のデバイスドライバがセッションリーダーのプロセスにSIGHUPを送信する。
技術/UNIX/なぜnohupをバックグランドジョブとして起動するのが定番なのか?(擬似端末, Pseudo Terminal, SIGHUP他) - Glamenv-Septzen.netより
If fildes refers to the manager side of a pseudo-terminal, and this is the last close, a SIGHUP signal shall be sent to the controlling process
https://pubs.opengroup.org/onlinepubs/9799919799/functions/close.html
bash
はSIGHUP
を受け取るとhangup_all_jobs()
を実行し、J_NOHUP
フラグが付いていないジョブにSIGHUPを送ります。
The shell exits by default upon receipt of a SIGHUP. Before exiting, an interactive shell resends the SIGHUP to all jobs, running or stopped. Stopped jobs are sent SIGCONT to ensure that they receive the SIGHUP. To prevent the shell from sending the signal to a particular job, it should be removed from the jobs table with the disown builtin (see SHELL BUILTIN COMMANDS below) or marked to not receive SIGHUP using disown -h.
https://linux.die.net/man/1/bash
SIGHUP
を受け取ってもnohup
をつけて実行したコマンドはそれを無視しますが、pt-online-shema-change
のようにSIGHUP
を受け取ったときに終了するようにシグナルハンドラーを再設定していると、上述したようにたとえnohup
をつけていたとしてもSIGHUP
を受け取ったときに終了します。
exit
で終了した場合
exit
で終了するときは、SSH接続が切れた場合とは異なりexit_shell
で終了します。
該当コード
int exit_builtin (list) WORD_LIST *list; { ~~ return (exit_or_logout (list)); } ~~ static int exit_or_logout (list) WORD_LIST *list; { ~~ /* Exit the program. */ jump_to_top_level (EXITBLTIN); /*NOTREACHED*/ }
https://git.savannah.gnu.org/cgit/bash.git/tree/builtins/exit.def?h=bash-5.2#n58
int reader_loop () { ~~ case EXITBLTIN: current_command = (COMMAND *)NULL; EOF_Reached = EOF; goto exec_done; ~~ exec_done: QUIT; ~~ return (last_command_exit_value); }
https://git.savannah.gnu.org/cgit/bash.git/tree/eval.c?h=bash-5.2#n98
int main (argc, argv, env) int argc; char **argv, **env; #endif /* !NO_MAIN_ENV_ARG */ { ~~ /* Read commands until exit condition. */ reader_loop (); exit_shell (last_command_exit_value); }
https://git.savannah.gnu.org/cgit/bash.git/tree/shell.c?h=bash-5.2#n834
exit_shell
ではhuponexit
がオフの場合、hangup_all_jobs
を実行しないのでpt-online-schema-change
を実行しているプロセスグループにはSIGHUP
が送られません。huponexit
はデフォルトでオフになっています。
また、セッションリーダーが終了したときにカーネルからSIGHUP
が送られるのはフォアグラウンドのプロセスグループのみなので、バックグラウンドで実行しているpt-online-schema-change
はSIGHUP
を受け取らずそのまま実行が継続されます。
SIGHUPの受信にかかわらず、プロセスが終了するとき、そのプロセスがセッションリーダーだった場合、カーネルからそのセッションのフォアグラウンドプロセスグループに対してSIGHUPが送信される。
技術/UNIX/なぜnohupをバックグランドジョブとして起動するのが定番なのか?(擬似端末, Pseudo Terminal, SIGHUP他) - Glamenv-Septzen.netより
If the process is a controlling process, the SIGHUP signal shall be sent to each process in the foreground process group of the controlling terminal belonging to the calling process.
https://pubs.opengroup.org/onlinepubs/9799919799/functions/_Exit.html
対策
SSH接続が切れた場合でもpt-online-schema-change
の実行が継続されるようにするためには、以下のような対策が必要でした。
- バックグランドで
pt-online-schema-change
を実行してexit
で終了する - バックグラウンドで実行した
pt-online-schema-change
に対して、disown
を実行する screen
、tmux
などのターミナルマルチプレクサを使う
バックグランドでpt-online-schema-change
を実行してexit
で終了する
上述したようにhuponexit
がオフの場合、exit
で終了するとpt-online-schema-change
を実行しているプロセスグループにはSIGHUP
が送られません。
バックグラウンドで実行したpt-online-schema-change
に対して、disown
を実行する
bash
はSIGHUP
を受け取ると、J_NOHUP
フラグが付いていないジョブにSIGHUPを送ります。このJ_NOHUP
フラグはdisown -h
コマンドで指定したジョブに付けることができます。
該当コード
int disown_builtin (list) WORD_LIST *list; { ~~ case 'h': nohup_only = 1; break; ~~ else if (nohup_only) nohup_job (job); ~~ }
https://git.savannah.gnu.org/cgit/bash.git/tree/builtins/jobs.def?h=bash-5.2#n234
void nohup_job (job_index) int job_index; { register JOB *temp; if (js.j_jobslots == 0) return; if (temp = jobs[job_index]) temp->flags |= J_NOHUP; }
https://git.savannah.gnu.org/cgit/bash.git/tree/jobs.c?h=bash-5.2#n1598
J_NOHUP
フラグが付いていればbash
はそのジョブにSIGHUP
を送らないのでそのまま実行が継続されます。
また、オプションをつけずにdisown
を実行しpt-online-schema-change
をジョブリストから除外するのでも大丈夫です。
screen
、tmux
などのターミナルマルチプレクサを使う
tmux
で新しくセッションを作り、そのセッションでpt-online-schema-change
を実行したとき、sshd
、bash
、tmux
の関係は以下のようになっています。
tmux
で新しくセッションを作ると新しくbash2
が起動し、そのbash2
はtmux-server
と疑似端末を通して通信します。sshd
から起動されたbash1
のセッションでは、tmux-client
がフォアグラウンドで起動し疑似端末を通してsshd
と通信します。
SSH接続が切れると、カーネルからセッションリーダーのbash1
にSIGHUP
が送られbash1
から各ジョブに対してSIGHUP
を送りますが、SIGHUP
が送られるのはtmux client
であるため、pt-online-schema-change
は何も影響を受けずそのまま実行が継続されます。
最終的にはpt-online-schema-change
は以下のような状態になり、SSH接続が切れた場合でも実行が継続されます。
まとめ
今回は、pt-online-schema-change
にnohup
をつけてバックグラウンドで実行しても、SIGHUP
を受け取って終了する現象の調査内容を共有しました。nohup
をつけてバックグラウンドで実行しているコマンドがSIGHUP
を受け取って終了してしまうことに驚きましたが、対策は良く言及されているものが有効であることを再確認できました。
最後に、Reproではエンジニアを募集中です。もし興味を持っていただけるようでしたら、採用情報ページからぜひご応募ください。
参考にしたサイトやドキュメント
- 技術/UNIX/なぜnohupをバックグランドジョブとして起動するのが定番なのか?(擬似端末, Pseudo Terminal, SIGHUP他) - Glamenv-Septzen.net
- https://man7.org/linux/man-pages/
- https://pubs.opengroup.org/onlinepubs/9799919799/
- Terminals and pseudoterminals | Viacheslav Biriukov
- Terminal under the hood - TTY & PTY - Ahmed Yakout
- Tmux How-To - Product Documentation and Knowledge Base - Stromasys Wiki
- pt-online-schema-change の実行が必要かどうか判断するタイミングをより早くした話 - Repro Tech Blog↩
-
sig_int
という名前でSIGINT
以外のシグナルも対象にシグナルハンドラーを設定しているのが本当に意図したことなのか疑問ですが、ここでは意図通りであるという前提で話を進めます↩