対象のUnitがreload動作に対応している必要があるがUnitに対して設定ファイルの再起動などをを促す操作として使われることが多い機能だと思います。例えばnginxなどは公式で以下のようなサンプルが公開されています。systemctl reload nginx
などと実行するとExecReload
に設定されているコマンドが実行されます。下記の例だと/usr/sbin/nginx -s reload
が実行されますが内部的にはmasterプロセスを特定してSIGHUPを送るという実装となっています。nginxはSIGHUPを受け取るとダウンタイムなしでconfigのホットリロードを実行するというとても便利な機能です。
[Unit] Description=The NGINX HTTP and reverse proxy server After=syslog.target network-online.target remote-fs.target nss-lookup.target Wants=network-online.target [Service] Type=forking PIDFile=/run/nginx.pid ExecStartPre=/usr/sbin/nginx -t ExecStart=/usr/sbin/nginx ExecReload=/usr/sbin/nginx -s reload ExecStop=/bin/kill -s QUIT $MAINPID PrivateTmp=true [Install] WantedBy=multi-user.target
nginxはSIGHUPを受け取るとダウンタイムなしでconfigのホットリロードを実行するというとても便利な機能です。
この部分を少し細く見ていくと以下のような実装となっています。
- SIGHUPを受け取ったら呼ばれるシグナルハンドラが呼び出される
- シグナルハンドラの中でconfigの読み込みとパースが実行される
- 構文的に正しければ設定を反映させていく
このときに2番目に行っているシグナルハンドラの中でconfigの読み込みとパースが実行される
で構文的なエラーがあれば処理的にはnginxはエラーログへエラーが有ったことを書き込みreload処理は途中で終了するという流れになっています。このような実装となっているおかげで例えnginxでエラーとなるような構文のconfigファイルがデプロイされてもnginxは停止しないためサービス継続可能な状態を維持することができます。
ここで困るポイントとして例えばGitHub ActionsやらJenkinsやらでサーバへconfigをsyncしてreloadするような仕組みのときにconfigにバグがあるものがsyncされたとしても反映時のコマンドがsystemctl reload nginxとなっている場合は工夫して実装しないと気づくことができません。
なぜ気づけないの?
GitHub Actionsでsystemctl reload nginx
と書いたフローがある場合はコマンドの終了ステータスが0以外の場合はCI自体は失敗になります。reloadは上述したように以下のような設定になっています。reloadはあくまでもmasterプロセスを検出してSIGHUPを送るだけなのでmasterプロセスがいないかSIGHUPを送る権限の無いユーザで実行したみたいなケース以外ではめったに失敗しません。
[Service] Type=forking PIDFile=/run/nginx.pid ExecStartPre=/usr/sbin/nginx -t ExecStart=/usr/sbin/nginx ExecReload=/usr/sbin/nginx -s reload ExecStop=/bin/kill -s QUIT $MAINPID PrivateTmp=true
意図的にカンマを抜いたconfigをおいておいてreloadを実行すると以下のような出力がnginxのエラーログへ行われますがコマンド自体の終了ステータスは0となっています。
[emerg] 7336#7336: invalid number of arguments in "worker_processes" directive in /etc/nginx/nginx.conf:3
エラーを検知するには
MAINPID(今回で言うnginxのmasterプロセス)とExecReloadのプロセスでIPCをしていい感じに通知するかreloadの失敗時にプロセスをexit(1)してsystemdに再起動してもらうみたいな方法が取れそうです。後者はconfigエラーだと完全にサービス断になってしまうのでやるなら前者を頑張るになりそうです。
例えば/usr/sbin/nginx -s reload
を呼び出し時に内部的にkill(2)とかでシグナル送るのではなくshmctl(2)とかで共有メモリをmasterプロセスへ通知してmasterプロセスは共有メモリにreloadの結果を書き込むみたいな実装にしておけば呼び出し元は結果が書き込まれるまでブロックしておけばrealodの結果を読むことができるみたいな仕組み(共有メモリを例でも良さそう)