ブログ一覧へ戻る

なぜマイクロサービスではなく 1 つの Rust バイナリで Tablize を構築したか

2026-05-23 · Tablize チーム

Tablize は 1 つの Rust バイナリとして出荷します。そのバイナリの中に:エージェントランタイム、HTTP サーバー、5 つのプロダクトドメイン(Data、IoT、App、Media、Platform)、38 のサードパーティ連携、LLM プロバイダークライアント、ツールレジストリ、認証層、課金台帳。docker run でデプロイ。1 プロセス。1 イメージ。

これは 2026 年では不流行な選択です。私たちのようなプロダクトの主要パターンは:5 つ以上のマイクロサービス、ドメイン別の独立したデプロイ、サービスメッシュ、サービスごとの別データベース、それらを結びつけるイベントバス。私たちはそうしませんでした。この記事はその理由です。

(エンジニアリングブログ記事には、誰かが対抗的なアーキテクチャ選択を擁護するために利点をリストし、欠点を払いのける伝統があります。これはそんな記事にはなりません。選択には実コストがあります。それも扱います。)

セットアップ

最適化していた制約:

1. セルフホスト可能。 ユーザーが docker compose up できて完全機能の Tablize インスタンスを持てるべき。「Tablize Lite」ではない。本物。

2. 小規模チームで運用可能。 お客様(Kubernetes を運用しない DTC オペレーター)にも私たち(12 サービスをメンテするプラットフォームチームを持たない)にも。

3. コードでドメイン分離。 Data と IoT ロジック間のもつれたクロスインポートなし。各ドメインは独立して理解可能であるべき。

4. 速い反復。 私たちは速く動いています。機能追加が 4 サービスに触れて 4 デプロイを調整することを要求すべきではない。

5. 低いコールドスタートコスト。 マネージドクラウドはユーザーごとにワークスペースを起動します。コールドスタートが重要 — ワークスペースごとに 5 コンテナを起動するのは遅すぎる。

そのうち 3 つの制約(1、2、5)はモノリスへ強く押します。制約 3 はマイクロサービス分割が解決すると思うものです。制約 4 モノリスへ押す — クロスサービス変更の調整がマイクロサービスチームの最大の税。

そこで私たちは尋ねました:運用の複雑さなしにマイクロサービスの コード組織化 の利益(制約 3)を得られるか?

代わりに何を構築したか

答えは 17 クレートを持つ単一 Rust ワークスペース:

server                ← HTTP エントリーポイント + ルート登録
rusty-claude-cli      ← CLI / 対話シェル
console-server        ← 管理 / 課金 / ワークスペース管理
  └── domain-data         ← 分析、取り込み、CSV/JSON/Parquet
  └── domain-iot          ← MQTT、デバイス、空間インデックス
  └── domain-app          ← アプリ生成、コントラクト
  └── domain-media        ← S3 ストレージ
  └── domain-platform     ← 認証、RBAC、課金、ジョブ
  └── domain-integrations ← 38 コネクター
  └── tools               ← 全ドメインの tool_specs() を集約
      └── runtime → plugins → commands
          └── api         ← LLM プロバイダークライアント

ルール:ドメインはランタイムでクロス依存ゼロ。 Data は IoT を import しない。IoT は App を import しない。各々が公開するのは:

  • ドメインのリソース(PG プール、MQTT クライアントなど)を所有する {Domain}State 構造体
  • このドメインがエージェントに提供するツールを返す tool_specs() 関数
  • 実際にツールを実行するためランタイムが呼ぶ execute_tool_async() 関数

tools クレートだけが 5 つのドメインすべてを import し、それは 1 つのグローバルツールレジストリに集約するためです。エージェントランタイムは要求されたツールを所有するドメインの execute_tool_async() を呼びます。

これが マイクロサービス風のコード分離モノリスデプロイ です。

なぜ Rust か

3 つの理由。

GC ポーズなしのメモリ安全性。 多くの並行 I/O を実行します:エージェントからの WebSocket 接続、MQTT サブスクリプション、LLM プロバイダーへの HTTP リクエスト、Postgres クエリ、S3 ストリーミング。間違った瞬間の世界停止 GC はメッセージをドロップしクエリをタイムアウトさせます。Rust のコンパイル時メモリ安全性 + ゼロコスト async により、プロセスあたり数千の並行接続を実行できます。

1 つのバイナリ、ランタイムなし。 Node や Python と比較して、デプロイのストーリーが劇的にシンプル。「libffi バージョン X がインストールされているか確認」や「Python 3.12 ではなく 3.11 を使用」なし。バイナリは必要なものすべてを静的リンク。Docker イメージは 55 MB。

コンパイラがコードレビューで捕捉するものを捕捉。 これは軽薄に聞こえますが、そうではありません。出荷していたバグだったケースの約 30% がコンパイル時に捕捉されました — 通常は共有状態のライフタイム正確性周り。ペアプログラマーとしてのコンパイラは本物です。

コスト:Rust は Python より書くのが遅く、チームは async Rust パターンに慣れる必要がありました。Tablize は根本的にインフラプロダクトであり、このコードと長年付き合うことになるので、このトレードオフを選びました。

なぜ 1 つの PG インスタンスか

各ドメインは 1 つの Postgres インスタンス内に自分の スキーマ を持ちます(iot.*data.*app.* など) — 別のデータベースでも、別のサーバーでもありません。5 スキーマ、1 PG。

理由:

ドメイン横断結合が一般的。 「顧客がアクティブな IoT アラートも持つ注文を見せて」 — それは data.ordersiot.alerts を横断する結合。1 つの PG なら、これは 1 つのクエリ。ドメインごとに別データベースなら、2 つのサービスから取得後にメモリ内結合をすることになります。

1 つのバックアップは 5 つよりずっとシンプル。 セルフホストユーザーは pg_dump してすべて持ちます。マネージドユーザーはワークスペースごとに 1 つの論理バックアップを得ます。

TimescaleDB 拡張はどこでも適用。 iot.sensor_readingsdata.event_logs の時系列加速は同じ拡張、同じハイパーテーブル機構、同じ保持ポリシーを使用。

接続プールがシンプル。 1 つのプール、一度サイズされ、全ドメインで共有。

コスト:1 つのドメインで暴走したクエリが他に影響する可能性。ドメインごとのステートメントタイムアウトと接続制限で軽減。問題になったか? 1 回、ユーザーの IoT サブスクリプションが秒間 50K メッセージを受信し始め、insert が接続プールを詰まらせたとき。ドメインごとのプールサイジングを追加し、問題は再発していません。安い修正。

なぜドメイン間にイベントバスがないか

ほとんどのマイクロサービスアーキテクチャは Kafka / NATS / Redis pub-sub をサービス間通信に使います。私たちは使いません。IoT ドメインが分析をトリガーする必要があるとき(例:センサー異常 → データクエリを実行)、エージェントランタイム経由のツール呼び出しでそれを行います。

これは直接イベントバスより遅く聞こえます。そうではありません。エージェントランタイムは同じプロセスだから。ツール呼び出しはマイクロ秒でディスパッチ。ネットワークホップなし、シリアル化なし、ブローカーなし。

欠点:エージェントのツール呼び出しトレース以外には「ドメイン A がドメイン B にイベント X を送った」の監査ログがない。それを受け入れる。エージェントトレース ドメイン横断ログ。

ドメイン分離が何をもたらすか

実際には、「ドメインは互いに import できない」というルールが私たちが持つ最も価値あるアーキテクチャ制約です。

意味:開発者が IoT ドメインで作業するとき、Data ドメインがどう機能するか知る必要がない。IoT クレートは自分の Cargo.toml、自分のテスト、PG に対する自分の統合テストを持つ。data::orders への依存を誤って取れない。import がコンパイルできないから。

意味:1 つのドメインにコントラクターをオンボードするとき、コードベース全体ではなく 1 つのドメインのコードでウォームアップ。メンタルモデルが頭に入る。

私たちの経験では、強制された分離はデプロイ分離より重要です。コード がよく整理されていればモノリスを実行できる;コードが絡まっていればマイクロサービスをうまく実行できない。私たちは 2 つのうち簡単な方の問題を選びました。

違ったやり方をするとしたら

率直なリスト:

過剰分離したものがある。 早期に domain-media(S3 ストレージ)を独自クレートにしました。振り返ると、S3 ストレージは非常に薄い抽象なので、どのドメインもインポートする util クレートに住めたはずです。本当のドメインではないものにドメインパターンを過剰適用しました。

エージェントランタイムクレートが大きすぎる。 runtime → plugins → commands は 1 つの論理単位ですが、混乱した境界を持つ 8 サブクレートに成長しました。徐々に統合中;進行中。

WebSocket セッション管理が間違った場所にある。 今は server にあります。HTTP 層ではなくエージェントライフサイクルに紐づくので、おそらく runtime にあるべき。リスト上のリファクタリング。

1 つの PG が永遠にスケールするわけではない。 現在のスケール(単一 PG での 1 億行未満のワークスペース)には大丈夫。数十億に成長するワークスペースには、高ボリュームの IoT データを別 TimescaleDB に分割するかシャードする必要があるでしょう。ブレークポイントを観察中。

マイクロサービスをしなかったコスト

マイクロサービスが与えるが私たちが諦めたもの:

独立したデプロイケイデンス。 新しい IoT 機能を出荷するとき、バイナリ全体が再デプロイ。マイクロサービスなら IoT だけデプロイできた。実際には週 2〜3 回デプロイし、全ロールアウトは速い(Fly.io ローリング再起動、2 分未満)ので、これは痛んでいません。

ドメインごとのスケーリング。 マイクロサービスでは、IoT コンシューマが分析層から独立してスケール可能。私たちの世界では、ワークスペースごとにバイナリ全体をスケール。私たちのコストモデル(ワークスペースごとの Fly.io マシン)では、これで結構 — ワークスペースがスケーリング単位、ドメインではない。

言語の柔軟性。 マイクロサービスは IoT コンシューマを Rust で、LLM ルーターを Python で書くことを許す。私たちはオール Rust。私たちには勝利(1 言語、1 ツールチェーン)ですが、強い既存 Python/Go 投資を持つチームには欠点。

ブラスト半径分離。 エージェントランタイムがクラッシュすると、そのワークスペースの Tablize 全部がクラッシュ。マイクロサービスなら、エージェントランタイムコンテナだけがクラッシュ。Rust の広範なパニックハンドリングとワークスペースごとのプロセス(1 ワークスペースのクラッシュが他に影響しない)で軽減。本物のトレードオフ。

あなたが私たちのやったことをすべきとき

すべてのプロダクトが単一 Rust バイナリとして出荷すべきではありません。この形がフィットするシグナル:

  • セルフホストを些細にする必要がある。 「この Docker イメージを実行」は「この Kubernetes マニフェストを実行」より劇的にシンプル。
  • 当面小規模チーム(20 エンジニア未満)。 マイクロサービスの運用税は、複数チームが複数サービスを所有するまで元を取らない。
  • ドメインがほとんど共有永続状態への読み取り中心。 私たちのドメインはすべて同じ PG に読み書き。本当に別状態のドメインなら、分割の方が理にかなう。
  • コールドスタートレイテンシを最適化。 ワークスペースごとプロセスごとマシンごとの分離は、ワークスペースごとコンテナごとデプロイごとよりずっと速い。

フィットしないシグナル:

  • 独立したリリースケイデンスを持つ独立サービスを所有する複数チーム。 ならマイクロサービスが組織複雑性をスケールする方法。
  • ドメインが極端に異なるトラフィックプロファイル。 1 つのドメインが残りより 100 倍トラフィックを処理するなら、それだけを分割するとスケール可能。
  • ポリグロットが必要。 サービスごとに異なる言語ならマイクロサービス。

ここから何を取るか

アーキテクチャ選択は永続的ではありません。スケールが要求するなら、いつか Tablize を複数バイナリに分割するかもしれません。現在の形は と次の 12〜24 か月の成長に対する正しい形。ドメインクレートとしてよく分離されたコード構造を保つことで、もし分割が起きるなら、それは書き換えではなくリファクタリングです。

似たプロダクトを今日始めるなら、尋ねることを勧めます:「サービスに分割することで解決している コード組織化 問題は何か、規律あるクレート / パッケージ / モジュールで代わりに解決できないか?」 ほとんどの場合、答えはイエス。マイクロサービスのデプロイ複雑性は、コードがよく組織化されているかどうかとはほぼ直交です。

Tablize を無料で試す →


関連記事: