Node.js と TypeScript を使用して Nginx のようなスケーラブルなリバース プロキシ サーバーを構築する
インスピレーション
今日のマイクロサービス アーキテクチャでは、リバース プロキシは、受信リクエストを管理し、さまざまなバックエンド サービスにルーティングする上で重要な役割を果たしています。
リバース プロキシは、アプリケーションの Web サーバーの前に配置され、クライアント マシンからのリクエストをインターセプトします。これには、負荷分散、セキュリティの向上につながるオリジンサーバーの IP アドレスの隠蔽、キャッシュ、レート制限など、多くの利点があります。
分散型マイクロサービス アーキテクチャでは、単一のエントリ ポイントが必要です。 Nginx などのリバース プロキシ サーバーは、このようなシナリオに役立ちます。サーバーの複数のインスタンスを実行している場合、効率的なリクエスト ルーティングの管理と確保は困難になります。この場合、Nginx のようなリバース プロキシが完璧なソリューションです。ドメインを Nginx サーバーの IP アドレスに指定すると、Nginx は、各インスタンスで処理される負荷を処理しながら、構成に従って受信リクエストをいずれかのインスタンスにルーティングします。
Nginx はどのようにして優れているのでしょうか?
Nginx がどのようにして非常に高い信頼性と速度で大規模なリクエストをサポートできるかを詳しく説明した Nginx の記事「Nginx アーキテクチャ」を一読することをお勧めします。
つまり、Nginx にはマスター プロセスと多数のワーカー プロセスがあります。キャッシュ ローダーやキャッシュ マネージャーなどのヘルパー プロセスもあります。マスタープロセスとワーカープロセスはすべての重い作業を実行します。
- マスター プロセス: 構成を管理し、子プロセスを生成します。
- キャッシュ ローダー/マネージャー: 最小限のリソースでキャッシュのロードとプルーニングを処理します。
- ワーカー プロセス: 接続、ディスク I/O、およびアップストリーム通信を管理し、ノンブロッキングかつ独立して実行します。
ワーカー プロセスは複数の接続をノンブロッキングで処理し、コンテキストの切り替えを減らします。これらはシングルスレッドで独立して実行され、キャッシュやセッション データなどの共有リソースに共有メモリを使用します。このアーキテクチャは、Nginx がコンテキスト スイッチの数を減らし、ブロッキング マルチプロセス アーキテクチャよりも高速に速度を上げるのに役立ちます。
これからインスピレーションを得て、マスター プロセスとワーカー プロセスの同じ概念を使用し、ワーカー プロセスごとに数千の接続を処理できる独自のイベント駆動型リバース プロキシ サーバーを実装します。
プロジェクトのアーキテクチャ
リバース プロキシの実装は、次の重要な設計原則に従っています。
- 構成主導: すべてのプロキシの動作は YAML 構成ファイルで定義されるため、ルーティング ルールを簡単に変更できます。
- 型安全性: TypeScript と Zod スキーマにより、構成の有効性と実行時の型安全性が保証されます。
- スケーラビリティ: Node.js クラスター モジュールにより、複数の CPU コアを利用してパフォーマンスを向上させることができます。
- モジュール性: 構成、サーバー ロジック、スキーマ検証のための個別のモジュールによる懸念の明確な分離。
プロジェクトの構造
├── 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
主要コンポーネント
- config.yaml: ポート、ワーカー プロセス、アップストリーム サーバー、ヘッダー、ルーティング ルールなどのサーバーの構成を定義します。
- config-schema.ts: Zod ライブラリを使用して検証スキーマを定義し、構成構造が正しいことを確認します。
- server-schema.ts: マスター プロセスとワーカー プロセス間で交換されるメッセージ形式を指定します。
- config.ts: YAML 構成ファイルを解析および検証するための関数を提供します。
- server.ts: クラスターのセットアップ、HTTP 処理、リクエスト転送などのリバース プロキシ サーバー ロジックを実装します。
- 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 コアを利用する
- リクエストを同時に処理します
- 高可用性を維持する
- リクエスト処理を分離する
-
マスタープロセス:
- ワーカープロセスを作成します
- 受信したリクエストをワーカー全体に分散します
- ワーカープールを管理します
- ワーカーのクラッシュと再起動を処理します
-
ワーカープロセス:
- 個々の 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 リクエストを作成します
- 応答データをストリーミングします
- レスポンスボディを集約します
- マスタープロセスに応答を送り返します
サーバーの実行
サーバーを実行するには、次の手順に従います:
- プロジェクトをビルドします:
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 レスポンスを受け取りました。
通信の流れ
完全なリクエスト フローを視覚化してみましょう:
クライアントがリクエストを送信 → マスタープロセス
マスタープロセス → 選択されたワーカー
ワーカー → 上流サーバー
上流サーバー → ワーカー
ワーカー → マスタープロセス
マスタープロセス → クライアント
パフォーマンスに関する考慮事項
マルチプロセス アーキテクチャにより、いくつかのパフォーマンス上の利点が得られます。
- CPU 使用率: ワーカー プロセスは、利用可能なハードウェア リソースを利用して、さまざまな CPU コアで実行できます。
- プロセスの分離: 1 つのワーカーでクラッシュしても他のワーカーに影響を与えず、信頼性が向上します。
- 負荷分散: リクエストをランダムに分散することで、単一のワーカーが過負荷になるのを防ぎます。
今後の改善点
現在の実装は機能しますが、次のように拡張できます。
- 負荷分散の改善: ラウンドロビンや最小接続など、より高度なアルゴリズムを実装します。
- ヘルスチェック: 上流サーバーの定期的なヘルスチェックを追加します。
- キャッシュ: 応答キャッシュを実装して、上流サーバーの負荷を軽減します。
- メトリクス: 監視用にプロメテウススタイルのメトリクスを追加します。
- WebSocket サポート: WebSocket 接続を処理するためにプロキシを拡張します。
- HTTPS サポート: SSL/TLS 終了機能を追加します。
まとめ
リバース プロキシ サーバーをゼロから構築するのは、最初は恐ろしいように思えるかもしれませんが、これまで調べてきたように、やりがいのある経験です。 Node.js クラスター、TypeScript、YAML ベースの構成管理を組み合わせることで、Nginx からインスピレーションを得たスケーラブルで効率的なシステムを作成しました。
この実装にはまだ改善の余地があり、負荷分散、キャッシュ、WebSocket サポートの改善などは検討すべきアイデアのほんの一部です。しかし、現在の設計は、さらなる実験と拡張のための強力な基盤を確立します。ここまで進めていただければ、リバース プロキシについてさらに深く掘り下げたり、ニーズに合わせたカスタム ソリューションの構築を開始したりできるようになりました。
私の作品につながりたい場合、または私の作品をもっと見たい場合は、私の GitHub や LinkedIn をチェックしてください。
このプロジェクトのリポジトリはここにあります。
ご意見、フィードバック、改善のアイデアをお聞かせください。読んでいただきありがとうございます。コーディングを楽しんでください。 ?
以上がNode.js と TypeScript を使用して Nginx のようなスケーラブルなリバース プロキシ サーバーを構築するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中国語版
中国語版、とても使いやすい

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

ホットトピック











Pythonは、スムーズな学習曲線と簡潔な構文を備えた初心者により適しています。 JavaScriptは、急な学習曲線と柔軟な構文を備えたフロントエンド開発に適しています。 1。Python構文は直感的で、データサイエンスやバックエンド開発に適しています。 2。JavaScriptは柔軟で、フロントエンドおよびサーバー側のプログラミングで広く使用されています。

C/CからJavaScriptへのシフトには、動的なタイピング、ゴミ収集、非同期プログラミングへの適応が必要です。 1)C/Cは、手動メモリ管理を必要とする静的に型付けられた言語であり、JavaScriptは動的に型付けされ、ごみ収集が自動的に処理されます。 2)C/Cはマシンコードにコンパイルする必要がありますが、JavaScriptは解釈言語です。 3)JavaScriptは、閉鎖、プロトタイプチェーン、約束などの概念を導入します。これにより、柔軟性と非同期プログラミング機能が向上します。

Web開発におけるJavaScriptの主な用途には、クライアントの相互作用、フォーム検証、非同期通信が含まれます。 1)DOM操作による動的なコンテンツの更新とユーザーインタラクション。 2)ユーザーエクスペリエンスを改善するためにデータを提出する前に、クライアントの検証が実行されます。 3)サーバーとのリフレッシュレス通信は、AJAXテクノロジーを通じて達成されます。

現実世界でのJavaScriptのアプリケーションには、フロントエンドとバックエンドの開発が含まれます。 1)DOM操作とイベント処理を含むTODOリストアプリケーションを構築して、フロントエンドアプリケーションを表示します。 2)node.jsを介してRestfulapiを構築し、バックエンドアプリケーションをデモンストレーションします。

JavaScriptエンジンが内部的にどのように機能するかを理解することは、開発者にとってより効率的なコードの作成とパフォーマンスのボトルネックと最適化戦略の理解に役立つためです。 1)エンジンのワークフローには、3つの段階が含まれます。解析、コンパイル、実行。 2)実行プロセス中、エンジンはインラインキャッシュや非表示クラスなどの動的最適化を実行します。 3)ベストプラクティスには、グローバル変数の避け、ループの最適化、constとletsの使用、閉鎖の過度の使用の回避が含まれます。

PythonとJavaScriptには、コミュニティ、ライブラリ、リソースの観点から、独自の利点と短所があります。 1)Pythonコミュニティはフレンドリーで初心者に適していますが、フロントエンドの開発リソースはJavaScriptほど豊富ではありません。 2)Pythonはデータサイエンスおよび機械学習ライブラリで強力ですが、JavaScriptはフロントエンド開発ライブラリとフレームワークで優れています。 3)どちらも豊富な学習リソースを持っていますが、Pythonは公式文書から始めるのに適していますが、JavaScriptはMDNWebDocsにより優れています。選択は、プロジェクトのニーズと個人的な関心に基づいている必要があります。

開発環境におけるPythonとJavaScriptの両方の選択が重要です。 1)Pythonの開発環境には、Pycharm、Jupyternotebook、Anacondaが含まれます。これらは、データサイエンスと迅速なプロトタイピングに適しています。 2)JavaScriptの開発環境には、フロントエンドおよびバックエンド開発に適したnode.js、vscode、およびwebpackが含まれます。プロジェクトのニーズに応じて適切なツールを選択すると、開発効率とプロジェクトの成功率が向上する可能性があります。

CとCは、主に通訳者とJITコンパイラを実装するために使用されるJavaScriptエンジンで重要な役割を果たします。 1)cは、JavaScriptソースコードを解析し、抽象的な構文ツリーを生成するために使用されます。 2)Cは、Bytecodeの生成と実行を担当します。 3)Cは、JITコンパイラを実装し、実行時にホットスポットコードを最適化およびコンパイルし、JavaScriptの実行効率を大幅に改善します。
