コールドスタートの神話:イベント処理のための JVM 最適化
SchemaBridge Team · 2026-01-20 · Serverless, Performance, JVM, Java
Java のコールドスタートの評判は JVM ではなく FaaS の問題です。スレッドプール、JIT のウォームアップ、長時間稼働ワーカーモデルを解説します。
現代クラウドのレイテンシ税
「サーバーレス」は私たちを救うはずでした。無限のスケーリングと管理不要を約束していたのです。しかしユーザーが体感するレイテンシに関して、汎用的なサーバーレス関数(FaaS)には不都合な秘密があります。それがコールドスタートです。
しばらく実行されていない Lambda にリクエストが届くと、プロバイダーはコンテナを立ち上げ、ランタイムを起動し、コードを読み込まなければなりません。複雑なアプリケーションでは、これに数秒かかることもあります。現代の Web アプリケーションの世界では、2 秒は永遠にも等しい時間です。それは「キビキビした」体験と「壊れた」体験の差です。
長年、業界の常識は「Java はサーバーレスには重すぎる。Go か Node.js を使え」というものでした。
SchemaBridge では、この常識に異を唱えます。私たちは JVM(Java Virtual Machine)の堅牢さに依拠し、コールドスタート問題を Java を捨てることによってではなく、そもそも Java を短命な関数として実行しないことによって回避しています。
JVM:短距離走者ではなく、荷役馬
JVM はエフェメラルな関数ではなく、長時間稼働するサーバープロセスのために設計されました。起動直後の時間は「ウォームアップ」に費やされます。クラスのロード、バイトコードのインタプリタ実行、そしてホットパスの最適化(JIT コンパイル)です。
Java アプリケーションを 100 ms で終了する「関数」として扱うなら、ランタイムの物理法則と戦うことになります。JIT 最適化の恩恵を受けることなく、リクエストごとに起動コストを払い続けることになるのです。
実践:長時間稼働するアプリケーションコンテキスト
SchemaBridge は、ワークフローのステップを分離されたエフェメラルな関数として実行しません。代わりに、ステップは Kubernetes 上にデプロイされた長時間稼働する JVM ワーカーで実行されます。
1. スレッドプーリング:各ワーカーは、Valkey Streams から消費する温まったワーカースレッドの固定プールを保持しています。イベントが届くと、既存のスレッドがそれを引き受けます。イベントごとに OS プロセスを生成することも、JVM を起動することもありません。
2. JIT 最適化:ワーカーは長時間稼働するプロセスなので、JVM の C2 コンパイラにはホットパスをネイティブのマシンコードへ最適化する時間があります。ワーカーはウォームアップするにつれて速くなります。そして公平を期して言えば、デプロイしたばかりのワーカーはコールドな状態で起動し、そのウォームアップのコストを一度だけ払います。イベントごとではありません。
3. コネクションプーリング:データベース接続の管理はコストがかかります。FaaS モデルでは、接続はウォームな実行環境が存続する間しか生きられず、スケールアップのたび、あるいは環境がリサイクルされるたびに確立し直す必要があります。私たちの長時間稼働ワーカーは PostgreSQL と Valkey へのプールされた接続を保持し、外部サービス向けの HTTP クライアントも再利用するため、どのステップも接続をゼロから確立するコストを払いません。
温まったワーカーでも取り除けないのは、永続的(durable)なワークフローエンジンであることのコストです。各ステップは、あなたのロジックが実行される前に、ストリームのホップを経由し、PostgreSQL で自身の状態をクレームし、入力を取得します。これはステップの途中でクラッシュしても生き残るための代償であり、ステップのレイテンシはマイクロ秒ではなく、ネットワークとデータベースのラウンドトリップの範囲になります。
GraalVM の位置づけ
GraalVM はすでにプラットフォームの一部ですが、Java を速く起動するための手段としてではありません。作成者がワークフロー内に書く JavaScript や Python のスニペットを実行するポリグロットエンジンであり、プロセスの生成やファイル・ネットワーク I/O を禁止するサンドボックス内で動作します。
GraalVM Native Image――Java サービスを事前(ahead-of-time)にスタンドアロンのバイナリへコンパイルすること――は別の機能であり、私たちは使用していません。起動時間とメモリを大幅に削減すると広く報告されていますが、その代償としてビルドの複雑さや、リフレクションと動的クラスロードへの制約が伴います。長時間稼働するワーカー群にとって、起動時間の改善は短命な関数ほど重要ではありません。だからこそ、私たちは代わりに長時間稼働ワーカーを選んだのです。
例示シナリオ:レイテンシに敏感なパイプライン
FaaS 上の Java 関数で、途切れないイベントストリームを処理している架空のアナリティクスチームを考えてみましょう。
問題
プラットフォームがスケールアップしたり、アイドル状態の関数をリサイクルしたりするたびに、周期的なコールドスタートが秒単位のレイテンシスパイクを生み、テールレイテンシはチーム自身のコードではなく起動によって支配されています。
長時間稼働ワーカーによるアプローチ
その処理を温まった長時間稼働ワーカーに移すと、レイテンシ曲線の形が変わるはずです。
1. 温まったスレッド:イベントは、JIT でウォームアップ済みのプロセス内にすでに存在するスレッド上で実行されます。
2. より安定したテール:イベントごとのコンテナプロビジョニングがなくなるため、起動に起因するスパイクは消えます。残るレイテンシは、処理そのものと、各ステップが行う永続的なホップによって支配されます。
3. トレードオフ:イベントが届くかどうかに関係なく稼働しているワーカーの費用を払うことになります。また、1 桁ミリ秒で測られる厳格なリアルタイム予算は、依然として永続的なワークフローエンジンには不向きな仕事です。それはあなたのアプリケーション自身のリクエストパスに属するものです。
比較:エフェメラルな FaaS vs. 長時間稼働ワーカー
| 機能 | 標準的な FaaS(Lambda) | SchemaBridge の温まったワーカー |
| :--- | :--- | :--- |
| コールドスタート | スケールアップ時にプロセス/ランタイムを起動 | イベントごとのプロセス起動なし |
| ランタイム | スケールアップ時/アイドル後に起動 | 長時間稼働プロセス |
| 最適化 | 限定的(環境がリサイクルされる) | 完全な JIT コンパイル |
| 接続 | 環境のリサイクル時に失われる | プールして再利用 |
| ステップごとのオーバーヘッド | 関数呼び出し | ストリームのホップ + 永続的な状態のクレーム |
Java パフォーマンスのためのエキスパート・チェックリスト
1. リソースを再利用する:ハンドラー関数の中でデータベースクライアントを作成してはいけません。一度だけ(static/グローバルに)作成し、再利用しましょう。
2. ヒープを調整する:積極的なガベージコレクションを避けるため、JVM ヒープがコンテナに対して適切なサイズになっていることを確認しましょう。
3. GC を CPU 制限に合わせる:CPU 制限が 1 コア未満のコンテナでは、パラレルコレクターの恩恵を受けられない場合があります。JVM が実際の制限に対してどの GC を選択しているかを確認しましょう。
4. リフレクションを避ける:リフレクションを多用するライブラリ(一部の古い JSON パーサーなど)はウォームアップが遅くなります。可能な限りコンパイル時生成を選びましょう。
5. GC の停止時間を監視する:ツールを使って、ガベージコレクションがワークフローにレイテンシスパイクを持ち込んでいないことを確認しましょう。
// 高コストなクライアントはイベントごとではなく、プロセスごとに一度だけ作成する。
private static final HttpClient CLIENT = HttpClient.newHttpClient();
結論:ランタイムは重要である
「Java はコールドスタートが遅い」というのは、Java そのものではなく、Java のデプロイ方法についての話です。ランタイムの根底にある物理法則を理解し、その法則を尊重するアーキテクチャを選べば、イベントごとに起動コストを払うことなく Java エコシステムの生産性を維持できます。ただし、残るものについては正直であるべきです。永続的なエンジンは、ステップがクラッシュを生き延びるという保証と引き換えに、多少のレイテンシを支払っているのです。
「Building the Bridge」シリーズの一部です。関連記事:コンプライアンスと監査:副作用としてのエビデンス、汎用コネクタの構築。