在當今的微服務架構中,反向代理在管理傳入請求並將其路由到各種後端服務方面發揮著至關重要的作用。
反向代理位於應用程式的 Web 伺服器前面,攔截來自客戶端電腦的請求。這有許多好處,例如負載平衡、隱藏來源伺服器 IP 位址,從而提高安全性、快取、速率限制等。
在分散式微服務架構中,單一入口點是必要的。像 Nginx 這樣的反向代理伺服器可以在這種情況下提供協助。如果我們有多個伺服器實例在運行,管理和確保有效的請求路由就會變得很棘手。在這種情況下,像 Nginx 這樣的反向代理程式是一個完美的解決方案。我們可以將網域名稱指向 Nginx 伺服器的 IP 位址,Nginx 將根據配置將傳入請求路由到其中一個實例,同時處理每個實例處理的負載。
我建議閱讀 Nginx 的這篇文章,它詳細解釋了 Nginx 如何以超強的可靠性和速度支持大規模的請求:Nginx 架構
簡單來說,Nginx有一個Master進程和一堆worker進程。它還具有快取載入器和快取管理器等輔助進程。主進程和工作進程完成所有繁重的工作。
工作進程以非阻塞方式處理多個連接,從而減少上下文切換。它們是單線程的,獨立運行,並使用共享記憶體來共享快取和會話資料等資源。這種架構幫助 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 的 cluster 模組的主從架構。這種設計使我們能夠:
主流程:
工作進程:
├── 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
master程序建立一個worker池,並透過環境變數將配置傳遞給每個worker。這確保所有工作人員都可以存取相同的配置。
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".
master程序使用簡單的隨機分配策略將請求指派給worker。雖然不像循環演算法或最少連接演算法那麼複雜,但這種方法為大多數用例提供了良好的負載分配。請求分發邏輯:
每個工作人員都會偵聽訊息,根據路由規則匹配請求,並將它們轉送到適當的上游伺服器。
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。如果我們希望所有到達我們伺服器的請求都路由到 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 收到 JSON 回應:jsonplaceholder.typicode.com/todos。
讓我們可視化完整的請求流程:
客戶端發送請求→主程序
主流程→選定的工人
Worker → 上游伺服器
上游伺服器 → Worker
工人 → 主進程
主流程 → 客戶端
多進程架構提供了多種效能優勢:
雖然功能正常,但目前的實作可以透過以下方式增強:
從頭開始建立反向代理伺服器一開始可能看起來很嚇人,但正如我們所探索的,這是一次有益的體驗。透過結合 Node.js 叢集、TypeScript 和基於 YAML 的設定管理,我們創建了一個受 Nginx 啟發的可擴展且高效的系統。
此實作仍有增強的空間 - 更好的負載平衡、快取或 WebSocket 支援只是一些需要探索的想法。但目前的設計為進一步實驗和擴展奠定了堅實的基礎。如果您已經按照要求進行操作,那麼您現在就可以更深入地研究反向代理,甚至可以開始建立適合您需求的自訂解決方案。
如果您想聯絡或查看我的更多作品,請查看我的 GitHub、LinkedIn。
該項目的存儲庫可以在這裡找到。
我很想聽聽您的想法、回饋或改進想法。感謝您的閱讀,祝您編碼愉快! ?
以上是使用 Node.js 和 TypeScript 建立可擴充的反向代理伺服器,例如 Nginx的詳細內容。更多資訊請關注PHP中文網其他相關文章!