Nitro: 小型で柔軟な init システムおよびプロセススーパーバイザー
(git.vuxu.org)- Nitro は、組み込み、サーバー、デスクトップ、コンテナのすべてに適用できる超小型プロセススーパーバイザー兼 init システム
- システム状態を RAM にのみ保存するため、読み取り専用ファイルシステムでも問題なく動作し、高速で効率的なイベント駆動設計を提供
- 構成方式はシンプルなスクリプトディレクトリ構造で、複雑な設定ファイルや追加のビルド工程なしにサービス管理が可能
- パラメータ化サービス、堅牢な再起動、個別サービスごとの信頼性の高いロギング機能など、コンテナや組み込み環境に最適化された機能をサポート
- nitroctl ツールによるリモート制御、シグナルベースの動作制御など、高い柔軟性と制御性を保証
概要
Nitro は、Linux で pid 1 としても使用できる超小型プロセススーパーバイザー
主な適用分野は以下の通り
- 組み込み、デスクトップ、サーバーなど多様な用途の Linux マシン向け init
- Linux initramfs の init
- Docker/Podman/LXC/Kubernetes などコンテナ環境の init
- POSIX システムで権限なしに動作するスーパービジョンデーモン
構成はディレクトリベースのスクリプト構造を使用し、デフォルトの場所は /etc/nitro
要件
- カーネルの Unix ソケット サポートが必要
tmpfsまたは書き込み可能な/runディレクトリが必要
他システムに対する利点
- すべての状態情報は RAM にのみ保持されるため、読み取り専用ルートファイルシステムでも特別な工夫なしで動作
- イベント駆動でポーリングなしの動作方式により効率性を提供
- ランタイム中にメモリの動的確保がない
- ファイルディスクリプタが無制限に消費されない
- 1 つの**self-contained バイナリ(オプションで制御バイナリを追加)**だけでよい
- 設定ファイルの変換やコンパイルは不要で、サービスはスクリプトが入ったシンプルなディレクトリ
- サービス再起動とロギングチェーンをサポート
- システムクロックが正確でなくても正常動作
- FreeBSD では
/etc/ttysを通じて実行可能 - musl libc 使用時は超小型の static バイナリを作成可能
サービス管理
-
各サービスディレクトリ(デフォルトでは
/etc/nitro配下)は以下のファイルを含められるsetup: サービス開始前に実行される(任意の)スクリプト。正常終了(0)の場合にのみサービス開始可能run: サービス本体のスクリプト。終了しない限りサービスが生存していると見なされる。未実装の場合は one-shot サービスとして扱われるfinish:run終了後に実行される(任意の)スクリプト。終了状態とシグナル値を引数として受け取るlog: 別のサービスディレクトリを指すシンボリックリンク。run の出力をそのサービスの入力へパイプ接続する(ロギングチェーンに活用可能)down: このファイルが存在すると、nitro はデフォルトでこのサービスを起動しない- ディレクトリ名が
@で終わる場合は無視され、パラメータサービスとして活用可能 - サービス名は 64 文字未満で、
/、,、改行文字を含めることはできない
-
runit の
chpstユーティリティはrunスクリプト作成時に役立つ
特殊サービス
LOG: log リンクを持たないすべてのサービスのログ記録用デフォルトサービスSYS:SYS/setupはすべてのサービス起動前に実行され、順序付きサービス起動を実装可能SYS/finish: 全体の終了段階に入る前に実行SYS/final: すべてのプロセス終了後に実行SYS/fatal: 致命的エラー発生時に終了の代わりに実行(存在する場合)SYS/reincarnate: shutdown の代わりに実行され、たとえば initramfs の再実装などに活用可能
パラメータ化サービス
@で終わるサービスディレクトリは nitro からは無視されるが、シンボリックリンクまたはnitroctlコマンドで直接指定可能@の後ろのパラメータは各スクリプトに第 1 引数として渡される- 例:
agetty@/runとagetty@tty1のシンボリックリンクがある場合、agetty@/run tty1を実行 nitroctl up agetty@tty2を入力するとagetty@/run tty2を実行可能(ディレクトリの存在有無は不問)
- 例:
動作モード
- ライフサイクル全体は起動、サービス実行(スーパービジョン)、終了の 3 段階で構成
- 起動: 特殊サービス
SYSが存在する場合はsetupから実行し、その後すべての non-down サービスを実行 - サービスが終了すると再起動し、直近の再起動が速すぎる場合は 2 秒待機
nitroctl RebootまたはShutdownで終了シグナルを送信可能- この場合
SYS/finish→ 全サービスへ SIGTERM(最大 7 秒待機)→ SIGKILL →SYS/final→ 終了シーケンス
- この場合
- コンテナや権限のないスーパーバイザー用途ではプロセスのみ終了
- 起動: 特殊サービス
nitroctl による制御
- nitroctl CLI ツールで離れた場所から nitro を制御可能
コマンド例:
- list: サービス一覧、状態、PID、uptime、最後の終了状態を表示
- up/down/start/stop/restart: サービスの起動・停止・再起動などを制御
- シグナル送信: p(SIGSTOP), c(SIGCONT), h(SIGHUP), a(SIGALRM), i(SIGINT), q(SIGQUIT), 1(SIGUSR1), 2(SIGUSR2), t(SIGTERM), k(SIGKILL)
- pidof: 指定サービスの PID を表示
- rescan: サービスディレクトリを再読み込みし、追加・削除されたサービスを反映
- Shutdown/Reboot: システム全体の停止・再起動
シグナルによる制御
- nitro プロセスへシグナルを直接送って制御可能
- SIGHUP: サービス再スキャン(rescan)
- SIGINT: 再起動
- SIGTERM: 終了(nitro が pid 1 でない場合)
Linux で init として使う nitro
- Nitro は自己完結型バイナリとして Linux の pid 1 に直接ブート可能
/dev、/runを必要に応じてマウントし、その他の処理はSYS/setupで行う- Ctrl-Alt-Del イベントで秩序ある再起動をトリガー
Docker コンテナで init として Nitro を使う
- Nitro は静的にビルドでき、コンテナへ簡単に組み込める
- デフォルトのソケットパスを使うにはコンテナ内に
/runが存在する必要がある - 制御ソケットをバインドマウントすれば、外部から nitroctl でリモート制御可能
FreeBSD での Nitro
/etc/ttysに次の行を追加すると、FreeBSD init から nitro をスーパーバイズ可能/etc/nitro "/usr/local/sbin/nitro" "" on
著者
- Leah Neukirchen leah@vuxu.org
謝辞
- daemontools, freedt, runit, perp, s6 など既存のプロセススーパービジョンシステムを詳細に分析した上で開発
ライセンス
- 0BSD ライセンス(詳細は LICENSE ファイルを参照)
1件のコメント
Hacker Newsのコメント
runitとの比較を見てみたい。runitは極限までミニマルでありながら、ほぼ完全な init システムだ。コントロールディレクトリ、宣言的ではない依存関係、似たようなスクリプト構成、ロギングのアプローチなど、共通点が多い。説明ページでも runit に少し触れられており、chpst ユーティリティを併用することが推奨されている。違いとしては、1つのサービスディレクトリで複数の類似プロセス(例: agetty)をパラメータ化して管理できる構造が良いと思う。reboot や shutdown を単一バイナリ(
nitroctl)から直接実行できる一方、runit は複数バイナリ構成だ昨年、runit でプロセスを管理していた最後のサーバー群を引退させたときは、とても名残惜しかった。約15年前に初めて自分で runit サービスを書いたとき、これこそが Linux におけるサービス管理の標準的なやり方だと信じていた。その後5年間 Linux から離れて戻ってくると、systemd がデフォルトになっていた。悪評は何度も聞いていたが、次第にそれには歪んだ反感も多いのだと分かった。今は爬虫類の vivarium で Pi Zero を使ってカメラと温度データのストリーミングサービスを動かしているが、systemd でのセットアップは非常に簡単だった。OpenSuse のデスクトップや業務用ノート PC でも、systemd でさまざまなサービスを手軽に運用できた。「標準があるのはむしろ良いことだ」と思う
runit と nitro の適切なミニマル比較は、2024年に公開された Leah Neukirchen の発表スライド(PDF)にある
https://leahneukirchen.org/talks/#nitroyetanotherinitsy
Leah Neukirchen は Void Linux コミュニティで活発に活動している人だ。このプロジェクトは Void と密接に関わることになりそうだ。もう少し公式に、Void で nitro を使う方法についての記事を書いてほしい
「宣言的な依存関係がない」ことが利点なのか気になる。systemd を init として批判する意見はよく聞くが、宣言的設計そのものを批判する例はあまり見ない。何か理由があるのなら詳しく聞いてみたい
Void Linux で runit を知って以来、init システムとして便利に使っているが、UI とドキュメント不足は惜しい点だった。特にロギング設定は本当に難しかった。同じようにシンプルでありつつ、より妥当なデフォルト、より直感的な UI、より良いドキュメントを備えた代替を試してみたい
コンテナで init システムを動かそうという話を見るたびに、いつも考え込んでしまう。実際に必要性があってそう設計する場合もあるが、むしろ過剰に複雑にしてしまっていることも多く見てきた(特に Kubernetes やクラウド環境では、本来もっときちんと分離設計すべきだった)。「どうせみんなこう使っているから」が現状なのかもしれないが、わざわざ「もっと良く作る」と言って問題を広げるのが良いのか、それとも既存の解決策で派手に失敗させておく方が良いのか、いつも判断が難しい
アプリケーションコンテナは、Unix 哲学の「1つのことをうまくやる」に従うべきだと思う。ただし、コンテナ内で何らかの理由で fork するなら、PID 1 には本物の init がいるべきだ
ロボティクス分野での経験では、多くのコンテナはもともとベアメタルで動いていた複雑なシステムをそのままコンテナに移したものだ。プロセス間に非構造的な RPC が多く、複数の独立したコンテナに細かく分割する利点があまりない。モノリシックなアプリコンテナの中で複数プロセスを動かすとなると、supervisor、runit、systemd、tmux まで、さまざまな選択肢が使われている
Fly.io、Render、Google Cloud Run のような、コンテナ単位で課金するホスティングを使ったことがある。料金の都合で、1つのコンテナ内に複数プロセスを動かさざるを得ないことが時々ある
NixOS の新機能 modular-services が Nixpkgs に取り込まれた。新しい init システムや新カーネルへの NixOS の移植がずっと容易になるはずで、今は nitro のような実験を試すのに良い時期だと思う
Chimera Linux で使われている dinit と nitro を比較してみたい。readme をざっと見た限りでは、サービス依存関係の管理はまだできないように見える
dinit: https://github.com/davmac314/dinit
Nitro は宣言的にサービス依存関係を扱わない。そのため、コマンド1つでサービス間の依存グラフをきれいに表示することはできない。ただ、setup スクリプトに必要なサービスを書いておけば、そのサービスが起動しているかを確認し、自動で待機してリトライしてくれる。依存グラフを見たければ、grep のようなものを使って自分でスクリプトを書くしかない。一方で、サービスが落ちたときに依存先のサービスも連鎖的に正しく停止させるのは忘れやすく、nitro 自体にはそれを検出する便利な方法はない
Artix Linux で dinit を使ったことがあるが、本当に軽量で印象的だった
Artix FAQ: https://artixlinux.org/faq.php
こういう低レベルのプロジェクトを見ると本当にわくわくする。systemd が従来の SysV・POSIX の枠を超えて Linux カーネル特有の機能をうまく活用したのは良かった。でもそれで終わってほしくはなく、これからも新しいアイデアや革新が出てきてほしい。最近、自分は製造自動化向けに、UEFI ファームウェアから直接 netboot される Linux カーネルに、Go で自作した単一の init バイナリだけを組み込んだ構成を実装した。自分で持ち込んだコードと高水準言語だけで OS 環境全体を制御できるので、各種サブプロセスや大量のテキスト設定ファイルを管理しなくてよいのが本当に自由だった
約13年前に C で自前の init システムを構築したことがある。想定よりはるかに手間がかかり、低性能ハードウェアで GUI とバックエンドを素早く起動するために使っていた。面白いプログラミング練習ではあったが、後になって、似たような解決策はすでに存在していたかもしれないとも思った。同僚が同じ会社でもう1つ別の init を作ったせいで、自分の最初の版はほぼ libc 以外に依存がない軽量なものだった一方、同僚の版は libevent ベースで、より高度な機能が多かった
AWS Nitro と名前も機能も重なっている点が気になる
https://docs.aws.amazon.com/whitepapers/latest/security-design-of-aws-nitro-system/the-nitro-system-journey.html
名前が同じなだけで、init システムとハイパーバイザーは根本的にまったく別物だ
問題になることはほとんどないと思う。片方は誰でも使える init システムで、AWS Nitro は企業内部でしか使われない KVM のフォークだ
s6 と比べて nitro がどうなのか気になる。最近 Docker コンテナで s6 を使って init システムを組んでみたが、s6-overlay では大量のファイルを自分で作る必要があり、思っていたほど直感的ではなかった
tini も見てみる価値がある: https://github.com/krallin/tini
Distrust では Rust で 500 行未満の超シンプルな init システムを自作しており、セキュリティ必須の enclave 環境で、一部クライアントが本番運用している。Rust 標準ライブラリだけを使っているので、監査が非常に容易だった
https://git.distrust.co/public/nit
nitより 33% 大きい)、readme にはビルド方法しかなく、実際のインターフェースや動作についての説明がない依存関係を指定できない、ユーザー/グループ設定がない、順序を手動で指定する必要がある、サービスの並列起動がない、リソース管理がない。こういうものが欠けたシステムを init システムと呼ばないでほしい。ただのベアボーンなプロセススーパーバイザだ