在当今的微服务架构中,反向代理在管理传入请求并将其路由到各种后端服务方面发挥着至关重要的作用。
反向代理位于应用程序的 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中文网其他相关文章!