世界を動かす技術を、日本語で。

Nitro: 小さくても柔軟な初期化システムとプロセス監視ツール

概要

Nitro は、軽量なプロセス監督ツールで、Linuxの initプロセス (pid 1)としても利用可能。 組み込み機器サーバーコンテナ など多様な用途に対応。 RAM上で全状態管理 し、効率的なイベント駆動型動作を実現。 シンプルな ディレクトリ構成 によるサービス設定。 nitroctl によるリモート制御や、柔軟なログ管理機能を搭載。

Nitro の特徴と用途

  • Nitro は、極小サイズの プロセススーパーバイザー
  • Linuxの initプロセス(pid 1) としても動作
    • 組み込み機器デスクトップサーバー 用途
    • Linux initramfs 用init
    • Docker/Podman/LXC/Kubernetes などのLinuxコンテナinit
    • POSIXシステム上の 非特権監督デーモン

要件

  • Unixソケット 対応カーネル
  • tmpfs または書き込み可能な /run ディレクトリ

他システムとの比較優位性

  • 全状態をRAM管理、読み取り専用ルートfsでも動作
  • イベント駆動型、ポーリング不要
  • 実行時の動的メモリ割当ゼロ
  • ファイルディスクリプタ数の無制限使用なし
  • 自己完結型バイナリ (musl libc利用時は極小静的バイナリ)
  • 設定ファイルのコンパイル不要、サービスはスクリプトディレクトリのみ
  • 信頼性の高いサービス再起動
  • サービスごとのログ機能、ログチェーン構成も可能
  • システムクロック非依存
  • FreeBSD 上でも動作(/etc/ttys利用)

サービスディレクトリ構成

  • /etc/nitro (または指定ディレクトリ)内の各ディレクトリがサービス
    • setup: サービス開始前に実行、0で継続
    • run: サービス本体、プロセスが生存中はサービス稼働
    • finish: run終了後に実行、runの終了ステータスとシグナルを引数で受け取る
    • log: 他サービスディレクトリへのシンボリックリンク、ログパイプ処理
    • down: 存在時はデフォルトで起動しない
    • @で終わるディレクトリ: パラメータ化サービス用、nitroは無視
  • サービス名制限: 64文字未満、/・カンマ・改行禁止

特殊サービス

  • LOG: logシンボリックリンクがない全サービスのデフォルトログサービス
  • SYS:
    • SYS/setup: 他サービス起動前に実行
    • SYS/finish: シャットダウン時に実行
    • SYS/final: 全プロセス終了後に実行
    • SYS/fatal: 致命的エラー時に実行
    • SYS/reincarnate: シャットダウン時に実行、initramfs実装などに利用

パラメータ化サービス

  • @で終わるディレクトリ は直接起動せず、シンボリックリンクやnitroctlで起動
    • 例: agetty@/run + agetty@tty1 -> agetty@ シンボリックリンク
    • nitroctl up agetty@tty2 で agetty@/run tty2 実行

動作モード・ライフサイクル

  • 起動: gSYS/setup→全サービス起動(down指定除く)
  • サービス再起動: 異常終了時は最大2秒間隔で再起動
  • シャットダウン/再起動: nitroctl Reboot/Shutdown
    • SYS/finish→SIGTERM→最大7秒→SIGKILL→SYS/final→再起動/シャットダウン/終了

nitroctlによる制御

  • nitroctl [COMMAND] [SERVICE] 形式でリモート制御
  • 主なコマンド
    • list: サービス一覧・状態・pid・稼働時間・終了ステータス
    • up/down/start/restart/stop: サービス起動・停止・再起動
    • p/c/h/a/i/q/1/2/t/k: 各種シグナル送信
    • pidof: サービスのpid出力
    • rescan: /etc/nitro再読込
    • Shutdown/Reboot: システムシャットダウン/再起動

シグナルによる制御

  • SIGHUP: rescan
  • SIGINT: reboot
  • SIGTERM: shutdown(pid 1以外)

Linux initとしての利用

  • 自己完結型バイナリ、pid 1で直接起動可能
  • 必要に応じて /dev/run マウント
  • Ctrl-Alt-Delete で秩序立った再起動

Dockerコンテナinitとしての利用

  • 静的バイナリ なので容易にコンテナへコピー可能
    • 例:
      • COPY ./nitro /bin/
      • COPY ./nitroctl /bin/
      • CMD ["/bin/nitro"]
  • /run ディレクトリ必須
  • NITRO_SOCK で外部からnitroctl制御も可能

FreeBSDでの利用

  • /etc/ttys に以下を追加し、FreeBSD initで監督起動
    • /etc/nitro "/usr/local/sbin/nitro" "" on

ライセンス・著者

  • 著者: Leah Neukirchen(leah@vuxu.org)
  • ライセンス: 0BSD
  • 参考: daemontools, freedt, runit, perp, s6 など先行システムからの知見

Hackerたちの意見

s6と比べてどうなの?最近、Dockerコンテナでinitシステムをセットアップするのに使ったんだけど、nitroがいい代替になるのか気になってる。s6-overlayで設定しなきゃいけないファイルが多くて、思ったほど直感的じゃなかったんだよね。

s6はもっと複雑でリッチだね。Nitroやrunitはシンプルな代替になるだろうし、もしかしたらhttps://github.com/krallin/tiniもいいかも。

コンテナ内でinitシステムを動かすって話を見ると、いつも悩むんだよね。一方では、その用途を考えて設計されてるのはいいことだと思う。でも、単一のコンテナ内で、もっと適切にKubernetesやクラウド向けに設計されるべきものを、あまりにも複雑にしようとしてるのを見すぎた気がする。「人は結局やるだろう」って感じかな。でも、「もっと良くやる」ことで問題を広げるリスクがあるのか、古い解決策を残しておく方がいいのか、ちょっと迷ってる。

ロボティクスの経験から言うと、多くのコンテナは「これが元々はベアメタルで動いてたものをコンテナに移した」って感じで始まるんだ。プロセス間で無秩序なRPCが行われてるから、プロセスを別々のコンテナに分けるメリットはあまりないよ。Supervisor、runit、systemd、tmuxセッションなんかは、モノリシックな「アプリ」コンテナでいろいろなものを動かすための人気の選択肢だね。

そうだね、アプリケーションコンテナは「一つのことをやって、それをうまくやる」っていうUnixの哲学を守るべきだ。でも、もしDockerコンテナ内のプロセスが何らかの理由でフォークするなら、PID 1にはちゃんとしたinitが必要だよ。

コンテナ単位で料金を取るホスティングプロバイダーをいくつか使ったことがあるよ - Fly.io、Render、Google Cloud Run。価格の理由で、コンテナ内で複数のプロセスを動かしたくなることがよくあるんだ。

Distrustでは、セキュリティクリティカルなエンクレーブのユースケースで使われている、超シンプルなinitシステムをRustで書いたよ。https://git.distrust.co/public/nit

おそらくすごく良さそう(Nitより33%大きいけど)、でもREADMEにはビルド方法しか書いてなくて、インターフェースや動作については説明がないんだよね。

runitとの比較もしてみたいな。runitはすごくミニマルだけど、ほぼフル機能のinitシステムだし。制御ディレクトリや宣言的な依存関係がないこと、似たようなスクリプトのセット、ログの取り扱い方が似てるところが多いね。ページではrunitに触れられていて、chpstユーティリティを使うことも提案されてる。一つの対照的な特徴は、パラメータ化されたサービスで、いくつかの似たプロセス(agettyみたいなの)を一つのサービスディレクトリで制御できるのが面白い。もう一つの違いは、同じバイナリ(nitroctl)で再起動やシャットダウンをアクションとして実行できること。あと、nitroは単一のバイナリだけど、runitはいくつかあるからね。

Void Linuxを通じてrunitに慣れたけど、initシステムとしては機能してるものの、UIとドキュメントには不満が残る。特にログの設定が難しくて、サービスのために設定しようとしたときは本当にイライラした。シンプルで、まともなデフォルト、より良いドキュメント、直感的なUIを持つ他のものを試してみてもいいかな。

Leah NeukirchenはVoid Linuxコミュニティのアクティブなメンバーだから、ここでたくさんのクロスポリネーションが期待できるね。彼女がVoidでの使い方について何か書いてくれたら本当に素晴らしいと思う。

去年、runitで設定されたプロセスを動かしてた最後のサーバーを廃止したんだ。悲しい日だったな。15年前くらいにrunitサービスを書くことを学んで、それはすごくクールで理解しやすかったから、Linuxではそういうふうにサービスが動くものだと思ってた。そんで、5年間Linuxから離れてたら、戻ったときにはSystemdが主流になってた。悪い噂もいくつか聞いたけど、結局その議論の多くが悪意から来てることに気づいて、今では本当の理由が何なのかもわからなくなっちゃった。今はPi Zeroで、我が家のヒゲトカゲのビバリウムからカメラと温度データをストリーミングするサービスをいくつか動かしてるけど、systemdを使って設定するのはすごく簡単だったよ。それに、メインのOpenSuseデスクトップでemacsdを動かしたり、仕事用のノートパソコンでGoogle DriveのFuseソリューションを使ったりもできた。「標準的なものがあるのは実際いいことだね」って感じかな。

宣言的な依存関係がない、これって売りになるの?なんでそう言えるの?systemdが嫌われる理由は色々聞いたけど、宣言的なデザインに対する批判はあまり聞かないな。

彼女が2024年に行ったトークのスライドに、runitとの適切なミニマルな比較があるよ。(PDF): https://leahneukirchen.org/talks/#nitroyetanotherinitsy

友達で同僚の仕事を宣伝させてもらうと、https://nixos.org/manual/nixos/unstable/#modular-services がNixpkgsに追加されたばかりなんだ。これはNixOSを新しいinitシステムや新しいカーネルに移植するためのゲームチェンジャーになるよ。だから、Nitroみたいなものを試すにはいいタイミングだね!

名前と機能がAWS Nitroとかなり重なってるね。https://docs.aws.amazon.com/whitepapers/latest/security-desi...

問題はないと思うよ。一つは誰でも使えるinitシステムで、もう一つは社内用のKVMフォークだから、他の誰も気にしてないし。

名前は似てるけど、initシステムとハイパーバイザーはかなり違うよね。

これをdinit[1]と比較するのも面白そうだね。chimera-linuxで使われてるやつだし。READMEをざっと見た感じ、今のところサービスの依存関係は扱ってないみたい? [1]: https://github.com/davmac314/dinit

Nitroはサービスの依存関係を明示的に扱わないから、一発でキレイなグラフを得るのは無理だね。でも、セットアップスクリプトで他のサービスを起動するようにリクエストすることはできるし、依存するサービスが動いてるときにNitroが待って再起動を試みることもできるよ。キレイなグラフが欲しいなら、grepを使って簡単なスクリプトを書くといい。逆に、サービスがダウンしたときに依存するサービスのシャットダウンを忘れがちで、Nitroのユーティリティを使ってそれを発見する方法はないんだよね。

Artix Linuxでdinitを使ったことがあるよ。軽量で、すごく良かったよ。(https://artixlinux.org/faq.php)

依存関係を指定できないinitシステム?ユーザーやグループの設定もなし?順序を手動で設定しなきゃいけない?並行してサービスを立ち上げられない?リソース管理もなし?こんなのをinitシステムって呼ばないでほしい。単なるプロセス監視ツールだよ。

実際、これらのことは全部できるよ。私の経験では、systemdよりもずっと良い。nitroは使ってないけど、何十年もdaemontools(nitroの進化版)を使ってる。使いやすくて、安定してて、理解しやすくて、管理もしやすい。依存関係をどう扱うかは明確な方法がないし(依存関係がプロセスの3秒後に死んだらどうするの?正解はいくつもあるよ)。djb/daemontoolsのやり方は「それは君の問題だ。でも、ここに信頼できるシンプルで安価なツールがあるから、依存関係を始めたり止めたり監視したりしてね」って感じ。

13年前にCで自分のinitシステムをゼロから作ったことがある。自分も承認したマネージャーも、思ったより大変だったよ。LinuxのGUIを立ち上げて、あまり性能の良くないハードウェアでn秒でバックエンドを動かすためのもので、nは覚えてないけど、すごかった。いいプログラミングの練習になったよ。もしかしたら当時すでに似たようなものがあったかもしれないし、全体的に見て何が手に入るかの洞察が足りなかっただけかも。多分、コードは今もどこかのバックアップに残ってるけど、見返してないし、わからないな…権利を持ってた会社はもう倒産しちゃったし。追記:これを書いてたら、同じ会社で別の同僚がまた別のinitを作ったことを思い出した。私のはlibc以外に依存関係がなくて、あまり機能もなかったけど、新しいのはlibeventをベースにしてて、たぶんもう少し進んでたんだろうね。

こういうプロジェクト、めっちゃ好き!Unixユーザーランドの低レベルな部分にたくさん触れてるし。systemdが古典的なSysVやPOSIXを超えて、Linuxカーネル特有の機能をうまく活用しようとしたのも評価してる。でも、これが最後の答えじゃないことを願ってるし、この分野で新しいアイデアや革新がもっと探求されるといいな。最近、製造時のデバイスプロビジョニングプロセスを実装したんだけど、Linuxカーネル(efistub付き)を使って、UEFIファームウェアから直接ネットブートして、Goで書かれた単一のinitバイナリを含むinitramfsを組み込んだんだ。全てのオペレーティング環境が、自分が選んだ高級言語で直接プログラムするパッケージからインポートしたコードで構成されてるのは、本当に自由だよね。サブプロセスや色んなテキスト設定ファイルを通じてシステムとやり取りするのとは全然違う感じ。