ホームページ >ウェブフロントエンド >jsチュートリアル >Node.js と TypeScript を使用して Nginx のようなスケーラブルなリバース プロキシ サーバーを構築する
今日のマイクロサービス アーキテクチャでは、リバース プロキシは、受信リクエストを管理し、さまざまなバックエンド サービスにルーティングする上で重要な役割を果たしています。
リバース プロキシは、アプリケーションの Web サーバーの前に配置され、クライアント マシンからのリクエストをインターセプトします。これには、負荷分散、セキュリティの向上につながるオリジンサーバーの IP アドレスの隠蔽、キャッシュ、レート制限など、多くの利点があります。
分散型マイクロサービス アーキテクチャでは、単一のエントリ ポイントが必要です。 Nginx などのリバース プロキシ サーバーは、このようなシナリオに役立ちます。サーバーの複数のインスタンスを実行している場合、効率的なリクエスト ルーティングの管理と確保は困難になります。この場合、Nginx のようなリバース プロキシが完璧なソリューションです。ドメインを Nginx サーバーの IP アドレスに指定すると、Nginx は、各インスタンスで処理される負荷を処理しながら、構成に従って受信リクエストをいずれかのインスタンスにルーティングします。
Nginx がどのようにして非常に高い信頼性と速度で大規模なリクエストをサポートできるかを詳しく説明した Nginx の記事「Nginx アーキテクチャ」を一読することをお勧めします。
つまり、Nginx にはマスター プロセスと多数のワーカー プロセスがあります。キャッシュ ローダーやキャッシュ マネージャーなどのヘルパー プロセスもあります。マスタープロセスとワーカープロセスはすべての重い作業を実行します。
ワーカー プロセスは複数の接続をノンブロッキングで処理し、コンテキストの切り替えを減らします。これらはシングルスレッドで独立して実行され、キャッシュやセッション データなどの共有リソースに共有メモリを使用します。このアーキテクチャは、Nginx がコンテキスト スイッチの数を減らし、ブロッキング マルチプロセス アーキテクチャよりも高速に速度を上げるのに役立ちます。
これからインスピレーションを得て、マスター プロセスとワーカー プロセスの同じ概念を使用し、ワーカー プロセスごとに数千の接続を処理できる独自のイベント駆動型リバース プロキシ サーバーを実装します。
リバース プロキシの実装は、次の重要な設計原則に従っています。
├── 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
設定システムは 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".
受信リクエストはルールに照らして評価されます。リバース プロキシは、パスに基づいて、リクエストを転送する上流サーバーを決定します。
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 モジュールは、構成ファイルを解析および検証するためのユーティリティ関数を提供します。
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; }
サーバーは、スケーラビリティのために Node.js クラスター モジュールを利用し、リクエストの処理のために http モジュールを利用します。マスター プロセスはリクエストをワーカー プロセスに分散し、ワーカー プロセスがリクエストを上流のサーバーに転送します。 server.ts ファイルを詳しく調べてみましょう。このファイルには、リバース プロキシ サーバーのコア ロジックが含まれています。各コンポーネントを分析し、それらがどのように連携してスケーラブルなプロキシ サーバーを作成するかを理解します。
サーバー実装は、Node.js のクラスター モジュールを使用したマスターワーカー アーキテクチャに従います。この設計により、次のことが可能になります。
マスタープロセス:
ワーカープロセス:
├── 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 スキーマを使用してメッセージ構造を検証することにより、ワーカーと通信します。
ワーカーは実際のリクエストの処理とプロキシを処理します。各ワーカー:
ワーカーは次の方法で上流サーバーを選択します:
リクエスト転送メカニズム:
サーバーを実行するには、次の手順に従います:
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; }
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}); }); }
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)); });
上のスクリーンショットでは、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".
これをテストしてみましょう!
うわー、すごいですね! localhost:8080 に移動していますが、応答として、jsonplaceholder.typicode.com のホームページを受信したことがわかります。エンド ユーザーは、別のサーバーからの応答が表示されていることさえ知りません。このため、リバース プロキシ サーバーが重要です。同じコードを実行する複数のサーバーがあり、それらのすべてのポートをエンド ユーザーに公開したくない場合は、抽象化レイヤーとしてリバース プロキシを使用します。ユーザーは、非常に堅牢で高速なサーバーであるリバース プロキシ サーバーにアクセスし、要求をルーティングするサーバーを決定します。
ここで localhost:8080/todos にアクセスして、何が起こるか見てみましょう。
私たちのリクエストは再び jsonplaceholder サーバーにリバース プロキシされ、解決された URL: jsonplaceholder.typicode.com/todos から JSON レスポンスを受け取りました。
完全なリクエスト フローを視覚化してみましょう:
クライアントがリクエストを送信 → マスタープロセス
マスタープロセス → 選択されたワーカー
ワーカー → 上流サーバー
上流サーバー → ワーカー
ワーカー → マスタープロセス
マスタープロセス → クライアント
マルチプロセス アーキテクチャにより、いくつかのパフォーマンス上の利点が得られます。
現在の実装は機能しますが、次のように拡張できます。
リバース プロキシ サーバーをゼロから構築するのは、最初は恐ろしいように思えるかもしれませんが、これまで調べてきたように、やりがいのある経験です。 Node.js クラスター、TypeScript、YAML ベースの構成管理を組み合わせることで、Nginx からインスピレーションを得たスケーラブルで効率的なシステムを作成しました。
この実装にはまだ改善の余地があり、負荷分散、キャッシュ、WebSocket サポートの改善などは検討すべきアイデアのほんの一部です。しかし、現在の設計は、さらなる実験と拡張のための強力な基盤を確立します。ここまで進めていただければ、リバース プロキシについてさらに深く掘り下げたり、ニーズに合わせたカスタム ソリューションの構築を開始したりできるようになりました。
私の作品につながりたい場合、または私の作品をもっと見たい場合は、私の GitHub や LinkedIn をチェックしてください。
このプロジェクトのリポジトリはここにあります。
ご意見、フィードバック、改善のアイデアをお聞かせください。読んでいただきありがとうございます。コーディングを楽しんでください。 ?
以上がNode.js と TypeScript を使用して Nginx のようなスケーラブルなリバース プロキシ サーバーを構築するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。