ホームページ >ウェブフロントエンド >jsチュートリアル >Node.js と TypeScript を使用して Nginx のようなスケーラブルなリバース プロキシ サーバーを構築する

Node.js と TypeScript を使用して Nginx のようなスケーラブルなリバース プロキシ サーバーを構築する

Susan Sarandon
Susan Sarandonオリジナル
2025-01-05 08:48:41647ブラウズ

インスピレーション

今日のマイクロサービス アーキテクチャでは、リバース プロキシは、受信リクエストを管理し、さまざまなバックエンド サービスにルーティングする上で重要な役割を果たしています。

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

リバース プロキシは、アプリケーションの Web サーバーの前に配置され、クライアント マシンからのリクエストをインターセプトします。これには、負荷分散、セキュリティの向上につながるオリジンサーバーの IP アドレスの隠蔽、キャッシュ、レート制限など、多くの利点があります。

分散型マイクロサービス アーキテクチャでは、単一のエントリ ポイントが必要です。 Nginx などのリバース プロキシ サーバーは、このようなシナリオに役立ちます。サーバーの複数のインスタンスを実行している場合、効率的なリクエスト ルーティングの管理と確保は困難になります。この場合、Nginx のようなリバース プロキシが完璧なソリューションです。ドメインを Nginx サーバーの IP アドレスに指定すると、Nginx は、各インスタンスで処理される負荷を処理しながら、構成に従って受信リクエストをいずれかのインスタンスにルーティングします。

Nginx はどのようにして優れているのでしょうか?

Nginx がどのようにして非常に高い信頼性と速度で大規模なリクエストをサポートできるかを詳しく説明した Nginx の記事「Nginx アーキテクチャ」を一読することをお勧めします。

つまり、Nginx にはマスター プロセスと多数のワーカー プロセスがあります。キャッシュ ローダーやキャッシュ マネージャーなどのヘルパー プロセスもあります。マスタープロセスとワーカープロセスはすべての重い作業を実行します。

  • マスター プロセス: 構成を管理し、子プロセスを生成します。
  • キャッシュ ローダー/マネージャー: 最小限のリソースでキャッシュのロードとプルーニングを処理します。
  • ワーカー プロセス: 接続、ディスク I/O、およびアップストリーム通信を管理し、ノンブロッキングかつ独立して実行します。

ワーカー プロセスは複数の接続をノンブロッキングで処理し、コンテキストの切り替えを減らします。これらはシングルスレッドで独立して実行され、キャッシュやセッション データなどの共有リソースに共有メモリを使用します。このアーキテクチャは、Nginx がコンテキスト スイッチの数を減らし、ブロッキング マルチプロセス アーキテクチャよりも高速に速度を上げるのに役立ちます。

これからインスピレーションを得て、マスター プロセスとワーカー プロセスの同じ概念を使用し、ワーカー プロセスごとに数千の接続を処理できる独自のイベント駆動型リバース プロキシ サーバーを実装します。

プロジェクトのアーキテクチャ

リバース プロキシの実装は、次の重要な設計原則に従っています。

  1. 構成主導: すべてのプロキシの動作は YAML 構成ファイルで定義されるため、ルーティング ルールを簡単に変更できます。
  2. 型安全性: TypeScript と Zod スキーマにより、構成の有効性と実行時の型安全性が保証されます。
  3. スケーラビリティ: Node.js クラスター モジュールにより、複数の CPU コアを利用してパフォーマンスを向上させることができます。
  4. モジュール性: 構成、サーバー ロジック、スキーマ検証のための個別のモジュールによる懸念の明確な分離。

プロジェクトの構造

├── config.yaml           # Server configuration
├── src/
│   ├── config-schema.ts  # Configuration validation schemas
│   ├── config.ts         # Configuration parsing logic
│   ├── index.ts         # Application entry point
│   ├── server-schema.ts # Server message schemas
│   └── server.ts        # Core server implementation
└── tsconfig.json        # TypeScript configuration

主要コンポーネント

  1. config.yaml: ポート、ワーカー プロセス、アップストリーム サーバー、ヘッダー、ルーティング ルールなどのサーバーの構成を定義します。
  2. config-schema.ts: Zod ライブラリを使用して検証スキーマを定義し、構成構造が正しいことを確認します。
  3. server-schema.ts: マスター プロセスとワーカー プロセス間で交換されるメッセージ形式を指定します。
  4. config.ts: YAML 構成ファイルを解析および検証するための関数を提供します。
  5. server.ts: クラスターのセットアップ、HTTP 処理、リクエスト転送などのリバース プロキシ サーバー ロジックを実装します。
  6. index.ts: エントリ ポイントとして機能し、コマンドライン オプションを解析してサーバーを開始します。

構成管理

設定システムは YAML を使用します。仕組みは次のとおりです:

server:
    listen: 8080          # Port the server listens on.
    workers: 2            # Number of worker processes to handle requests.
    upstreams:            # Define upstream servers (backend targets).
        - id: jsonplaceholder
          url: jsonplaceholder.typicode.com
        - id: dummy
          url: dummyjson.com
    headers:              # Custom headers added to proxied requests.
        - key: x-forward-for
          value: $ip      # Adds the client IP to the forwarded request.
        - key: Authorization
          value: Bearer xyz  # Adds an authorization token to requests.
    rules:                # Define routing rules for incoming requests.
        - path: /test
          upstreams:
              - dummy     # Routes requests to "/test" to the "dummy" upstream.
        - path: /
          upstreams:
              - jsonplaceholder  # Routes all other requests to "jsonplaceholder".

受信リクエストはルールに照らして評価されます。リバース プロキシは、パスに基づいて、リクエストを転送する上流サーバーを決定します。

構成の検証 (config-schema.ts)

Zod を使用して、構成検証のための厳密なスキーマを定義します。

import { z } from "zod";

const upstreamSchema = z.object({
    id: z.string(),
    url: z.string(),
});

const headerSchema = z.object({
    key: z.string(),
    value: z.string(),
});

const ruleSchema = z.object({
    path: z.string(),
    upstreams: z.array(z.string()),
});

const serverSchema = z.object({
    listen: z.number(),
    workers: z.number().optional(),
    upstreams: z.array(upstreamSchema),
    headers: z.array(headerSchema).optional(),
    rules: z.array(ruleSchema),
});

export const rootConfigSchema = z.object({
    server: serverSchema,
});

export type ConfigSchemaType = z.infer<typeof rootConfigSchema>;

構成の解析と検証 (config.ts)

config.ts モジュールは、構成ファイルを解析および検証するためのユーティリティ関数を提供します。

import fs from "node:fs/promises";
import { parse } from "yaml";
import { rootConfigSchema } from "./config-schema";

export async function parseYAMLConfig(filepath: string) {
    const configFileContent = await fs.readFile(filepath, "utf8");
    const configParsed = parse(configFileContent);
    return JSON.stringify(configParsed);
}

export async function validateConfig(config: string) {
    const validatedConfig = await rootConfigSchema.parseAsync(
        JSON.parse(config)
    );
    return validatedConfig;
}

リバース プロキシ サーバー ロジック (server.ts)

サーバーは、スケーラビリティのために Node.js クラスター モジュールを利用し、リクエストの処理のために http モジュールを利用します。マスター プロセスはリクエストをワーカー プロセスに分散し、ワーカー プロセスがリクエストを上流のサーバーに転送します。 server.ts ファイルを詳しく調べてみましょう。このファイルには、リバース プロキシ サーバーのコア ロジックが含まれています。各コンポーネントを分析し、それらがどのように連携してスケーラブルなプロキシ サーバーを作成するかを理解します。

サーバー実装は、Node.js のクラスター モジュールを使用したマスターワーカー アーキテクチャに従います。この設計により、次のことが可能になります。

  • 複数の CPU コアを利用する
  • リクエストを同時に処理します
  • 高可用性を維持する
  • リクエスト処理を分離する
  1. マスタープロセス:

    • ワーカープロセスを作成します
    • 受信したリクエストをワーカー全体に分散します
    • ワーカープールを管理します
    • ワーカーのクラッシュと再起動を処理します
  2. ワーカープロセス:

    • 個々の HTTP リクエストを処理する
    • リクエストをルーティング ルールと照合します
    • リクエストを上流サーバーに転送します
    • 応答を処理してクライアントに送り返す

マスタープロセスのセットアップ

├── config.yaml           # Server configuration
├── src/
│   ├── config-schema.ts  # Configuration validation schemas
│   ├── config.ts         # Configuration parsing logic
│   ├── index.ts         # Application entry point
│   ├── server-schema.ts # Server message schemas
│   └── server.ts        # Core server implementation
└── tsconfig.json        # TypeScript configuration

マスター プロセスはワーカーのプールを作成し、環境変数を通じて各ワーカーに設定を渡します。これにより、すべてのワーカーが同じ構成にアクセスできるようになります。

リクエスト配布

server:
    listen: 8080          # Port the server listens on.
    workers: 2            # Number of worker processes to handle requests.
    upstreams:            # Define upstream servers (backend targets).
        - id: jsonplaceholder
          url: jsonplaceholder.typicode.com
        - id: dummy
          url: dummyjson.com
    headers:              # Custom headers added to proxied requests.
        - key: x-forward-for
          value: $ip      # Adds the client IP to the forwarded request.
        - key: Authorization
          value: Bearer xyz  # Adds an authorization token to requests.
    rules:                # Define routing rules for incoming requests.
        - path: /test
          upstreams:
              - dummy     # Routes requests to "/test" to the "dummy" upstream.
        - path: /
          upstreams:
              - jsonplaceholder  # Routes all other requests to "jsonplaceholder".

マスター プロセスは、単純なランダム分散戦略を使用してリクエストをワーカーに割り当てます。このアプローチは、ラウンドロビン アルゴリズムや最小接続アルゴリズムほど洗練されていませんが、ほとんどのユース ケースで適切な負荷分散を提供します。リクエスト分散ロジック:

  • プールからワーカーをランダムに選択します
  • ワーカー間でバランスの取れたワークロードを作成します
  • 従業員が不在の可能性がある特殊なケースに対処します

ワーカープロセスリクエストロジック

各ワーカーはメッセージをリッスンし、リクエストをルーティング ルールと照合して、適切な上流サーバーに転送します。

import { z } from "zod";

const upstreamSchema = z.object({
    id: z.string(),
    url: z.string(),
});

const headerSchema = z.object({
    key: z.string(),
    value: z.string(),
});

const ruleSchema = z.object({
    path: z.string(),
    upstreams: z.array(z.string()),
});

const serverSchema = z.object({
    listen: z.number(),
    workers: z.number().optional(),
    upstreams: z.array(upstreamSchema),
    headers: z.array(headerSchema).optional(),
    rules: z.array(ruleSchema),
});

export const rootConfigSchema = z.object({
    server: serverSchema,
});

export type ConfigSchemaType = z.infer<typeof rootConfigSchema>;

マスター プロセスは、Node.js IPC (プロセス間通信) を使用して、必要なすべてのリクエスト情報を含む標準化されたメッセージ ペイロードを構築し、Zod スキーマを使用してメッセージ構造を検証することにより、ワーカーと通信します。

ワーカーは実際のリクエストの処理とプロキシを処理します。各ワーカー:

  • 環境変数から設定をロードします
  • Zod スキーマを使用して構成を検証します
  • 構成の独自のコピーを維持します

ワーカーは次の方法で上流サーバーを選択します:

  • ルールから適切なアップストリーム ID を見つける
  • アップストリームサーバー構成の検索
  • 上流サーバーの存在を検証しています

リクエスト転送メカニズム:

  • 上流サーバーへの新しい HTTP リクエストを作成します
  • 応答データをストリーミングします
  • レスポンスボディを集約します
  • マスタープロセスに応答を送り返します

サーバーの実行

サーバーを実行するには、次の手順に従います:

  1. プロジェクトをビルドします:
import fs from "node:fs/promises";
import { parse } from "yaml";
import { rootConfigSchema } from "./config-schema";

export async function parseYAMLConfig(filepath: string) {
    const configFileContent = await fs.readFile(filepath, "utf8");
    const configParsed = parse(configFileContent);
    return JSON.stringify(configParsed);
}

export async function validateConfig(config: string) {
    const validatedConfig = await rootConfigSchema.parseAsync(
        JSON.parse(config)
    );
    return validatedConfig;
}
  1. サーバーを起動します:
if (cluster.isPrimary) {
    console.log("Master Process is up ?");
    for (let i = 0; i < workerCount; i++) {
        const w = cluster.fork({ config: JSON.stringify(config) });
        WORKER_POOL.push(w);
        console.log(Master Process: Worker Node spinned: ${i});
    }

    const server = http.createServer((req, res) => {
        const index = Math.floor(Math.random() * WORKER_POOL.length);
        const worker = WORKER_POOL.at(index);

        if (!worker) throw new Error("Worker not found.");

        const payload: WorkerMessageSchemaType = {
            requestType: "HTTP",
            headers: req.headers,
            body: null,
            url: ${req.url},
        };
        worker.send(JSON.stringify(payload));

        worker.once("message", async (workerReply: string) => {
            const reply = await workerMessageReplySchema.parseAsync(
                JSON.parse(workerReply)
            );

            if (reply.errorCode) {
                res.writeHead(parseInt(reply.errorCode));
                res.end(reply.error);
            } else {
                res.writeHead(200);
                res.end(reply.data);
            }
        });
    });

    server.listen(port, () => {
        console.log(Reverse Proxy listening on port: ${port});
    });
}
  1. 開発モード:
const server = http.createServer(function (req, res) {
    const index = Math.floor(Math.random() * WORKER_POOL.length);
    const worker = WORKER_POOL.at(index);

    const payload: WorkerMessageSchemaType = {
        requestType: "HTTP",
        headers: req.headers,
        body: null,
        url: ${req.url},
    };
    worker.send(JSON.stringify(payload));
});

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

上のスクリーンショットでは、1 つのマスター ノードと 2 つのワーカー プロセスが実行されていることがわかります。リバース プロキシ サーバーはポート 8080 でリッスンしています。
config.yaml ファイルには、jsonplaceholder と dummy という 2 つの上流サーバーを記述します。サーバーに届くすべてのリクエストを jsonplaceholder にルーティングしたい場合は、ルールを次のようにします:

├── config.yaml           # Server configuration
├── src/
│   ├── config-schema.ts  # Configuration validation schemas
│   ├── config.ts         # Configuration parsing logic
│   ├── index.ts         # Application entry point
│   ├── server-schema.ts # Server message schemas
│   └── server.ts        # Core server implementation
└── tsconfig.json        # TypeScript configuration

同様に、/test エンドポイントへのリクエストをダミーの上流サーバーにルーティングする必要がある場合は、ルールを次のように設定します。

server:
    listen: 8080          # Port the server listens on.
    workers: 2            # Number of worker processes to handle requests.
    upstreams:            # Define upstream servers (backend targets).
        - id: jsonplaceholder
          url: jsonplaceholder.typicode.com
        - id: dummy
          url: dummyjson.com
    headers:              # Custom headers added to proxied requests.
        - key: x-forward-for
          value: $ip      # Adds the client IP to the forwarded request.
        - key: Authorization
          value: Bearer xyz  # Adds an authorization token to requests.
    rules:                # Define routing rules for incoming requests.
        - path: /test
          upstreams:
              - dummy     # Routes requests to "/test" to the "dummy" upstream.
        - path: /
          upstreams:
              - jsonplaceholder  # Routes all other requests to "jsonplaceholder".

これをテストしてみましょう!

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

うわー、すごいですね! localhost:8080 に移動していますが、応答として、jsonplaceholder.typicode.com のホームページを受信したことがわかります。エンド ユーザーは、別のサーバーからの応答が表示されていることさえ知りません。このため、リバース プロキシ サーバーが重要です。同じコードを実行する複数のサーバーがあり、それらのすべてのポートをエンド ユーザーに公開したくない場合は、抽象化レイヤーとしてリバース プロキシを使用します。ユーザーは、非常に堅牢で高速なサーバーであるリバース プロキシ サーバーにアクセスし、要求をルーティングするサーバーを決定します。

ここで localhost:8080/todos にアクセスして、何が起こるか見てみましょう。

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

私たちのリクエストは再び jsonplaceholder サーバーにリバース プロキシされ、解決された URL: jsonplaceholder.typicode.com/todos から JSON レスポンスを受け取りました。

通信の流れ

完全なリクエスト フローを視覚化してみましょう:

クライアントがリクエストを送信 → マスタープロセス
マスタープロセス → 選択されたワーカー
ワーカー → 上流サーバー
上流サーバー → ワーカー
ワーカー → マスタープロセス
マスタープロセス → クライアント

パフォーマンスに関する考慮事項

マルチプロセス アーキテクチャにより、いくつかのパフォーマンス上の利点が得られます。

  1. CPU 使用率: ワーカー プロセスは、利用可能なハードウェア リソースを利用して、さまざまな CPU コアで実行できます。
  2. プロセスの分離: 1 つのワーカーでクラッシュしても他のワーカーに影響を与えず、信頼性が向上します。
  3. 負荷分散: リクエストをランダムに分散することで、単一のワーカーが過負荷になるのを防ぎます。

今後の改善点

現在の実装は機能しますが、次のように拡張できます。

  1. 負荷分散の改善: ラウンドロビンや最小接続など、より高度なアルゴリズムを実装します。
  2. ヘルスチェック: 上流サーバーの定期的なヘルスチェックを追加します。
  3. キャッシュ: 応答キャッシュを実装して、上流サーバーの負荷を軽減します。
  4. メトリクス: 監視用にプロメテウススタイルのメトリクスを追加します。
  5. WebSocket サポート: WebSocket 接続を処理するためにプロキシを拡張します。
  6. HTTPS サポート: SSL/TLS 終了機能を追加します。

まとめ

リバース プロキシ サーバーをゼロから構築するのは、最初は恐ろしいように思えるかもしれませんが、これまで調べてきたように、やりがいのある経験です。 Node.js クラスター、TypeScript、YAML ベースの構成管理を組み合わせることで、Nginx からインスピレーションを得たスケーラブルで効率的なシステムを作成しました。

この実装にはまだ改善の余地があり、負荷分散、キャッシュ、WebSocket サポートの改善などは検討すべきアイデアのほんの一部です。しかし、現在の設計は、さらなる実験と拡張のための強力な基盤を確立します。ここまで進めていただければ、リバース プロキシについてさらに深く掘り下げたり、ニーズに合わせたカスタム ソリューションの構築を開始したりできるようになりました。

私の作品につながりたい場合、または私の作品をもっと見たい場合は、私の GitHub や LinkedIn をチェックしてください。
このプロジェクトのリポジトリはここにあります。

ご意見、フィードバック、改善のアイデアをお聞かせください。読んでいただきありがとうございます。コーディングを楽しんでください。 ?

以上がNode.js と TypeScript を使用して Nginx のようなスケーラブルなリバース プロキシ サーバーを構築するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。