本稿はHeroku Advent Calendar 2020の6日目の記事です。5日目はsho7650さんによる「[Heroku] index.html をダイレクトに開く静的コンテンツサイトを、すぐ公開したい」でした。buildpackは奥が深いよ!! 7日目はkimihomさんによる「Heroku Addons で SendGrid を使ってる方が必要な年内対応」です。SendGridをご利用の方はお早めに!
Herokuではdynoの停止時にすべてのプロセスにSIGTERMを送り、メインプロセスの停止を待ってからdynoを停止させます(graceful shutdown)。ここでメインプロセスがシェルの場合には、SIGTERMの送信直後にシェルが停止するためdynoが停止されてしまいます。本稿ではシェルがメインプロセスの場合にもSIGTERMを無視するようにして、graceful shutdownするようにしてみます。
Dynoでのプロセスの起動
Dynoの起動後にps -xf
コマンドを実行してプロセスツリーを確認してみると、Herokuではbashをログインシェルとして起動しProcfile
の内容を実行させることで、スタートアップスクリプトの読み込みもおこなっているようです。
bash --login -c "puma -C config/puma.rb"
Bashによるプロセスの実行
多くのHerokuアプリでは、プラットフォームが起動したbashはProcfile
で指定されたコマンドをexecして自プロセスと置き換えます。Dynoの停止時にプラットフォームから全てのプロセスに送られたSIGTERMはProcfile
で指定されたプロセスが直接受け取ることになります。Pumaなど多くのサーバはSIGTERMを受け取るとgraceful shutdownするようになっています。
プロセスツリーにはProcfile
で指定されたコマンドの親プロセスは見られません。
puma -C config/puma.rb
Bashがコマンドをexecできない場合には、指定されたコマンドをforkして子プロセスとして実行します。プラットフォームから送られたSIGTERMはbashにも届き、bashは停止し、アプリのプロセスが稼動を続けていてもdynoは停止されてしまいます。
Bashのソースコードのコメントによれば、スタートアップスクリプトにtrapがあった場合にはbashはコマンドをexecするのではなくforkするようです。この場合プロセスツリーにはProcfile
で指定されたコマンドの親プロセスとしてbash
が見られます。
bash --login -c "puma -C config/puma.rb"
\_ puma -C config/puma.rb
Bashは起動するコマンドが複文だった場合にもコマンドをforkするようです。
bash --login -c "puma -C config/puma.rb & sidekiq &"
\_ puma -C config/puma.rb
\_ sidekiq
Forkするアプリでもgraceful shutdownしてもらう
メインプロセスがforkされるアプリでもgraceful shutdownさせるには、SIGTERMを受け取ってもbashが停止しないように、SIGTERMを無視させると良さそうです。Procfile
の最初に下記を実行します。
trap '' SIGTERM
ついでに、複数稼動しているプロセスのいずれかが停止してしまった場合に、他のプロセスにもSIGTERMを送り、graceful shutdownしてもらいます。いずれかのバックグラウンドプロセスが停止するまでwait -n
で待ち、kill
コマンドですべてのバックグラウンドプロセスにSIGTERMを送り、そして、すべてのプロセスが停止するまでwait
で待ちます:
wait -n; kill -SIGTERM -$$: wait
これらを併せて、pumaとsidekiqをバックグラウンドで起動しているアプリケーションでsidekiqがちゃんとshutdownするようになりました。
web: trap '' SIGTERM; puma -C config/puma.rb & sidekiq & wait -n; kill -SIGTERM -$$; wait
アプリケーションログを確認すると、trap
の追加前にはbashがシグナルを受けて停止していた(exit statusが143だった)のに対して:
2020-10-27T18:28:53.674073+00:00 heroku web.1 - - Restarting
2020-10-27T18:28:53.676489+00:00 heroku web.1 - - State changed from up to starting
2020-10-27T18:28:54.965022+00:00 heroku web.1 - - Stopping all processes with SIGTERM
2020-10-27T18:28:55.263424+00:00 heroku web.1 - - Process exited with status 143
trap
の追加後にはそれぞれのバックグラウンドプロセスが停止してからbashが停止する様子が確認できます:
2020-11-04T19:25:17.574465+00:00 heroku[web.1]: Restarting
2020-11-04T19:25:17.587549+00:00 heroku[web.1]: State changed from up to starting
2020-11-04T19:25:18.621855+00:00 heroku[web.1]: Stopping all processes with SIGTERM
2020-11-04T19:25:18.660761+00:00 app[web.1]: [7] - Gracefully shutting down workers...
2020-11-04T19:25:18.660678+00:00 app[web.1]: pid=8 tid=1hsg DEBUG: Got TERM signal
2020-11-04T19:25:18.660736+00:00 app[web.1]: pid=8 tid=1hsg INFO: Shutting down
2020-11-04T19:25:18.660788+00:00 app[web.1]: pid=8 tid=1hsg INFO: Terminating quiet workers
2020-11-04T19:25:18.661283+00:00 app[web.1]: pid=8 tid=3fw8 INFO: Scheduler exiting...
2020-11-04T19:25:19.164122+00:00 app[web.1]: pid=8 tid=1hsg INFO: Bye!
2020-11-04T19:25:19.272989+00:00 heroku[web.1]: Process exited with status 0