やあ、この投稿はLinuxに直接関係ないかもしれないけど、ここにいる多くの人がバックエンドやクラウドエンジニアリングに関わってると思う。元々このアイデアをr/Backendで共有したんだけど、あんまり反応がなかったから、ここで広くフィードバックをもらおうと思って投稿してる。

で、Androidの内部を掘り下げてたらZygoteに出くわしたんだ。Androidでは、ZygoteがARTランタイムを初期化して、共通のフレームワークやライブラリをプリロードする。アプリが起動すると、Zygoteがフォークして、アイソレーション(名前空間、cgroups、seccomp、SELinux)を適用する。子プロセスは初期化されたランタイムとクラス構造を引き継ぐから、ほぼ瞬時にスタートする。

これと似たアプローチをバックエンドインフラに適用してみたらどうかな。

クラスターのノードを想像してみて。親プロセスがJNI_CreateJavaVMを使ってJVMを初期化して、よく使われるフレームワークやライブラリ(例:JDKクラス、Spring Boot、gRPC、Kafkaクライアント)をプリロードする。この親はmain()を呼び出さない—それは無菌状態で、初期化されたランタイムとクラスメタデータ(klass構造、メソッドテーブル、定数プール、vtable)だけを保持してる。だから、親のヒープは主にこれらのフレームワークやライブラリのパースされたクラスメタデータと構造で汚染される。

サービスやポッドが起動する必要があるとき、親がフォークする。子は初期化されたランタイム状態、クラスメタデータ、プリパースされたフレームワークのバイトコードを引き継ぐ。自分のビジネスロジックの.jarと設定を読み込むだけで、ネットワーキング(ソケット、DB接続など)をセットアップする。フレームワーククラスの再パースや検証は不要。コールドスタートのレイテンシが下がるのは、実行時にサービス特有のコードだけが読み込まれるから。

フォークのセマンティクスがこれを効率的にする:

  1. 共有されたランタイムの.text + フレームワーク/ライブラリのバイトコード + これらのパースされたクラスメタデータは、読み取り専用で子プロセス間で共有される。

  2. 子のJITがこれらの共有フレームワークライブラリのクラス構造(メソッドテーブルや他の可変構造)を変更するときにコピーオンライトが適用される。

  3. 各子は異なる名前空間にマウントできるし、cgroupsやseccompなどの他のLinuxプリミティブも適用してコンテナのようなアイソレーションを提供できる。

-> 各ノードの親は、事前に初期化されたJVM状態のウォームプールとして機能する。

大規模な自己所有システム(Uber、Meta)では、マルチレベルのフォークもできるかも。例えば、トップレベルの親がランタイム + 共通ライブラリ/フレームワークを初期化して、その後、複数のサブ親がトップレベルからフォークしてサービス特有のフレームワークやビジネスロジック(例:Uberの乗車マッチングや運賃計算)をプリロードする。スケーリングはサブ親から直接フォークされて、インスタンスはグローバルなランタイム状態とサービス特有の状態をほぼ瞬時に立ち上げることができる。

コメント (15)

_________________________________________________

Try /r/linuxadmin

個人的には、AndroidでもZygoteは好きじゃない。Androidの制限を回避するためのハックで、解決策じゃなくて単なる修正だと思う。

コンテナや名前空間の方が、俺にとってはずっとエレガントな解決策だ。


でもZygoteは単にフォークするだけじゃない。子プロセスが作成された後、複数のアイソレーションプリミティブを適用する—名前空間(PID、マウント、ネットワークなど)、リソースの会計/制限のためのcgroups、システムコールを制限するためのseccompフィルター、そしてAndroidの場合はSELinuxまで。だからフォークは単なる入り口で、彼が従うアイソレーションモデルはコンテナがやることに精神的に似てる。


そう、コンテナみたいだね。


うん。


要するに、これはdocker/podman/k8sみたいなもので、単一のランタイムに制限されてる… Androidで動くのは、エコシステム全体がJVMの上に構築されてるから。別のユースケースでは、コンテナの方がずっと柔軟だ。

それにflatpaksもあるし、あれもアイソレーションのために非常に似たモデルを使ってる。

これはすでに知られている解決策で、すでに初期化されたランタイムからのフォークを除けば。起動時間が短いのは特定のユースケース以外ではあんまり重要じゃない。


サーバーレスでは、コールドスタート時間がAWS Lambdaみたいにもう少し優先されるけど、どう思う?


AWS LambdaはJavaだけじゃなくて複数のランタイムをサポートしてる。各ランタイムのためにZygoteのような環境を作ることもできるけど、なんで? コンテナで作業したことがあれば、あれがめっちゃ早く起動するのを知ってるだろうし、ずっと柔軟な解決策だ。俺の予想だけど、Androidモデルは初期の低電力デバイスでリソースを節約するために作られたんじゃないかな。バックエンドでは、正直あんまり関係ないと思う。今のスマホでも、そんなに重要じゃないんじゃないかな。


これにはWASMの方がいい解決策じゃない?もっと柔軟で、さらに安全だし。

_________________________________________________

Zygoteが解決するコールドスタートの問題は、一般的なバックエンドシナリオでは致命的な問題じゃないと思うけど、インタラクティブなモバイルアプリにはそうだね。早いスタートが必要なときでも、ライブラリコードを読み込むよりもIOで遅くなることが多い。(例:アプリがKafkaブローカーに接続した後に準備ができるのであって、単にKafkaクライアントライブラリのクラスを読み込んだ後ではない)


それに、React Nativeはたくさんのアプリで使われてる。あれはJVMの上にある完全なJavaScriptエンジンだ。Zygoteが節約する時間は、ほとんどの現代のデバイスではあんまり重要じゃないと思う。アプリが遅く起動する理由は他にもたくさんある。

_________________________________________________

…それはすでに存在する。Class Data Sharingって呼ばれてる。超高速にするけど、ゼロに近づけたいなら、そのユースケースのためにCRACサポートがある。

_________________________________________________

AWS LambdaのSnapStartは似たようなことをする。Emacsイメージもそう。

_________________________________________________

Androidは、プログラムの起動を数ミリ秒短縮するために新しいプロセスを事前に初期化するパフォーマンスハックを発明しなきゃいけないほど、複雑で過剰に設計されたランタイムを持ってるように思える。でも、彼らがすべてのプロセスをJVM/Dalvikに何らかの形でリンクさせることにこだわってることや、バインダーやその他の実装のために変なカーネルパッチを要求してることからも、これを推測できる。もし俺が外部からこういうシステムをターゲットにしてて、任意のメモリの読み書きができるベースバンド制御を持ってたら、Zygoteプロセスはいじるのが面白そうだと思う。だから、Zygoteがクラッシュしたらどうなるか気になる。


このアプローチが役立つバックエンドのニッチはある?


すべてはトレードオフだよ。特定の利益や目標のために余分な複雑さを追加したいのか? フォーク自体はかなり速いけど、cloneにはもっと速いオプションがあって、より強力だ。このデザインが理想的な場所を想像するのは難しいけど、主にセキュリティポリシーを適用することに焦点を当ててるように見える。でも、それはファイルの能力、sudo、setuid 0を通じて別の方法で処理できるかもしれない。