私がsystemdを嫌う理由

(訳注:7/24、いただいた翻訳フィードバックを元に記事を修正いたしました。)
(訳注2:8/4、いただいた翻訳フィードバックを元に記事を再修正いたしました。)

この2010年代にLinuxシステムの管理者をしていれば、systemdに関して何かしら思うところがあるでしょう。そして私は管理者たちの意見が両極端に分かれていることに驚きました。ほとんどの人(少なくとも意見を表明している人達)はsystemdが「大好き」か「大嫌い」かのどちらかのようです。私の場合、systemdをきっかけに昨年OpenBSDを使うことになったのですが、これを話したことで私がsystemdを「大嫌い」だと思われたようです。でも、それは違います。

本当は、systemd自体は私がOpenBSDに移った理由のほんの一部にすぎません。しかし、この経験によって2つの重要な点に気付きました。まず、最近のLinuxの設計の問題はソフトウェアのどの部分よりも根深いということです。私はこれを自分で経験して初めて気付きました。2番目に、これはDebianに限った話ですが、”ユニバーサルオペレーティングシステム”という考え方そのものに不備があるということです。他の全てを除いて選ばれた、それぞれが最適に動作する2つの選択肢に遭遇した場合、その両方の選択肢には一度に対応できません。

さて、私の話はこれくらいにして、systemdの話に戻りましょう。

最初にはっきりしておきたいのは、systemdは悪魔の仕業ではないということです。おそらく、NSAのバックドアを含んでいないでしょうし、大規模なLinuxディストリビューションがデフォルトで出荷しているソフトウェアの中で最悪なわけではありません。Linuxの一部の重要な問題には対処しますし、そのアプローチには利点があります。

半端な真実は嘘よりも悪いとよく言われます。systemdの場合、半端な改善は完全な欠陥よりもたちが悪いのです。ユーザは自分が使いたい機能に注目し、そうでないものは無視するものです。しかし、その無視した機能から後でしっぺ返しを食らいます。

でも、systemdの一番大きな問題はこれではありません。System Vの過ちを何度も何度も繰り返していることです。時代が21世紀に変わり、それに合わせて見た目を変えているにすぎません。

簡単な歴史

いかにしてこうなったのかを理解するには、1960年代半ばまで(50代以下の方は想像の世界で)記憶をさかのぼる必要があります。タイムシェアリングオペレーティングシステムは新しい革新的な概念でした。多くの人々はまだバッチ処理を使って作業するものだと考えていました。そんな中、タイムシェアリングのための様々なアプローチの1つとして考え出されたのがMulticsです。

MulticsはすべてのUNIXとUNIXライクなシステムの概念上の祖先です。Multicsがうまくいかなかったのは、コアとなる設計原理ではなく、設計とそのパーツの実装が原因でした。これについては1979年にDennis Ritchieによって最初に発表された論文で、より具体的に説明されています。

IOリダイレクトの表記として非常に便利な’>’と’<'はPDP-7 Unixシステムの最初からは存在しませんでしたが、それでも非常に早い段階で採用されました。他のUnixと同様、これはMulticsの考え方からインスパイアされています。Multicsにはより一般的なIOリダイレクトのメカニズムが含まれており、さまざまなデバイスやファイル、さらには特殊なストリーム処理のモジュールとの間でも動的にリダイレクトできるIOストリームが形になっています。なじみのある10年前のバージョンのMulticsでさえ、通常はターミナル向けの後続の出力をファイルに切り替えるコマンドと、出力をターミナルに再接続する別のコマンドが存在していました。Unixで次のように表記するとしましょう。

ls >xx

Multicsでxxにファイルの名前を一覧表示する場合は、次のように表記します。

iocall attach user_output file xx
list
iocall attach user_output syn user_i/o

このぎこちないシーケンスはMultics全盛の時代にはよく使われており、恐らくMulticsのシェルに統合するのは容易だったろうと思いますが、そういった発想を当時は誰も持っていませんでした。その理由として考えられるのが、Multicsのプロジェクトの規模の大きさです。I/Oシステムの実装者がいたのはマレーヒルのベル研究所でしたが、シェルの開発はMITで行われていました。私たちはシェルの変更を検討はしませんでしたし(それは彼らのプログラムでした)、同じようにシェルの管理者たちも、ぎこちないながらも使いやすいiocallのことは知りもしなかったでしょう(1969年のMulticsのマニュアルには、iocallが作者による管理、つまり非標準的なコマンドとしてリストされています)。反対にUnixのI/OシステムとそのシェルはどちらもThompsonの管理下にあるため、適切なアイデアが浮上してくれば、実装までにさほどの時間を要しないというわけです。

ここにあるのは、基本的にはトレードオフの問題です。トレードオフに関するRitchieの思惑は別として、Multicsは可能な限り一般的なインターフェースを提供することで、全ての人に対して万能であろうとしました。その結果、開発は非効率的にならざるを得ず、使用にも面倒さがつきまとうことになります。1%の少数派への使用効率を考えるあまり、99%の大多数の仕事を困難にしてしまったのです。そしてこの設計のパターンが、systemdでも繰り返されていると、私は思います。

私の指摘で重要な点は、iocallのようなインターフェースは明らかに間違っているということです。私たちは優れたインターフェースがどういうものか経験的に知っていますからね。Multicsの設計者にとって、Multicsは実際にある個別の問題を解決するアプローチとして完全に合理的だったのだろうと思います。しかし合理的と思うことと、それがいいソリューションであるかというのは、また別問題です。

UNIXの商品化

話を1980年代に進めましょう。AT&TがUNIXの商品化を画策するようになります。ソースコードとバイナリの配布を停止し、商品として売れると思われる機能の実装を始めました。そして、これらの努力の結晶が、Linuxもそのメンバーである(系譜の直下というよりも、そのクローンとしてですが)オペレーティングシステムのSystem Vファミリーです。この投稿では差異はさほど関係ないため、System VファミリーのメンバーとしてSystem IIIを参照することにします。

System VがResearch UNIXから受け継いだものの1つがinitです。initは固定されたプロセスモニタリングを提供し、増加し続けるシェルスクリプト(/etc/rc)のサービスを呼び出して起動する役目を持ちます。開発者たちは、initの問題に対処する最良の方法は新しい階層型言語を作り出すことだと考えました。つまり、より複雑でカスタマイズ可能なタスクをinitに実行させると共に、売り手による独自サービスのスタートアップルーティンを組み込む方法を追加して、それを起動時に自動で実行させるような言語が必要だと思ったのです。(注:情報に誤りがありました。これはどうやらSunOSから来たようです。こちらに、私よりも経験豊富な方の説明があるのでご覧ください。)

何だか聞き覚えがありませんか。そのはずです。BSDシステムが既存のコンポーネントに少しずつ改善を加えながら現代的な問題にうまく対応している一方で、System Vのinitがその変遷で大コケした事実は、教訓とするべきでしょう。

systemdの紹介

歴史を振り返るのはこの辺で終わりです。もっと私の言わんとしているところを聞きたいですよね。そろそろ始めるとします。

複雑さのたらい回し

systemdについてよく言われるのは、そのユニットファイルがinitのスクリプトよりも”シンプル”だということでしょう。表面的には、これは当を得ています。ただ、実際のところを言うと、複雑さがロジックから言語へと移行しただけです。

Bourne Shellとは対照的に、systemdのユニット指令の多くは特化されており、簡単には再利用できないブロックです。ただし、これ自体は必ずしも悪いことではありませんーsystemdのユニットが実際のプログラムではなく、必要に応じてより基本的なプリミティブに立ち戻る方法がない、ということを除けばですが。必然的に、時間の経過に伴って使用事例が発見されるたびに、指令が追加されることになります。

信じられませんか? 2025年になって、ユニット指令の数が現在の最低3倍を超えていないようなら(それと、担当者が自分の不在時にも対応できるようにするためにExecStartからラッパースクリプトを呼び出していないようなら)、1杯おごりますよ。なお、少なくともDebianパッケージの1つ(memcached)は、既にExecStartからラッパースクリプトを呼び出しています。

シンプルな言語で複雑なロジックを展開した場合は、実装を常にシンプルにすることができます。反対に複雑な言語でシンプルなロジックを展開した場合、他の誰かがそれを使い始めたとたんに、言語の複雑さにがんじがらめになるでしょう。

ユーザへの過保護

この不満は決してsystemdに固有のものではなく、多くの、特にLinuxのプログラムで増加傾向にあります。なぜ、systemctlのeditが問題だというのでしょうか。

あなたは単なるユーザなのだから、コンピュータを壊すことなしに設定ファイルのエディタを呼び出すジョブを管理することなんてできないはずです。そのためにラッパーがあるのですから。え、直接ファイルを編集してしまいましたか? 私からの助言ですが、systemctlのdaemon-reloadは実行すべきです。そうしないと、あなたが要求したアクションは古い設定のまま実行されてしまいます。

次いで、なぜsystemctlのdaemon-reloadが問題なのでしょうか。いつからファイルシステムは、真実の要素の下に位置する二流市民へと格下げされただけでなく、変更が加えられたと分かっている時にもファイルをリロードしないような無様なものとなったのでしょうか?

私が問題を必要以上に単純化しているということは十分に承知しています。より根深い設計上の問題があって、そのせいでsystemctl editの処理がエディタでユニットファイルを開くだけではすまなくなっています。しかしここでの私のポイントは、ユーザはそんなこと気にしないということです。ユーザが目にするのはファイルの編集以外に何ら役割を持たない過度に複雑なCLIでしかありません。そして、この点は次の話題にもつながります。

Multicsの相互運用性へのアプローチ

MulticsとUNIXの基本的な違いの1つは、プログラムの相互作用の方法です。Multicsのプログラムは個別に開発されています。それぞれが実行可能なソリューションで、必要に応じて相互に補完も可能ですが、効率的とは言えません。それに対し、UNIXの基本的な設計原理は”共同作業のためのプログラミング”というもので、Multicsの原理とは正反対と言えます。

systemdはD-Busに大きく依存しており、そうすることで他のアプリケーションからも制御できるようになっています。このように書くと、systemdは”共同作業”的に見えるかもしれませんが、どちらかと言えばUNIXのシェルのリダイレクトよりもiocallに近いというのが私の意見です。他のプログラムが使える機能を提供してはいますが、ワークフローは統合していません。

前に挙げた例が、このことをうまく表します。ユニットファイルのエディタを直接使っても作業は可能ですが、最適ではない上に、終わった後にはdaemon-reloadを実行しなくてはなりません。つまり、ポイントは動くかどうかではなく、systemd独自のインターフェース以外でユニットファイルを編集するのは非効率的であり非直感的でもあるということです。私はこれを”共同作業”とは呼びません。

重要なことなのでもう一度言いますね。Multicsの設計者たちにとって、Multicsは実際に存在したそれぞれの問題を解決するアプローチとして、完全に妥当なものだったでしょう。しかし、そのことと優れたソリューションであるということは、また別の話なのです。

予測不可能

systemdを実行しているコンピュータにログインします。そして、「どのユニットが次の起動で実行されるか」を考えてみましょう。「どのユニットが今回の起動で実行されたか」ではありませんよ。ユニット間の複雑な依存関係のせいで、この質問に答えるのはかなり難しいと思います。もしあなたがセキュリティを重要視するシステム管理者であれば、これはまさに一大事です。

依存性駆動サービスの初期化の失敗は、それが実際よりも理論上ではるかにうまく動作することを表しています。理論上はノード間の有向グラフがあり、default.targetが依存するそれらのグラフをスタートさせるだけです。しかし実際のシステム管理では、思い描く通りにはいきません。

私がシステム管理者なら、再起動した時に「次の再起動後にfooが実行されるどうか」を知りたいですし、ネットワーク内に入った時にシステムがどのような状態になるかを合理的な確信を持って知りたいと思います。sysvinitが、systemdよりもはるかに優れている点はこの点でしょう。

実行したくないユニットをマスクすればこれら全てを回避することはできますが、ポイントとはそこではありません。何度も言いますが、ポイントはそれが効率的ではないということです。ツールは作業を手助けしてくれるものであって、その邪魔をするものではありません。

ゆがめられたプロパティ

systemdの”特徴”の1つとして、システムを起動する際、シェルが全く必要ないということが挙げられます。これは、全く意味がない操作のように思えますし、sysvのinitのスクリプト内でシェルが過大化してしまったことに気づいて、脊髄反射的に反応してしまったとしか思えません。

実行中のD-Busサービス(またはkdbus)、ユニットファイルを包含するファイルシステム、参照される全てのバイナリ、リンクする全てのライブラリ、参照できる全ての設定ファイルが存在し、しかも最も普遍的なUNIXバイナリである/bin/shを欠いた環境などというものを想定する合理性が一体どこにあるというのでしょう?

この目的で引用されることの多い使用事例は、コンテナの内側でのサービス管理です。少数派の人々が特殊な仕事で使う機能のために、デスクトップ上のinitを複雑で限定的にする必要はないと思います。必ず、シェルに依存しないコンテナが起動するようにツールを記述するべきで、力ずくで何でもありという状態のinitにしてはいけません。

特に煩わしいのは、systemdがシェルのような機能を落としたパーサを持ち、”EnvironmentFile”も扱っていることです(通常、シェルをソースとした場合、実際に環境変数を設定することはありませんが、systemdのパーサは変数を処理します)。また、サービスユニットには引数リストの疑似シェルシンタックスもあります。これには、他のどんなソフトウェアを使っているユーザにとっても期待外れとなりそうな、変わった特徴があります。それは、$FOOが複数の引数に分けられるのに対し、${FOO}は分けることができないということです。

複雑さが正統化されている現実世界で、バイナリの実行を避けるのは本当に便利なのでしょうか。

まとめ

冒頭で述べたことを、もう一度、言わせてください。systemdにはメリットもありますが、Linuxと同じように、その思想に問題が見られます。歴史を無視し、現代の技術の方が優秀だと決めてかかっているのです。過去の失敗を知り、そこから学ぼうという姿勢がありません。

何か1つがソフトウェアの質を下げるというわけではありませんが、全体的に見ると問題を解決するよりも、問題になることの方が多くなるはずです。残念なことに、この問題の多くは、明らかになるまでに何年もかかり、それまでに主要な全てのLinuxディストリビューションが、完全にそれを統合してしまいます。この問題の解決は2040年代に生きる不運な人々に委ねられるでしょう。その時代の人々がこの問題から学び、悪循環を断ち切ってくれることを願います。

本投稿ではsystemdに焦点を当てましたが、これはLinux とSystem Vファミリーの間で一般的に蔓延している傾向の一例に過ぎません。既存のツールの欠陥を明らかにし、修正したものを記述して進歩とラベリングするのは簡単です。しかし、既存のツールの強みを明らかにし、更に機能が向上するよう調整するには格段に高い技術が必要になります。LinuxがBSDから多くを学ぶことがあるとすれば、この点です。

この記事の内容に異論がある場合は、Twitterなどで批判して頂いて結構ですが、私からの返事は期待しないでください。私はOpenBSDに移行してしまったため、systemdが役に立つかどうかはどうでもいいのです。私は経験したことが学ぶべき大切な教訓だと思ったので文章にしているだけです。私にとっては忘れたくない経験ですし、他の方にとっても訳に立てばと思います。