


Erstellen eines skalierbaren Reverse-Proxy-Servers wie Nginx mit Node.js und TypeScript
Jan 05, 2025 am 08:48 AMDie Inspiration
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.
Warum macht Nginx das so gut?
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.
- Master-Prozess: Verwaltet die Konfiguration und erzeugt untergeordnete Prozesse.
- Cache Loader/Manager: Erledigt das Laden und Bereinigen des Caches mit minimalen Ressourcen.
- Arbeitsprozesse: Verwalten Sie Verbindungen, Festplatten-E/A und Upstream-Kommunikation und laufen Sie nicht blockierend und unabhängig.
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.
Projektarchitektur
Unsere Reverse-Proxy-Implementierung folgt diesen wichtigen Designprinzipien:
- Konfigurationsgesteuert: Das gesamte Proxy-Verhalten ist in einer YAML-Konfigurationsdatei definiert, sodass Routing-Regeln einfach geändert werden können.
- Typsicherheit: TypeScript- und Zod-Schemas gewährleisten Konfigurationsgültigkeit und Laufzeittypsicherheit.
- Skalierbarkeit: Das Node.js-Clustermodul ermöglicht die Nutzung mehrerer CPU-Kerne für eine bessere Leistung.
- Modularität: Klare Trennung der Belange mit unterschiedlichen Modulen für Konfiguration, Serverlogik und Schemavalidierung.
Projektstruktur
├── 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
Schlüsselkomponenten
- config.yaml: Definiert die Konfiguration des Servers, einschließlich Port, Arbeitsprozesse, Upstream-Server, Header und Routing-Regeln.
- config-schema.ts: Definiert Validierungsschemata mithilfe der Zod-Bibliothek, um sicherzustellen, dass die Konfigurationsstruktur korrekt ist.
- server-schema.ts: Gibt Nachrichtenformate an, die zwischen den Master- und Worker-Prozessen ausgetauscht werden.
- config.ts: Bietet Funktionen zum Parsen und Validieren der YAML-Konfigurationsdatei.
- server.ts: Implementiert die Reverse-Proxy-Server-Logik, einschließlich Cluster-Setup, HTTP-Verarbeitung und Anforderungsweiterleitung.
- index.ts: Dient als Einstiegspunkt, analysiert Befehlszeilenoptionen und initiiert den Server.
Konfigurationsmanagement
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.
Konfigurationsvalidierung (config-schema.ts)
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>;
Parsen und Validieren von Konfigurationen (config.ts)
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; }
Reverse-Proxy-Server-Logik (server.ts)
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:
- Nutzen Sie mehrere CPU-Kerne
- Anfragen gleichzeitig bearbeiten
- Aufrechterhaltung einer hohen Verfügbarkeit
- Anfrageverarbeitung isolieren
-
Masterprozess:
- Erstellt Arbeitsprozesse
- Verteilt eingehende Anfragen an alle Mitarbeiter
- Verwaltet den Worker-Pool
- Behandelt Worker-Abstürze und Neustarts
-
Arbeiterprozesse:
- Einzelne HTTP-Anfragen bearbeiten
- Anfragen anhand von Routing-Regeln abgleichen
- Anfragen an Upstream-Server weiterleiten
- Verarbeiten Sie Antworten und senden Sie sie an Kunden zurück
Master-Prozess-Setup
├── 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.
Verteilung anfordern
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:
- Wählt zufällig einen Arbeiter aus dem Pool aus
- Sorgt für eine ausgewogene Arbeitsbelastung aller Mitarbeiter
- Behandelt Randfälle, bei denen Mitarbeiter möglicherweise nicht verfügbar sind
Worker-Prozessanforderungslogik
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:
- Lädt seine Konfiguration aus Umgebungsvariablen
- Validiert die Konfiguration mithilfe von Zod-Schemas
- Behält eine eigene Kopie der Konfiguration bei
Mitarbeiter wählen Upstream-Server aus durch:
- Suchen der entsprechenden Upstream-ID aus der Regel
- Auffinden der Upstream-Serverkonfiguration
- Überprüfung, dass der Upstream-Server vorhanden ist
Der Mechanismus zur Anforderungsweiterleitung:
- Erstellt eine neue HTTP-Anfrage an den Upstream-Server
- Streamet die Antwortdaten
- Aggregiert den Antworttext
- Sendet die Antwort zurück an den Masterprozess
Ausführen des Servers
Um den Server auszuführen, befolgen Sie diese Schritte:
- Erstellen Sie das Projekt:
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 starten:
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}); }); }
- Entwicklungsmodus:
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.
Kommunikationsfluss
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
Leistungsüberlegungen
Die Multiprozessarchitektur bietet mehrere Leistungsvorteile:
- CPU-Auslastung: Arbeitsprozesse können auf verschiedenen CPU-Kernen ausgeführt werden und nutzen dabei verfügbare Hardwareressourcen.
- Prozessisolierung: Ein Absturz bei einem Mitarbeiter hat keine Auswirkungen auf andere, was die Zuverlässigkeit erhöht.
- Lastverteilung: Durch die zufällige Verteilung von Anfragen wird verhindert, dass ein einzelner Mitarbeiter überlastet wird.
Zukünftige Verbesserungen
Die aktuelle Implementierung ist zwar funktionsfähig, könnte jedoch um Folgendes erweitert werden:
- Bessere Lastverteilung: Implementieren Sie anspruchsvollere Algorithmen wie Round-Robin oder Least-Connections.
- Gesundheitsprüfungen: Fügen Sie regelmäßige Gesundheitsprüfungen für Upstream-Server hinzu.
- Caching: Implementieren Sie Antwort-Caching, um die Belastung des Upstream-Servers zu reduzieren.
- Metriken: Fügen Sie Metriken im Prometheus-Stil zur Überwachung hinzu.
- WebSocket-Unterstützung: Erweitern Sie den Proxy, um WebSocket-Verbindungen zu verarbeiten.
- HTTPS-Unterstützung: Fügen Sie SSL/TLS-Terminierungsfunktionen hinzu.
Zusammenfassung
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!

Heißer Artikel

Hot-Tools-Tags

Heißer Artikel

Heiße Artikel -Tags

Notepad++7.3.1
Einfach zu bedienender und kostenloser Code-Editor

SublimeText3 chinesische Version
Chinesische Version, sehr einfach zu bedienen

Senden Sie Studio 13.0.1
Leistungsstarke integrierte PHP-Entwicklungsumgebung

Dreamweaver CS6
Visuelle Webentwicklungstools

SublimeText3 Mac-Version
Codebearbeitungssoftware auf Gottesniveau (SublimeText3)

Heiße Themen

Ersetzen Sie Stringzeichen in JavaScript

JQuery überprüfen, ob das Datum gültig ist

HTTP-Debugging mit Knoten und HTTP-Konsole

JQuery fügen Sie Scrollbar zu Div hinzu

Benutzerdefinierte Google -Search -API -Setup -Tutorial
