Puma は Ruby のアプリケーションサーバの一種であり、Ruby on Rails がデフォルトで使用していることでも有名です。
そんな Puma をサーバにデプロイする方法を調べていると、一昔前まではデーモン化 (Daemonization) する設定があったそうですが、現在はその設定が廃止されていました。
そこで
- Puma をデーモン化する設定はなぜ廃止されたのか
- 代替手段の puma-daemon によるデーモン化はどのように実現されているのか
を調べたところいろいろ勉強になったので、記事としてまとめます。
前提
Puma とは?という方には、Ruby における Web サーバとアプリケーションサーバについて説明している以下の翻訳記事が分かりやすくてオススメです。
Puma をデーモン化する設定はなぜ廃止されたのか
Puma の GitHub リポジトリの History.md によると、
5.0.0 / 2020-09-17
:
- Daemonization has been removed without replacement. (#2170)
ということで、デーモン化の設定はバージョン 5.0.0 で削除されています。
その理由などは、#2170 のプルリクエストに対応する issue (#1983) で議論されています。
この議論の中では、デーモン化機能の削除の理由として Don’t Daemonize your Daemons! | Mike Perham という記事がリンクされています。
この記事から重要な点をいくつか引用させていただくと、
Your application code should not be dealing with PID files, log redirection or other low-level concerns.
Let your operating system handle daemons, respawning and logging while you focus on your application features and users.
とのことで、要するに、「PID ファイルの管理やログのリダイレクトといった低レベルの関心事はアプリケーションで扱うべきではなく、デーモン化やリスポーン・ロギングといった機能は OS に任せて、アプリケーションの開発者は機能やユーザに注視するべき」ということです。
この記事に書かれている通り、自前のコードでデーモン化を実現しようとすると、ログローテーションなど多くの考慮事項ができてしまいます。
systemd などの提供する方法でデーモン化すれば、そういった考慮事項が減るので、systemd などを使ってデーモン化するべきということです。
systemd を使うことで、例えばログは単に標準出力に出せばよくなり、The Twelve-Factor App にもある移植性の高いアプリケーションの設計プラクティスを満たすこともできます。
なお、Puma を systemd で管理する際の設定については、docs/systemd.md に書かれています。
代替手段の puma-daemon によるデーモン化はどのように実現されているのか
さて、このような理由で Puma のデーモン化設定は廃止されたわけですが、代替手段として puma-daemon という gem が作られました。
この gem を使ってデーモン化する方法が systemd でのデーモン化とどう違うのか、コードを追いかけてみます。
※ CRuby、glibc とコードを見ていきます。この辺りのコードは読み慣れていないので、もし間違いなどあれば Twitter などでご指摘いただけますと幸いです
puma-daemon の該当箇所
puma-daemon のコードをなんとなく探ってみると、どうやら ここ で
Process.daemon(true)
と、Ruby の組み込みライブラリの関数 Process.daemon
が呼ばれており、この処理でデーモン化しているようです。
Ruby の Process.daemon の該当箇所
もう少し詳しく知りたいので、Ruby (CRuby) の実装を追いかけてみようと思います。
GitHub の ミラーリポジトリ を見てみると…
err = daemon(nochdir, noclose);
のように、daemon
という関数が呼ばれています。
※ daemon
という関数が呼ばれるのは #ifdef HAVE_DAEMON
が満たされた場合のみです。そうでない場合については別の方法でデーモン化されているようです
glibc の該当箇所
Man page of DAEMON によると、daemon
は、C 言語の標準ライブラリの関数のようです。
そこで標準ライブラリの代表的な 1 つである glibc のコードを見てみようと思います。
GitHub の ミラーリポジトリ を見てみると、daemon.c に以下のように実装されているようです。
int
daemon (int nochdir, int noclose)
{
int fd;
switch (__fork()) {
case -1:
return (-1);
case 0:
break;
default:
_exit(0);
}
if (__setsid() == -1)
return (-1);
if (!nochdir)
(void)__chdir("/");
if (!noclose) {
:
:
:
}
return (0);
}
このコードの一番重要な処理は以下の箇所です。
switch (__fork()) {
case -1:
return (-1);
case 0:
break;
default:
_exit(0);
}
glibc のコードの読み方に確信が持てないですが、おそらく __fork()
は fork というシステムコール を読んでいるのではないかと思います。
fork システムコールはその戻り値によって親プロセスと子プロセスで処理を分岐することが多いのですが、上記の switch 文はまさにその典型です。
子プロセスは case 0
に進み、以後の処理を継続し、アプリケーションサーバとして動きます。
一方、親プロセスは戻り値として子プロセスの PID を受け取るため、default
に進み、exit します。
『詳解 Linuxカーネル 第3版』p32 によると、
プロセスが終了する際、カーネルは終了するプロセスの子プロセスすべてに関して、init の子になるように適切なプロセスディスクリプタのポインタを変更します。
とのことで、親プロセスがいなくなったことで子プロセス (アプリケーションサーバのプロセス) は init の子プロセスになり、デーモン化が完了したわけです。
※ Linux 以外でも同様の挙動になるかは調べていません
結局 puma-daemon でのデーモン化と systemd でのデーモン化はどう違うのか
話がだいぶ込み入ってしまいましたが、ここまで読んできたコードからすると、
- puma-daemon によるデーモン化は、ただ init プロセスの子プロセスとして Puma が動き続けるようにするだけ
- したがって、systemd によるデーモン化のようにリスポーンやロギングのような機能は提供されない
ということです。
結局どうするのがオススメか
最後に、では結局どうするのがオススメかをまとめようと思います。
そもそもデーモン化を気にする必要があるか
そもそも論として、最近のクラウド環境を踏まえると、サーバに直接 Puma をデプロイすることはオススメしません。
Heroku などの PaaS や、Fargate などのコンテナ環境を使うのが第一候補になります。
これらのプラットフォームを使う場合は、デーモン化について気にする必要はありません。
サーバにデプロイする場合
デーモン化を気にする必要があるのは、PaaS やコンテナを使わず、サーバに直接デプロイする場合です。
その場合、この記事の内容を踏まえると、systemd などを使ってデーモン化する方が望ましいと言えます。
puma-daemon によるデーモン化は、あくまで簡易的な代替手段と考えるのが良さそうです。