なぜsystemdなのか?

このブログ記事は2014年5月21日に行った私の講演の内容に基づいています。

ここ数年、GNU/LinuxのディストリビューションはSysV initを避ける傾向にあり、代わりに多種多様な新しいinitシステムへと移行が進んでいます。SysV initに満足しているユーザにとっては、これは予想外の流れでしょう。問題なく使えるのに、なぜ多くのディストリビューションはSysV initに背を向けているのでしょうか。

この記事ではSysV initの問題点と、それに対してsystemdがどんな解決法を提供しているのか説明してみようと思います。

私は特にsystemdの大ファンだというわけではなく、ただ広く使われているツールだという認識以上の思い入れは無いことだけお断りしておきます。

initシステムの役割とは何か?

コンピュータが起動する時には、ビルトインされたファームウェア(コンピュータの場合はBIOSまたはUEFI)がまず制御を司り、システムを初期化して記憶装置(通常はハードディスクドライブ)からブートローダを起動します。ブートローダは記憶装置からカーネルをロードします。そしてカーネルはドライバをロードし、ハードウェアアクセスを初期化し、非常に特殊なプログラム、つまりinitプロセスを起動します。カーネルが実行させる最初のプロセスなので、プロセスID (PID)は1が付与されます。

さてそこから、まるで魔法のようにネットワークが立ち上がり、データベースが起動し、Webサーバーがリクエストに対応し始めます。

この魔法を起こしているのがinitシステムです。これは明確に定義されたタスクではなく、解釈の違いによってかなり広義にとらえられます。

とはいえ少なくともinitは、起動、停止と再起動のサービスに関わっています。最近のシステムにおいては、一連の手順は時系列ではなく並行して行われるはずです。それから、特化した環境で走らせる必要があるかもしれません。例えばroot以外のユーザとして、特定のユーザ制限(ulimit)を用い、特別なCPUアフィニティで、chroot環境の中あるいはcgroupとして実行する、などです。

それからinitシステムはサービスが実行状態かどうかステータス確認できるようにします。理想の姿としては、管理者に手動でサービスを確認させるだけでなく、サービスそのものが監視して、障害が起きている場合には再起動させるというようなこともします。

最後に、これは忘れられがちな点ですが、カーネルは特定のイベントをユーザランドに送り、それに応答させることができます。例えば、コンピュータ上ではLinuxがctrl-alt-delのキーコンビネーションを受け取り、適切なシグナルを送り、それに対してinitが例えばシステムをシャットダウンするなどして反応します。

なぜSysV initでは駄目なのか?

SysV initという用語は、全体のプロセスを指すと同時にバイナリ/sbin/initのことも指しています。実際、多くのディスカッションで、initプログラムそのものについては忘れ去られていることが多いようです。SysV initは多くのことが出来るのにもかかわらず非常に限られた役割だけが与えられているのです。

SysV initがカーネルから制御を引き継ぐと、init(8)プロセスが/etc/inittabを読み込み、そのコンフィギュレーションに従います。initプロセスはそれぞれのランレベルを把握し、utmp/wtmpアカウントを処理し、プロセスを起動し、終了してしまった場合は再起動したりもします。プロセスを起動するには7種類の方法があり(sysinit、boot、bootwait、once、wait、respawn、ondemand)、6種類のカーネルのイベント(powerwait, powerfail, powerokwait, powerfailnow, ctrlaltdel, kbrequest)に反応します。

通常gettysやsuloginのようなプロセス、時にはmonitのようなモニタリングソフトウェアを起動するのにも用いられますが、このプログラムが行うのは設定されたランレベルで/etc/init.d/rcを起動することだけで、この後これがinitスクリプトを実行させるのです。

起動と停止

SysV initサービスを起動したり停止したりするのに用いられる一般的な方法は”service”コマンドを用いることです。多くの人はinitスクリプト/etc/init.d/$nameを直接使いますが、実はこれには問題があります。initスクリプトをカレントの環境で起動するのですが、ブート時に有効な環境とは違う可能性があるからです。影響としてコマンドラインから実行させた時はソフトウェアが正しく起動するのに、ブート時の起動では失敗してしまう、というようなことがあります。

Debianシステムでは、initスクリプトはstartparプログラムを用いて並列処理されており、initスクリプト内に特別なフォーマットで記述してあるコメントから依存情報を読み出します。

サービスステータス

ある特定のプロセスが実行状態にあるかどうか確認するため、SysV initはそのプロセスに対しsignal 0を送ります。これはUnix下の特別なシグナルで、実際にプロセスに送るわけではありません。もしそのPIDがどのプロセスにも割り振られていない時にはkill(2)システムコールが失敗するという意味を持ちます。

残念ながら、ダブルforkを含むすべてのサービスはそれ自身をデーモン化するので、SysV initはサービスのPIDを知る方法がありません。この問題を回避するために、全てのサービスはその最終PIDを、initスクリプトが読める特別なファイル、つまりPIDファイルに書き込むことによってサポートしています。

さて必要なものが揃ったので、SysV initはPIDに対してsignal 0を送ることができます。しかし、サービスが随分前に終了していた場合、カーネルがそのPIDを別のプロセスに再利用しているかどうかは分かりません。ですから、GNU/Linux上でシグナルを送るだけでなく、/proc/$pid/exeがそのプロセスにとって正しい実行ファイルであるかどうかをチェックします。

これが全てうまく行くと、このような出力結果が出ます。

 [ ok ] sshd is running.

プロセス環境

管理者が実装中の環境を変えたいとき、例えばプロセスのためにroot権限を付与したい場合などは、initスクリプトを直接書き換える必要があります。スクリプトが複雑で微妙にしか差異がない場合などは、なかなか難しい作業となります。

例えば Debian sshd initスクリプトでは、sshdプロセスは違うポイントから起動するので、これを覚えておかなければいけません。

更に言えば、ヘルパー関数とコンフィギュレーションオプションは上記のことを考慮して設定されていません。ほとんどの場合、プロセス環境の調整はinitシステムでも、サービスそのものによっても行われません。

私の問題ではない

これこそSysV initの全てと言ってもいいでしょう。initシステムが処理する問題は、別のプログラムに依存しています。

例えば、コネクション型のソケット上でプロセスを起動するにはinetdまたはxinetdを使う必要があります。もしサービスを監視し、うまくいかずに再起動したい場合は、supervisorあるいはcircusなどを用いる必要があります。

しかしserviceスクリプトは複数のプログラムから起動するサービスについて把握していません。そのため、管理者はサービスが実行されているか、していないかを見つけるために複数の場所に権限を置きます。

似たように、SysV initは、機能をinit自身に実装することはせず、それぞれ全てのサービスに対して、共通の機能を実装するように求めています。

この状況は、全てのサービスが自分自身をデーモン化する必要があるという要求から始まります。これは当たり前のタスクではありません。正しく実行するためには15の異なるステップを行う必要があります。システムコールdaemon(7)でさえも、いくつかのステップを忘れてしまうので、お勧めできません。

1024より小さいTCPポートを開く全てのサービスは、root権限で起動し、ソケットを開き、自力でユーザ権限を付与することが求められます。一般的にユーザ権限の付与とchrootの設定はサービスによって行われる必要があります。ロギングもまた悪いことにあらゆるサービスで再実装が必要なアスペクトです。

SysVが提供する限定的な機能と、それが実装された複雑に入り組んだやり方のあいだに、もっと良いやり方あるはずです。

新しいinitシステム

以下は全てSysV initの問題を修正するための新しいinitシステムです。GNU/Linuxディストリビューションでは、もはや純粋なSysV initだけを使用しているユーザはほぼいません。大きなものになると、それぞれ、systemd(Fedora、OpenSUSE、Arch Linux、Mageia、OpenMandriva、Red Hat Enterprise Linux, CentOS)、upstart (Ubuntu)あるいはOpenRC (Gentoo)と入れ替わっています。未だSysV initを使用しているユーザは並列処理とmake-style init(Debian)などを用いた他機能用を拡張して使っています。GNU/Linuxの他のinitシステムにはinitng、busybox-init、s6、eINIT、procd (OpenWrt)、BootScripts (GoboLinux)、DEMONS(KahelOS)とMudur (Pardus)があります。もしくは、最初からSysV initは使用せず、簡易なBSD-style init setups (Slackware)を使用しています。そして最近、DebianとUbuntuがsystemdに移行することを公表しました。Mintがそうしたように、いくつかの派生物を持ってくることになるでしょう(Debianに関する報告書はsystemdを読むうえで、とても面白いのでお勧めです)。

しかしこういった改善はGNU/Linuxに限ったことではありません。Solarisはずっと前に、initシステムの代替として Service Management Facility(SMF)を使用していますし、MacOS Xは自身のlaunchdを使用しています。

GNU/LinuxにとってDebian とUbuntuがsysytemdに乗り換えたという決定は、まぎれもなくsystemdが勝ったということを意味しています。使い手の好みに関わらず、systemdは、いまやGNU/Linuxのinitシステムなのです。

ここでの良い知らせは、多くの異なるディストリビューションが別々にsystemdを採用したということで、systemdが全く使いものにならないものではないと言えることでしょう。

なぜsystemdなのか?

まずsystemdはとても巨大であるということです。公式文書には正直に書いてあります。「この目次は14のセクションで1504個のエントリがあり、個別のマニュアルは164ページあります」。そして中には、systemdディストリビューションには69のバイナリがあると言っている人もいます。とても似ていますが、SysV initは全体のプロセスを指すと同時にバイナリ/sbin/initのことも指しています。systemdは全プロジェクトと同時に中心となるsystemdバイナリのことも指しています。公式文書の1つのエントリではconfigファイルの中のディレクティブと個々のコマンドライン引数に触れていますが、これだけでも、まだ膨大です。

利点は?

少なくとも文書化されているということです。

起動と停止

systemdプロセスはsystemctlを用いて起動します。

それは依存関係を知り、トラックしていて、スタートタイムとランタイムを把握しています。つまり、systemdは、あるサービスが起動する前に他のサービスが立ち上がっていることを要求するということと、あるサービスがうまく作動するためにどこかの時点で他のサービスを起動させなければならないことの違いを分かっています。必要に応じてスタートアップのサービスを強引に並列化します。PIDファイルの有無にかかわらず、正確にサービスをトラックし、予期せず終了したときに再起動させます。

コンフィギュレーションファイルはSysV initのように手続的ではなく、宣言的で比較的短くなります。ちょっと見ただけでも、私のシステム上では平均15行のユニットファイルがinitスクリプトでは平均100行になります。

参考例です。

[Unit]
Description=My awesome service
After=network.target

[Service]
ExecStart=/usr/bin/mas
Restart=on-failure

[Install]
WantedBy=default.target

以上です。サービスは初期値で起動しますが、ネットワークが立ち上がった後、予期せぬ終了があった場合、再起動します。

アクティベーション

systemdはシステムのブート時や明示されたコマンドによる場合だけでなく、その他様々なイベントによってサービスを開始する場合もサポートしています。デバイスが接続されたとき、マウントポイントが利用できるようになったとき、またはパスが作成されたり、タイマーが設定されているときにもサービスが起動できます。

中でも役に立つアクティベーションプロトコルの1つにソケット・アクティベーションがあります。これを用いて、systemdはソケットを開き、サービスを起動します。initdに似た動きをするのは当たりまえですが、ソケット・アクティベーションはもっと多くの働きをします。

systemdは、ソケットを受け取るだけでなくリスニングソケットをサービスへと渡すことができます。この場合、2つの使用事例を念頭に置いてください。1つは、systemdがrootとして1024より小さいポートを開くことができ、rootでないユーザとしてプロセスを起動できるということです。そして、リスニングソケットをプロセスへと渡し、特権ポートでlistenするサービスを書くために簡略化します。もう1つの使用事例はこうです。

PostgreSQLのようなデータベースサービスを考えてみてください。initシステムがPostgreSQLを起動すると、ブートのためにいくつかの処理が行われ、そしてポート5432を開いて、データベースユーザを待ちます。systemdがスタートアップを積極的に並列化すると、データベースを使うサービスは、PostgreSQLが起動されてから(systemdはサービスが後で起動されることを保証します)、ポートが実際に開くまでの間に起動されますが、いくつかの厄介な競合状態につながります。初期化が済んだ時にサービスがアナウンスするためのプロトコルはsystemdが提供しますが、サービスはソケットそのものを開きPostgreSQLサーバに受け渡すこともできます。そうすることでクライアントはPostgreSQLの初期化が一旦完了してしまえば、すぐにソケットに接続することができます。

これにより、並列処理と起動のスピードが向上します。

新しいスタイルのデーモン

systemdと連動するためにサービスをデーモン化する必要はありません。supervisorを使った時と同じように、プロセスは通常通り実行できます。syslogにログメッセージを記録する時でさえ、stdoutとstderrに書き込むだけでよいのです。

systemdとsupervisorのどちらを使っても、ロギングを一つの場所で設定することができることに加えて、サービスの開発がはるかに単純になります。

更に、ログ出力の競合状態も防ぎます。SysV initでは、サービスがログファイルを開くことができなかったり、何らかの理由でsyslogに接続することができなかったりした場合、管理者にその問題を通知する方法がありません。新しいスタイルのデーモンでは、こういった問題を心配しなくてもよいのです。

サービスステータス

systemdでは、プロセスのトラックにコントロール・グループを使用します。つまり、PIDファイルやプロセスからの連携が全く必要ないということです。このおかげでSysv initよりも多くの情報を格納することができます。

lbcd.service - responder for load balancing
Loaded: loaded (/lib/systemd/system/lbcd.service; enabled)
Active: active (running) since Sun 2013-12-29 13:01:24 PST; 1h 11min ago
Docs: man:lbcd(8)
http://www.eyrie.org/~eagle/software/lbcd/
Main PID: 25290 (lbcd)
CGroup: name=systemd:/system/lbcd.service
└─25290 /usr/sbin/lbcd -f -l

Dec 29 13:01:24 wanderer systemd[1]: Starting responder for load balancing…
Dec 29 13:01:24 wanderer systemd[1]: Started responder for load balancing.
Dec 29 13:01:24 wanderer lbcd[25290]: ready to accept requests
Dec 29 13:01:43 wanderer lbcd[25290]: request from ::1 (version 3)

管理者は一目で、いつからサービスが起動しているのか(「アップグレードの後、サービスを再起動した?」)、主要なリストだけでなくサービスに属するプロセスの全リスト、そしてサービスが発動した最近のログメッセージを確認することができます。正しいログファイルを探す必要も大量のsyslogを検索する必要もありません。

画期的とは言えませんが、これは非常に役に立つと思います。

プロセス環境

少なくともユニットファイルのディレクティブがあれば、プロセス環境を設定するのはとても簡単です。幸運にもユニットファイルのディレクティブは大量に存在します。ユーザやグループの変更はディレクティブの1つです。niceレベル、CPUの時間の割り当てやアフィニティ、ユーザ制限、そしてカーネルケイパビリティも同様です。個々のデバイスやネットワークにアクセスするプロセスは簡単に拒否することができます。

最後に、コンテナ内でのサービスの開始をネイティブにサポートするのがsystemdです。

とても素晴らしいですよね。

未来はコンテナと共に

コンテナは仮想環境で、steroids上のchrootと似ています。しかし仮想マシンではありません。カーネルのコントロールグループや名前空間を使ってプロセスを分離しますが、実際にハードウェアをエミュレートすることはなく、仮想マシンの分離をベアメタルのフルスピードで行います。これからのGNU/Linuxサーバは、これに似た型になるでしょう。

1000px-Linux_kernel_unified_hierarchy_cgroups_and_systemd.svg


画像:Shuuel Csaba Otto Traian提供

ホストシステムは、カーネルやsystemd initスタックを実行する以外は何もしません。systemdは分離されていて独立したコンテナ内でサービスを起動します。

これにより、分離されたサービス環境が可能になり、サービスのセットアップやコンフィギュレーションが容易になり、セットアップを少しばかりセキュアにします。

まとめ

まとめると、このブログ記事から言えることは、SysV initは時代遅れだということです。SysV initには、最新のinitシステムが提供する機能がほんのすこしだけ備わっていたり、最善ではない形でそれらの機能が実装されています。ですから、SysV initが別のものに代替されてしまうのかという可能性の問題ではなく、いつ代替されるのかの問題なのです。

良いか悪いかは別として、SysV initとsystemdにおける標準プログラムを代替するかどうかの勝敗は、systemdに軍配が上がりました。

systemdにも問題はありますが(このブログ記事では取り上げていませんが、多数の問題が存在します)、システム管理者やソフトウェア開発者が使いたいと願う素晴らしい機能がたくさんあります。