Sprites の設計と実装
Sprites は Docker を使わない Linux VM。作成は 1〜2 秒、100GB の永続ルートファイルシステムを持ち、アイドル時は自動でスリープしてほぼ無料。それを支える三つの判断を解説する。
日本語
コピー

私たちは Fly.io なので、この辺りでいつもならあなたのコンテナを自社のハードウェア上で動かす話をする。世界中どこでも。だが先週Sprites をリリースした。それはまったく別物だ。Sprites は新しい何か、つまり Docker なき Docker、Docker なき Docker である。この記事はその仕組みの話だ。
凡庸な家主はペンを箱ごと買って「ペン入れの引き出し」に突っ込む。できる家主はペンを敵対的に見る。「システムの目的とは、それが実際にやっていることである」。家庭というシステムの目的は、ペンを均等に分散させることだ。数か月もすれば、どれだけ買い溜めしていようと引き出しは空になる。正解は、ペンを探しそうな場所すべてに撒くこと——引き出し、窓辺、机。誰かがペンを必要とした瞬間、手の届くところに何本もあり、しかも最初に探す場所にちょうどある。
これが私が見つけた Sprites を説明する最良の比喩だ。Sprites は私たちが Fly.io で先週リリースしたプラットフォームである。Sprites はボールペン式の使い捨てコンピュータだ。何かを残したいと思ったとき、私たちはそれを用意してある。使える Sprite まで、常に一、二秒しか離れていない。
Sprites は Linux 仮想マシンだ。root 権限がある。createのは一、二秒——どれくらい速いかというと、作成してシェルに入る体験が、既存のマシンに SSH でログインするのとまったく同じに感じられる。各 Sprite には 100GB の永続ルートファイルシステムが付く。アイドル時は自動でスリープし、スリープ中はほとんどコストがかからない。
その結果、私は Sprite に名前を付ける必要性をほとんど感じない。sprite create dkjsdjkと打って、仕事を始めることもある。Fly.io で Sprites を使っている人なら、誰でも何十個も抱えている。
クラウドコンピューティングには、Sprites とまったく同じ形をしたものはまだほとんどない。
- 瞬時に作成
- 時間制限なし
- 永続ディスク
- 安価な非アクティブ状態への自動スリープ
これはそれをどうやって作ったかの記事だ。私たちは新しいオーケストレーションスタックを組み、Fly Machines(私たちの旗艦製品)のために下したいくつかの中核的な決定を覆した。その新しい決定のおかげで、Sprites のスケールと管理はずっと楽になった。私たちは興奮している。
幸運なことに、Fly Machines から Sprites まで、道のりの 90% は、私たちがすでにやった三つのbig decisionsのおかげで進んだ。だからこの記事は楽に書けた。では、前置きはこのくらいにして。
決定 #1:コンテナイメージをやめる
これが一番説明しやすい決定だ。
Fly Machines はおおまかに言って、OCI コンテナを KVM マイクロ VM に再パッケージしたものだ。Docker の使い勝手があり、EC2 インスタンスの分離性と安全性がある。私たちはとても気に入っているが、使い捨てのボールペン式クラウドコンピュータの土台としては明らかに向いていない。
Fly Machines の「ちょっとした裏技」は、startとstopが瞬時にでき、入ってきた HTTP リクエストを処理するのに間に合うように起動できることだ。ただし、すでにcreatedしてある場合に限る。事前に割り当てておく必要がある。Creating Fly Machine には一分以上かかることがある。正しいやり方は、あらかじめ大量に作っておいてstopし、必要なときに準備万端にしておくことだ。しかし Sprites では、createが、ずっと前から待っていたかのように感じられる速さでなければならない。
私たちがユーザーのコンテナを殺すのは、ただ殺したいからだ。
creating Fly Machine が遅い理由の大部分はコンテナにある。愛着を持って言うが、君たちのコンテナは寄せ鍋よりもめちゃくちゃだ。大きくて扱いにくく、プルと展開に時間がかかる。リージョン局所性もひどい。gru-3838でcreateサンパウロの Fly Machine も、gru-d795でcreateを置くのも、速度は変わらない。私たちの OCI registry をこのシステムに追いつかせるために、心が砕けるほど多くのエンジニアリング工数が投入されてきた。
言えるのは、これは難しい仕事だということだ。Sprites はユーザー向けのコンテナを排除した。文字通り、問題は解決した。Sprites はこれをシンプルモードでやっている。
今、その下では、Sprites は依然として Fly Machines だ。ただし、どれも標準のコンテナから実行される。各物理ワーカーは、次の Sprite がどのコンテナから起動するかを正確に知っているので、「空」の Sprite のプールを維持するのは簡単だ。結果として、create Sprite は重い処理を何もする必要がない。基本的には、start Fly Machine のときにやっていることをやるだけだ。
これは今すぐ使える。
今すぐ何十個もの Sprite を作れる。好きなだけ。数秒しかかからない。 Sprite を作成する。→

決定 #2:ディスクにオブジェクトストレージを使う
各 Sprite には 100GB の永続ストレージが付く。これが可能なのは、ストレージの根底が S3 互換のオブジェクトストレージだからだ。
Fly Machine 1 台に 100GB のストレージを付けられる。200GB でもいいし、500GB でもいい。問題はこうだ:
- 自分から申請する必要がある(
flyctlを使う)。デフォルト構成にはできない。 - このストレージは NVMe で、Fly Machine が動いている物理サーバーに直接ぶら下がる。
[†] 単一ノードのクラスタを作ろうとすると、目立つ赤い警告を出す。
Fly Machines のストレージスタックは Postgres クラスタのために設計した。レプリカ構成の Postgres クラスタは Fly Volumes でよく動く。マウントストレージは速いが、データを失う可能性がある†——物理マシンが壊れたら、そこに載っていたビットを魔法で取り戻す手立てはない。直近のスナップショットバックアップまで巻き戻すしかない。レプリケーションを組んだ Postgres ならこれで問題ない。レプリケーションはそのためにある。だが明示的にレプリケーションしていないものにとっては、非常に鋭利な刃の上を歩く話だ。
我々の視点からすると、さらに悪いのは、マウントストレージがワークロードを特定の物理マシンに固定してしまうことだ。Fly Machine を動かしたい理由は山ほどある。Fly Volumes 以前なら、移動はサーバー上の「drain」ボタンを押すだけだった。その能力を失うところを想像してほしい。マウントストレージを使ったワークロードの移行を正しくやるのに 3 年かかったし、今でも「楽だ」とは言えない。
オブジェクトストレージはインターネットのフーバーダムだ。我々が手にした中で最もインフラの超大型工事に近いものと言っていい。
Sprite はこのモデルを捨てた。NVMe は今も使うが、ストレージの根としてではなく、オブジェクトストレージ上の blob に対するリードスルーキャッシュとして使う。S3 互換のオブジェクトストレージは、我々が持っている中で最も信頼できるストレージ技術だ。「Sprite はオブジェクトストレージに支えられている」と書き下すだけで血圧が下がるのが分かる。
これがオーケストレーションにとって意味するところは大きい。実用上、Sprite の永続状態は 1 つの URL だ。帽子を置いた場所が家だ! 移行(あるいは故障した物理マシンからの復旧)はたやすい。社内ツールはまだ初期段階だが、手に入った自由度は一気に増えた。
実際のストレージスタックのために Kurt が設計したクローネンバーグ的な仕掛けについては、あと 1500〜2000 字は書ける。だがまだ流動的なので、ここでは簡単に触れておく。
Sprite のストレージスタックは JuiceFS のモデルを軸に組み立てている(実際、今使っているのは原型をとどめないほど改造した JuiceFS で、SQLite のメタデータバックエンドは書き直した)。考え方は、ストレージをデータ(「chunk」)とメタデータ(chunk の位置を記録した対応表)に分けるというものだ。データ chunk はオブジェクトストレージに、メタデータはローカルの高速ストレージに置く。我々の構成では、このメタデータストアを Litestream で永続化している。ローカルストレージに依存しているものは何もない。
(プリインストールされている Claude Code は、確認も取らずにこちらから checkpoint を打つ)
これにより Sprite の checkpoint と restore も速くなる。checkpoint は、トラブル時の脱出経路ではなく、システムの基本機能として使ってほしいと思えるほど速い。システムの復元というより git restore に近い。checkpoint と restore がどちらもメタデータを動かすだけだから、これが成り立つ。
我々のストレージスタックには、マウントストレージを活用した dm-cache に似た仕組みがある。各 Sprite にスパースな 100GB の NVMe ボリュームを 1 つマウントし、ストレージスタックがそれを使って chunk をキャッシュし、読み取り増幅をなくす。重要なのは(自分の安静時心拍数が下がるのが分かる)、その NVMe ボリュームの中身はどうでもいいということだ。書き込まれる chunk は不変で、本当の状態はオブジェクトストレージにある。
オブジェクトストレージを好むのは Sprite のストレージスタックに限らない。Sprite のグローバルオーケストレーターは Elixir/Phoenix アプリで、アカウントのメタデータの主要な置き場所としてオブジェクトストレージを使う。さらにアカウントごとに独立した SQLite データベースを渡し、これも Litestream でオブジェクトストレージ上に永続化している。
決断 #3:内側から外へのオーケストレーション
クラウドホスティング業界では、ユーザーアプリケーションは互いに独立した同等に重要な 2 つのコンポーネントが管理する。ワークロードをオーケストレーションする host と、それを実行する guest だ。Sprite はこれを逆にする。最も重要なオーケストレーションと管理の仕事は VM の内部で起きる。
ここが肝心だ。Sprite 上で動くユーザーコードは root 名前空間にはいない。我々はあなたとカーネルの間にコンテナを挟み込んだ。あなたが見るのは内側の環境で、VM の root 名前空間で動く一連のサービスが管理している。
Fly Machines を最初からこう作ればよかったと思っている。欠点は見当たらない。内側のコンテナのおかげで、VM 全体を再起動せずに Sprite を再起動できる。checkpoint からの復元でも同じだ。Fly Machines のユーザーも、これから多くのものを得られるはずだ。
Sprites では、このアイデアを限界まで押し進めた。ルート環境には、私たちのオーケストレーションコードの大半が載っている。グローバル API と話しているとき、あなたはおそらく自分の VM と直接話している。その上に:
- 私たちのストレージスタックもそこに住んでいて、チェックポイント/リストアとオブジェクトストレージへの永続化を担っている。
- Sprite と一緒に再起動する必要のあるユーザーコードを登録する、Sprites に公開しているサービス マネージャーも同様だ。
- ロギングもそう。
*:8080にソケットをバインドすると、Sprite の外から到達できるようにする——そう、これもルート名前空間にある。
Fly.io 上で何かを作ったことがある人なら、init(コンテナ内)を変えるほうが、flyd——ホスト上で動く Fly Machines オーケストレーター——のようなものを変えるよりずっと簡単だと知っている。Sprites への変更はホストのコンポーネントを再起動せず、グローバルな状態も乱さない。影響範囲は、その変更を取得した新しい VM に限られる。私たちが眠れないのは、コードを書くのが難しいからではなく、プラットフォーム側の作業があまりに多く残っているからだ——無害に見える変更がクラスタ全体を準安定障害に陥らせないようにするには、とてつもない時間がかかる。Sprites を作る間、私たちはこのことを常に念頭に置いていた。
うまくいったものは残した
Fly.io 上の Sprites は、すでに持っていたインフラの上に成り立っている。たとえば、Claude や Gemini にインターネット上でフルスタックアプリを作らせたいなら、Sprites は今日存在するなかで最速の方法かもしれない。
というのも、Sprites はゴシップベースのサービスディスカバリシステムである Corrosion に直接つながっているからだ。Sprite API に自分の Sprite の公開 URL を生成するよう頼むと、私たちは Corrosion の更新を生成し、それがクラスタ全体に瞬時に伝播する。あなたのアプリは、プロキシのエッジノードを通じて HTTPS URL で配信される。
Sprites と Fly Machines は私たちのアーキテクチャの中で共存している。両者の違いには純粋な改善も含まれるが、ほとんどはトレードオフだ:
- 私たちはかねてから Fly Machine のディスクをオブジェクトストレージ上で動かしたいと考えてきた(それを実現するマイナーな LSVD 機能がある)が、本番環境のホットな Postgres ノードを支えるには性能が足りない。
- そのため、本格的な本番アプリは OCI コンテナとして CI/CD システムからデプロイされる。これも、Fly Machines のオーケストレーションがこれほど難しい大きな理由のひとつだ。
- Sprite の利用はほとんど(すべてではないが)がインタラクティブであり、Sprite のユーザーは VM が積極的にスリープしてコストを抑えられることに恩恵を受ける。一方、eコマースアプリは応答速度をミリ秒単位で測るため、ワークロードをホットなまま保ちたいと考える。
Sprites は、Fly Machines とは異なる種類のコンピューティングに最適化されている。Kurt は未来は可塑的で個別化されたアプリのものだと信じているが、私自身はそこまで確信していない。私の見方では、Sprites 上でプロトタイピングと受け入れテストを行うのは合理的だ。そして満足したら、それをコンテナ化して Fly Machine としてデプロイし、スケールさせる。これを実現する自動化ワークフローは、いずれ登場するだろう。
最後に、Sprites はユーザーコードとの契約だ。ひとつの API と、実行環境がどう動くかについての一連の期待。今は Fly Machines の上で動いているが、そうである必要はない。Jerome はオープンソースのローカル Sprite ランタイムを開発している。他に動かす場所だって、そのうち見つかるだろう。
使わないとわからない
セールスマンみたいに聞こえるのは避けられない。Sprites は、私たちがリリースした中で、個人的に唯一ハマったものだ。なぜ今、プロジェクトを始めるのがこんなに楽に感じるのか、まだうまく説明できない——指を鳴らすだけで新品のコンピュータが手に入る。ポイントは、それらを分割したり、どのコードをどこで動かすか決めたりする理由がないことだ。ただ新しく作ればいい。
だから、完全に理解するには、sprite コマンドをインストールして、Sprite を作って、その中で agent を動かすのが一番だと思う。Claude、Gemini、Codex はプリインストール済みで、checkpoint/restore、サービスの登録、ログの取得といったことを教えてある。Claude は --dangerously-skip-permissions モードで動く(なぜそうしない?)。何か作らせてみてほしい。私はとある Slack チャンネル向けに「シカゴ最高のサンドイッチ」トーナメントアプリを作った。
Sprite の課金は実際に使った分だけ(具体的には、実際に書き込んだストレージブロックのみで、100GB の容量全体ではない)。いくつも立ち上げるのは完全に合理的だ。使い捨てのボールペンみたいなコンピュータだ。慣れてしまうと、手元にないほうがむしろ落ち着かなくなる。