地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

systemctl reloadでエラーを検知するすべはあるのかを考えてみたけど特になさそうだった話

対象の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.com

ここで困るポイントとして例えば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の結果を読むことができるみたいな仕組み(共有メモリを例でも良さそう)