Heim >Web-Frontend >js-Tutorial >Erstellen eines skalierbaren Reverse-Proxy-Servers wie Nginx mit Node.js und TypeScript
In der heutigen Microservices-Architektur spielen Reverse-Proxys eine entscheidende Rolle bei der Verwaltung und Weiterleitung eingehender Anfragen an verschiedene Backend-Dienste.
Ein Reverse-Proxy sitzt vor den Webservern einer Anwendung und fängt die Anfragen ab, die von den Client-Rechnern kommen. Dies hat viele Vorteile wie Lastausgleich, versteckte IP-Adressen der Ursprungsserver, was zu besserer Sicherheit, Caching, Ratenbegrenzung usw. führt.
In einer verteilten Microservice-Architektur ist ein einziger Einstiegspunkt erforderlich. Reverse-Proxy-Server wie Nginx helfen in solchen Szenarien. Wenn mehrere Instanzen unseres Servers ausgeführt werden, wird die Verwaltung und Sicherstellung einer effizienten Anforderungsweiterleitung schwierig. Ein Reverse-Proxy wie Nginx ist in diesem Fall eine perfekte Lösung. Wir können unsere Domain auf die IP-Adresse des Nginx-Servers verweisen und Nginx leitet die eingehende Anfrage entsprechend der Konfiguration an eine der Instanzen weiter und kümmert sich dabei um die Last, die von jeder Instanz verarbeitet wird.
Ich empfehle die Lektüre dieses Artikels von Nginx, der ausführlich erklärt, wie Nginx eine große Anzahl von Anfragen mit höchster Zuverlässigkeit und Geschwindigkeit unterstützen kann: Nginx-Architektur
Kurz gesagt, Nginx verfügt über einen Master-Prozess und eine Reihe von Worker-Prozessen. Es verfügt auch über Hilfsprozesse wie Cache Loader und Cache Manager. Der Master und der Worker-Prozess erledigen die ganze schwere Arbeit.
Arbeitsprozesse verarbeiten mehrere Verbindungen nicht blockierend und reduzieren so Kontextwechsel. Sie sind Single-Threaded, laufen unabhängig und nutzen gemeinsam genutzten Speicher für gemeinsam genutzte Ressourcen wie Cache und Sitzungsdaten. Diese Architektur hilft Nginx, die Anzahl der Kontextwechsel zu reduzieren und die Geschwindigkeit schneller zu erhöhen als eine blockierende Multiprozessarchitektur.
Inspiriert davon werden wir das gleiche Konzept von Master- und Worker-Prozessen verwenden und unseren eigenen ereignisgesteuerten Reverse-Proxy-Server implementieren, der Tausende von Verbindungen pro Worker-Prozess verarbeiten kann.
Unsere Reverse-Proxy-Implementierung folgt diesen wichtigen Designprinzipien:
├── 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
Das Konfigurationssystem verwendet YAML. So funktioniert es:
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".
Eingehende Anfragen werden anhand der Regeln bewertet. Basierend auf dem Pfad bestimmt der Reverse-Proxy, an welchen Upstream-Server die Anfrage weitergeleitet werden soll.
Wir verwenden Zod, um strenge Schemata für die Konfigurationsvalidierung zu definieren:
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>;
Das config.ts-Modul bietet Hilfsfunktionen zum Parsen und Validieren der Konfigurationsdatei.
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; }
Der Server nutzt das Node.js-Clustermodul für Skalierbarkeit und das http-Modul für die Bearbeitung von Anfragen. Der Master-Prozess verteilt Anfragen an Worker-Prozesse, die diese an Upstream-Server weiterleiten. Lassen Sie uns die Datei server.ts im Detail untersuchen, die die Kernlogik unseres Reverse-Proxy-Servers enthält. Wir werden jede Komponente aufschlüsseln und verstehen, wie sie zusammenarbeitet, um einen skalierbaren Proxyserver zu erstellen.
Die Serverimplementierung folgt einer Master-Worker-Architektur unter Verwendung des Node.js-Cluster-Moduls. Dieses Design ermöglicht uns:
Masterprozess:
Arbeiterprozesse:
├── 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
Der Masterprozess erstellt einen Pool von Workern und übergibt die Konfiguration über Umgebungsvariablen an jeden Worker. Dadurch wird sichergestellt, dass alle Mitarbeiter Zugriff auf dieselbe Konfiguration haben.
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".
Der Masterprozess verwendet eine einfache Zufallsverteilungsstrategie, um Anfragen den Arbeitern zuzuweisen. Dieser Ansatz ist zwar nicht so ausgefeilt wie Round-Robin- oder Least-Connections-Algorithmen, bietet aber für die meisten Anwendungsfälle eine angemessene Lastverteilung. Die Anforderungsverteilungslogik:
Jeder Worker lauscht auf Nachrichten, gleicht Anfragen mit Routing-Regeln ab und leitet sie an den entsprechenden Upstream-Server weiter.
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>;
Der Masterprozess kommuniziert mit den Arbeitern, indem er mithilfe von Node.js IPC (Inter-Process Communication) eine standardisierte Nachrichtennutzlast einschließlich aller erforderlichen Anforderungsinformationen erstellt und die Nachrichtenstruktur mithilfe von Zod-Schemas validiert.
Mitarbeiter kümmern sich um die eigentliche Anforderungsverarbeitung und Weiterleitung. Jeder Arbeiter:
Mitarbeiter wählen Upstream-Server aus durch:
Der Mechanismus zur Anforderungsweiterleitung:
Um den Server auszuführen, befolgen Sie diese Schritte:
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)); });
Im obigen Screenshot können wir sehen, dass 1 Master-Knoten und 2 Worker-Prozesse ausgeführt werden. Unser Reverse-Proxy-Server überwacht Port 8080.
In der Datei config.yaml beschreiben wir zwei Upstream-Server, nämlich: jsonplaceholder und dummy. Wenn wir möchten, dass alle an unseren Server eingehenden Anfragen an jsonplaceholder weitergeleitet werden, geben wir die Regel wie folgt ein:
├── 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
Wenn wir möchten, dass unsere Anfrage an den Endpunkt /test an unseren Dummy-Upstream-Server weitergeleitet wird, geben wir die Regel wie folgt ein:
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".
Lass uns das testen!
Wow, das ist cool! Wir navigieren zu „localhost:8080“, sehen aber als Antwort, dass wir die Homepage für „jsonplaceholder.typicode.com“ erhalten haben. Der Endbenutzer weiß nicht einmal, dass wir eine Antwort von einem separaten Server sehen. Deshalb sind Reverse-Proxy-Server wichtig. Wenn auf mehreren Servern derselbe Code ausgeführt wird und nicht alle Ports den Endbenutzern zugänglich gemacht werden sollen, verwenden Sie einen Reverse-Proxy als Abstraktionsschicht. Benutzer greifen auf den Reverse-Proxy-Server zu, einen sehr robusten und schnellen Server, und dieser bestimmt, an welchen Server die Anfrage weitergeleitet werden soll.
Lassen Sie uns jetzt „localhost:8080/todos“ aufrufen und sehen, was passiert.
Unsere Anfrage wurde erneut rückwärts an den JSONPlaceholder-Server weitergeleitet und erhielt eine JSON-Antwort von der aufgelösten URL: jsonplaceholder.typicode.com/todos.
Lassen Sie uns den gesamten Anfrageablauf visualisieren:
Kunde sendet Anfrage → Masterprozess
Master-Prozess → Ausgewählter Arbeiter
Arbeiter → Upstream-Server
Upstream-Server → Worker
Arbeiter → Masterprozess
Masterprozess → Kunde
Die Multiprozessarchitektur bietet mehrere Leistungsvorteile:
Die aktuelle Implementierung ist zwar funktionsfähig, könnte jedoch um Folgendes erweitert werden:
Das Erstellen eines Reverse-Proxy-Servers von Grund auf mag auf den ersten Blick einschüchternd wirken, aber wie wir herausgefunden haben, ist es eine lohnende Erfahrung. Durch die Kombination von Node.js-Clustern, TypeScript und YAML-basiertem Konfigurationsmanagement haben wir ein skalierbares und effizientes System geschaffen, das von Nginx inspiriert ist.
Es gibt noch Raum für Verbesserungen dieser Implementierung – besserer Lastausgleich, Caching oder WebSocket-Unterstützung sind nur einige Ideen, die es zu erkunden gilt. Aber das aktuelle Design bildet eine solide Grundlage für Experimente und eine weitere Skalierung. Wenn Sie mitgemacht haben, sind Sie jetzt in der Lage, tiefer in Reverse-Proxys einzutauchen oder sogar mit der Entwicklung maßgeschneiderter Lösungen zu beginnen, die auf Ihre Bedürfnisse zugeschnitten sind.
Wenn Sie sich vernetzen oder mehr von meiner Arbeit sehen möchten, schauen Sie sich meinen GitHub und LinkedIn an.
Das Repository für dieses Projekt finden Sie hier.
Ich würde gerne Ihre Gedanken, Ihr Feedback oder Ihre Verbesserungsvorschläge hören. Vielen Dank fürs Lesen und viel Spaß beim Codieren! ?
Das obige ist der detaillierte Inhalt vonErstellen eines skalierbaren Reverse-Proxy-Servers wie Nginx mit Node.js und TypeScript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!