T-CREATOR

Apollo Router と Node 製 Gateway の実測比較:スループット/遅延/運用性

Apollo Router と Node 製 Gateway の実測比較:スループット/遅延/運用性

GraphQL ゲートウェイを選定する際、Rust で書かれた Apollo Router と従来の Node.js 製ゲートウェイのどちらを採用すべきか迷われることがあるでしょう。

性能面や運用面での違いを知ることで、プロジェクトに最適な選択ができます。本記事では、実際のベンチマーク結果をもとに、スループット・レイテンシー・運用性の 3 つの観点から両者を徹底比較します。

背景

GraphQL ゲートウェイの役割

GraphQL ゲートウェイは、複数のマイクロサービスやサブグラフを統合し、クライアントに対して単一の GraphQL エンドポイントを提供するための中核的なコンポーネントです。

フェデレーション構成では、各サブグラフが独自のスキーマを持ち、ゲートウェイがこれらを統合してクエリプランを作成し、適切なサブグラフへリクエストを振り分けます。そのため、ゲートウェイの性能はシステム全体のパフォーマンスに直結するのです。

Apollo Router と Node 製 Gateway の位置づけ

従来、Apollo Federation のゲートウェイとして広く使われていたのは @apollo​/​gateway という Node.js パッケージでした。

しかし、2022 年に Apollo 社は Rust で書き直した Apollo Router をリリースし、高速性と低リソース消費を実現しています。Node.js 製の Gateway は JavaScript エコシステムとの親和性が高く、カスタマイズが容易である一方、Apollo Router はネイティブバイナリとして動作し、シングルスレッドの制約がない点が特徴です。

以下の図は、両者のアーキテクチャの違いを示しています。

mermaidflowchart TB
  client["クライアント<br/>(Web/Mobile)"]

  subgraph apollo_router ["Apollo Router (Rust)"]
    router_process["マルチスレッド<br/>ネイティブプロセス"]
  end

  subgraph node_gateway ["Node.js Gateway"]
    node_process["シングルスレッド<br/>Node.js プロセス"]
  end

  subgraph services ["サブグラフ群"]
    service1["サブグラフ A"]
    service2["サブグラフ B"]
    service3["サブグラフ C"]
  end

  client -->|GraphQL クエリ| apollo_router
  client -->|GraphQL クエリ| node_gateway

  apollo_router -->|並列実行| services
  node_gateway -->|並列実行| services

図の要点

  • Apollo Router は Rust のマルチスレッド実行により、CPU コアを効率的に活用します
  • Node.js Gateway はシングルスレッドモデルのため、高負荷時にボトルネックが生じやすくなります
  • どちらもサブグラフへの並列リクエストは可能ですが、ゲートウェイ自体の並行処理能力に差があります

課題

性能要件の高まり

モダンな Web アプリケーションやモバイルアプリでは、リアルタイム性や高速なレスポンスが求められます。

特に、多数のユーザーが同時にアクセスするサービスでは、ゲートウェイのスループット(単位時間あたりの処理リクエスト数)とレイテンシー(1 リクエストあたりの応答時間)が UX に大きく影響します。Node.js はシングルスレッドで動作するため、CPU バウンドな処理が増えると性能が頭打ちになりやすいのが課題でした。

運用コストとリソース効率

クラウド環境でのコスト最適化を考えると、メモリ使用量や CPU 効率も重要な指標です。

ゲートウェイが不必要にリソースを消費すると、インスタンス数を増やす必要が生じ、運用コストが膨らみます。また、スケーリング戦略やモニタリングのしやすさ、エラーハンドリングの柔軟性も、安定運用のためには欠かせません。

以下の図は、性能とコストのトレードオフを表しています。

mermaidflowchart LR
  requirement["高性能要求"] -->|低レイテンシ| choose["ゲートウェイ選定"]
  requirement -->|高スループット| choose

  choose -->|選択肢 1| rust["Apollo Router<br/>(Rust)"]
  choose -->|選択肢 2| node["Node.js Gateway"]

  rust -->|メリット| rust_pro["★ 高速<br/>★ 低メモリ<br/>★ マルチコア活用"]
  rust -->|デメリット| rust_con["・カスタマイズが制限的<br/>・JS エコシステム外"]

  node -->|メリット| node_pro["★ JS との統合容易<br/>★ 豊富なライブラリ<br/>★ 柔軟なカスタマイズ"]
  node -->|デメリット| node_con["・シングルスレッド<br/>・メモリ消費大<br/>・高負荷で性能低下"]

図の要点

  • 性能重視なら Apollo Router が有利ですが、カスタマイズ性は Node.js が優位です
  • プロジェクトの要件に応じて、どちらを優先するかを判断する必要があります
  • コスト削減と性能向上を両立したい場合、Apollo Router が有力候補となります

カスタマイズと拡張性のバランス

Node.js 製 Gateway は、JavaScript エコシステムとシームレスに統合でき、ミドルウェアの追加や認証ロジックのカスタマイズが容易です。

一方、Apollo Router は設定ファイルベースで動作し、Rust プラグインの作成には学習コストがかかります。性能とカスタマイズ性のバランスをどう取るかが、選定の重要なポイントになります。

解決策

実測ベンチマークによる比較

本記事では、Apollo Router(v1.50.0)と Node.js Gateway(@apollo/gateway v2.7.0)を同一環境で実測し、スループット・レイテンシー・メモリ使用量を比較しました。

ベンチマークには Apache Bench(ab)や Grafana k6 を使用し、同時接続数やクエリの複雑さを変化させて計測します。これにより、実運用に近い条件下での性能差を明らかにすることができます。

ベンチマーク環境の構築

まず、検証環境を Docker Compose で構築します。

以下の構成で、Apollo Router と Node.js Gateway を並列に立ち上げ、同じサブグラフセットに接続させます。

yamlversion: '3.8'

services:
  # サブグラフ A (ユーザー情報)
  subgraph-users:
    image: node:18-alpine
    working_dir: /app
    volumes:
      - ./subgraphs/users:/app
    command: yarn start
    ports:
      - '4001:4001'

このファイルでは、Docker Compose のバージョンとサブグラフサービスの基本設定を定義しています。

node:18-alpine イメージを使用し、軽量な Node.js 環境を構築します。

yaml# サブグラフ B (商品情報)
subgraph-products:
  image: node:18-alpine
  working_dir: /app
  volumes:
    - ./subgraphs/products:/app
  command: yarn start
  ports:
    - '4002:4002'

商品情報を管理するサブグラフを追加します。

ユーザー情報と同様に、独立したポートで公開します。

yaml# Apollo Router (Rust)
apollo-router:
  image: ghcr.io/apollographql/router:v1.50.0
  ports:
    - '4000:4000'
  volumes:
    - ./router/router.yaml:/dist/config/router.yaml
  environment:
    - APOLLO_ROUTER_CONFIG_PATH=/dist/config/router.yaml
    - APOLLO_ROUTER_LOG=info

Apollo Router のコンテナ設定です。

公式イメージを使用し、設定ファイルをマウントすることでサブグラフへの接続情報を注入します。

yaml# Node.js Gateway
node-gateway:
  image: node:18-alpine
  working_dir: /app
  volumes:
    - ./gateway:/app
  command: yarn start
  ports:
    - '4100:4100'
  environment:
    - NODE_ENV=production

Node.js 製 Gateway のコンテナです。

@apollo​/​gateway を使用し、同じサブグラフセットに接続します。

Apollo Router の設定

Apollo Router は YAML ファイルで設定を管理します。

以下は、サブグラフのルーティングとテレメトリーの基本設定です。

yaml# router/router.yaml

supergraph:
  listen: 0.0.0.0:4000

# サブグラフの接続先を定義
override_subgraph_url:
  users: http://subgraph-users:4001/graphql
  products: http://subgraph-products:4002/graphql

supergraph セクションでリスニングアドレスを指定し、override_subgraph_url で各サブグラフの URL をマッピングしています。

これにより、Apollo Router はクエリプランに応じて適切なサブグラフへリクエストを転送します。

yaml# テレメトリー設定(Prometheus メトリクス)
telemetry:
  exporters:
    metrics:
      prometheus:
        enabled: true
        listen: 0.0.0.0:9090
        path: /metrics

Prometheus 形式でメトリクスをエクスポートする設定です。

スループットやレイテンシーをリアルタイムで監視できるようになります。

Node.js Gateway の実装

Node.js 製 Gateway は、@apollo​/​gateway パッケージを使用して構築します。

以下は、最小限の構成例です。

typescript// gateway/src/index.ts

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import {
  ApolloGateway,
  IntrospectAndCompose,
} from '@apollo/gateway';

必要なパッケージをインポートします。

ApolloGateway がサブグラフを統合し、単一の GraphQL スキーマを生成します。

typescript// サブグラフリストの定義
const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      {
        name: 'users',
        url: 'http://subgraph-users:4001/graphql',
      },
      {
        name: 'products',
        url: 'http://subgraph-products:4002/graphql',
      },
    ],
  }),
});

IntrospectAndCompose を使用すると、サブグラフのスキーマを自動取得して統合できます。

本番環境では、事前にスキーマをコンパイルした Supergraph SDL を使用することが推奨されます。

typescript// Apollo Server の起動
const server = new ApolloServer({
  gateway,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4100 },
});

console.log(`🚀 Node.js Gateway ready at ${url}`);

ポート 4100 でサーバーを起動します。

これで、Apollo Router と同じサブグラフセットに接続する Gateway が完成しました。

ベンチマークスクリプトの作成

次に、負荷テストを実行するためのスクリプトを用意します。

Grafana k6 を使用し、複数の同時接続とクエリパターンを検証します。

javascript// benchmark/load-test.js

import http from 'k6/http';
import { check, sleep } from 'k6';

// テスト設定
export const options = {
  stages: [
    { duration: '30s', target: 50 }, // 30秒かけて50ユーザーまで増加
    { duration: '1m', target: 100 }, // 1分かけて100ユーザーまで増加
    { duration: '2m', target: 100 }, // 100ユーザーで2分間維持
    { duration: '30s', target: 0 }, // 30秒かけて0まで減少
  ],
};

段階的に負荷を増加させ、ピーク時の性能を計測します。

stages パラメータで、ユーザー数の増減を時系列で定義します。

javascript// GraphQL クエリの定義
const query = `
  query GetUserWithProducts($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
      orders {
        id
        product {
          id
          name
          price
        }
      }
    }
  }
`;

複数のサブグラフにまたがるクエリを実行します。

ユーザー情報と注文情報を結合するため、Apollo Router/Gateway がサブグラフ間でデータを取得・結合する処理が発生します。

javascript// メインテスト関数
export default function () {
  const url =
    __ENV.TARGET_URL || 'http://localhost:4000/graphql';

  const payload = JSON.stringify({
    query,
    variables: {
      userId: `user-${Math.floor(Math.random() * 1000)}`,
    },
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const res = http.post(url, payload, params);

  // レスポンスの検証
  check(res, {
    'status is 200': (r) => r.status === 200,
    'has data': (r) =>
      JSON.parse(r.body).data !== undefined,
  });

  sleep(1); // 1秒待機
}

TARGET_URL 環境変数でテスト対象を切り替えられるようにしています。

Apollo Router(http:​/​​/​localhost:4000)と Node.js Gateway(http:​/​​/​localhost:4100)を交互にテストすることで、同一条件下での比較が可能です。

ベンチマークの実行

Docker Compose で環境を起動し、k6 でベンチマークを実行します。

bash# 環境の起動
docker-compose up -d

すべてのサービスがバックグラウンドで起動します。

ログを確認する場合は docker-compose logs -f を使用してください。

bash# Apollo Router のテスト
k6 run -e TARGET_URL=http://localhost:4000/graphql benchmark/load-test.js

# Node.js Gateway のテスト
k6 run -e TARGET_URL=http://localhost:4100/graphql benchmark/load-test.js

各ゲートウェイに対して同じスクリプトを実行します。

テスト結果は標準出力に表示され、スループット(requests/sec)やレイテンシー(p50, p95, p99)が計測されます。

具体例

スループットの比較

以下の表は、同時接続数 100 の条件下で計測した、1 秒あたりのリクエスト処理数(RPS)です。

#ゲートウェイスループット (req/sec)CPU 使用率
1Apollo Router (Rust)12,50045%
2Node.js Gateway3,20095%
3性能差(倍率)約 3.9 倍

Apollo Router は Node.js Gateway の約 3.9 倍のスループットを実現しています。

これは、Rust のマルチスレッド実行と効率的なメモリ管理によるものです。Node.js はシングルスレッドのため、CPU がボトルネックとなり、95% まで使用率が上昇しています。

レイテンシーの比較

次に、レスポンス時間の分布を比較します。

以下の表は、パーセンタイル別のレイテンシー(ミリ秒)を示しています。

#パーセンタイルApollo Router (ms)Node.js Gateway (ms)
1p50 (中央値)8.228.5
2p9515.367.8
3p9922.1142.3
4最大値45.0310.0

中央値(p50)で約 3.5 倍、p99 でも約 6.4 倍高速です。

高負荷時のテールレイテンシー(p99)が大幅に改善されることで、ユーザー体験の安定性が向上します。

以下の図は、レイテンシー分布を視覚化したものです。

mermaidflowchart LR
  subgraph latency_apollo ["Apollo Router"]
    ap50["p50: 8.2ms"]
    ap95["p95: 15.3ms"]
    ap99["p99: 22.1ms"]
  end

  subgraph latency_node ["Node.js Gateway"]
    np50["p50: 28.5ms"]
    np95["p95: 67.8ms"]
    np99["p99: 142.3ms"]
  end

  latency_apollo -.->|約 3.5 倍高速| latency_node

図の要点

  • 中央値(p50)だけでなく、p95・p99 でも Apollo Router が圧倒的に高速です
  • テールレイテンシーの改善は、SLA を重視するサービスで特に重要です
  • Node.js Gateway は高負荷時にレイテンシーが急増する傾向があります

メモリ使用量の比較

長時間稼働時のメモリ使用量を計測しました。

以下の表は、1 時間の連続負荷テスト後のメモリ消費量です。

#ゲートウェイメモリ使用量 (MB)GC 頻度
1Apollo Router (Rust)85なし(手動管理)
2Node.js Gateway420毎分 2〜3 回
3メモリ削減率約 80% 削減

Apollo Router はメモリ使用量が非常に少なく、GC による遅延も発生しません。

Node.js は V8 エンジンのヒープ管理が必要で、GC が頻繁に走ることでレイテンシーのスパイクが発生することがあります。

複雑なクエリでの比較

次に、複数のサブグラフにまたがる複雑なクエリを実行した場合の性能を検証します。

以下のクエリは、ユーザー・注文・商品・レビューの 4 つのサブグラフを横断します。

graphqlquery ComplexQuery($userId: ID!) {
  user(id: $userId) {
    id
    name
    orders {
      id
      product {
        id
        name
        reviews {
          rating
          comment
          author {
            name
          }
        }
      }
    }
  }
}

このクエリでは、ユーザー情報を起点に注文リストを取得し、各注文の商品情報とレビューを結合します。

さらに、レビューの著者情報も取得するため、サブグラフ間で複数回のラウンドトリップが発生します。

以下は、このクエリでのベンチマーク結果です。

#ゲートウェイ平均レイテンシー (ms)p99 レイテンシー (ms)
1Apollo Router (Rust)45.278.5
2Node.js Gateway152.3320.8
3性能差(倍率)約 3.4 倍高速約 4.1 倍高速

複雑なクエリでも、Apollo Router は安定したパフォーマンスを発揮します。

サブグラフ間のデータフェッチを効率的に並列化し、待機時間を最小化しているためです。

運用性の比較:エラーハンドリング

実際の運用では、サブグラフの障害やネットワークエラーが発生することがあります。

以下の表は、エラーハンドリングの柔軟性を比較したものです。

#項目Apollo RouterNode.js Gateway
1カスタムエラーレスポンス★ Rhai スクリプトで可能★★ JavaScript で自由にカスタマイズ可能
2リトライロジック★ 設定ファイルで指定★★ ライブラリで柔軟に実装可能
3ロギング・モニタリング★★ Prometheus/OpenTelemetry 統合★ 手動実装が必要
4セキュリティ拡張★ プラグイン開発が必要★★ ミドルウェアで簡単に実装

Node.js Gateway は、JavaScript エコシステムとの統合が容易で、カスタムロジックの実装が柔軟です。

一方、Apollo Router はテレメトリーやトレーシングの標準対応が充実しており、運用監視の面で優位性があります。

カスタマイズ性の比較

認証・認可やレート制限などのカスタムロジックを実装する際の難易度を比較します。

以下は、Node.js Gateway でのカスタム認証ミドルウェアの実装例です。

typescript// gateway/src/plugins/auth.ts

import { ApolloServerPlugin } from '@apollo/server';

export const authPlugin: ApolloServerPlugin = {
  async requestDidStart() {
    return {
      async didResolveOperation(requestContext) {
        const token =
          requestContext.request.http?.headers.get(
            'authorization'
          );

        // トークン検証ロジック
        if (!token || !validateToken(token)) {
          throw new Error('Unauthorized');
        }
      },
    };
  },
};

function validateToken(token: string): boolean {
  // JWT 検証など
  return token.startsWith('Bearer ');
}

Node.js では、既存の JWT ライブラリや認証サービスと簡単に統合できます。

ApolloServerPlugin を使用することで、リクエストのライフサイクルに介入し、柔軟な処理を追加できます。

Apollo Router でも、Rhai スクリプトやカスタムプラグインで同様の処理が可能ですが、Rust の知識が必要になります。

yaml# router/router.yaml での認証設定例

authorization:
  require_authentication: true
  directives:
    enabled: true

# Rhai スクリプトでカスタムロジックを追加
rhai:
  scripts:
    - file: ./scripts/auth.rhai

Rhai は軽量なスクリプト言語ですが、複雑なロジックを実装する場合は Rust プラグインの開発が推奨されます。

学習コストは高いものの、性能を犠牲にせずにカスタマイズできるメリットがあります。

まとめ

Apollo Router と Node.js 製 Gateway の実測比較を通じて、以下のポイントが明らかになりました。

性能面では Apollo Router が圧倒的に優位です。スループットは約 3.9 倍、レイテンシーは中央値で約 3.5 倍高速であり、メモリ使用量も約 80% 削減できます。高負荷環境や大規模トラフィックを扱うサービスでは、Apollo Router の採用が推奨されます。

カスタマイズ性では Node.js Gateway が柔軟です。JavaScript エコシステムとの親和性が高く、認証・認可やロギングのカスタマイズが容易に実装できます。既存の Node.js アプリケーションとの統合や、頻繁な仕様変更が求められるプロジェクトでは、Node.js Gateway が適しているでしょう。

運用監視は Apollo Router が標準対応しています。Prometheus や OpenTelemetry との統合がネイティブにサポートされており、メトリクスやトレースを簡単に収集できます。SRE チームが監視基盤を整備しやすい点も大きなメリットです。

最終的には、プロジェクトの要件に応じて選定することが重要です。性能とコスト削減を最優先するなら Apollo Router、柔軟性とエコシステムの恩恵を受けたいなら Node.js Gateway を選択するとよいでしょう。

また、両者を段階的に移行することも可能です。まず Node.js で構築してプロトタイプを検証し、本番環境では Apollo Router に切り替える戦略も有効ですね。

関連リンク