오늘날의 마이크로서비스 아키텍처에서 역방향 프록시는 들어오는 요청을 관리하고 다양한 백엔드 서비스로 라우팅하는 데 중요한 역할을 합니다.
역방향 프록시는 애플리케이션의 웹 서버 앞에 위치하며 클라이언트 시스템에서 오는 요청을 가로챕니다. 여기에는 로드 밸런싱, 보안 강화로 이어지는 숨겨진 원본 서버 IP 주소, 캐싱, 속도 제한 등 많은 이점이 있습니다.
분산 및 마이크로서비스 아키텍처에서는 단일 진입점이 필요합니다. Nginx와 같은 역방향 프록시 서버는 이러한 시나리오에 도움이 됩니다. 서버의 여러 인스턴스가 실행 중인 경우 효율적인 요청 라우팅을 관리하고 보장하는 것이 까다로워집니다. 이 경우에는 Nginx와 같은 역방향 프록시가 완벽한 솔루션입니다. 도메인을 Nginx 서버의 IP 주소로 지정할 수 있으며 Nginx는 구성에 따라 들어오는 요청을 인스턴스 중 하나로 라우팅하는 동시에 각 인스턴스에서 처리되는 로드를 처리합니다.
Nginx가 뛰어난 안정성과 속도로 어떻게 대규모 요청을 지원할 수 있는지 자세히 설명하는 Nginx의 기사인 Nginx Architecture를 읽어 보시기 바랍니다.
간단히 말하면 Nginx에는 마스터 프로세스와 여러 작업자 프로세스가 있습니다. 또한 캐시 로더 및 캐시 관리자와 같은 도우미 프로세스도 있습니다. 힘든 작업은 모두 마스터와 워커 프로세스가 담당합니다.
작업자 프로세스는 여러 연결을 비차단으로 처리하여 컨텍스트 전환을 줄입니다. 단일 스레드이고 독립적으로 실행되며 캐시 및 세션 데이터와 같은 공유 리소스에 공유 메모리를 사용합니다. 이 아키텍처는 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의 클러스터 모듈을 사용하는 마스터-워커 아키텍처를 따릅니다. 이 디자인을 통해 다음을 수행할 수 있습니다.
마스터 프로세스:
작업자 프로세스:
├── 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 스키마를 사용하여 메시지 구조를 검증하여 필요한 모든 요청 정보를 포함한 표준화된 메시지 페이로드를 구성하여 작업자와 통신합니다.
작업자는 실제 요청 처리 및 프록시를 처리합니다. 각 작업자:
작업자는 다음을 기준으로 업스트림 서버를 선택합니다.
요청 전달 메커니즘:
서버를 실행하려면 다음 단계를 따르세요.
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인 jsonplaceholder.typicode.com/todos에서 JSON 응답을 받았습니다.
전체 요청 흐름을 시각화해 보겠습니다.
클라이언트가 요청 보내기 → 마스터 프로세스
마스터 프로세스 → 작업자 선택
작업자 → 업스트림 서버
업스트림 서버 → 워커
작업자 → 마스터 프로세스
마스터 프로세스 → 클라이언트
다중 프로세스 아키텍처는 여러 가지 성능상의 이점을 제공합니다.
기능적이지만 현재 구현은 다음을 통해 향상될 수 있습니다.
역방향 프록시 서버를 처음부터 구축하는 것은 처음에는 겁이 날 수도 있지만, 살펴본 것처럼 보람 있는 경험입니다. Node.js 클러스터, TypeScript 및 YAML 기반 구성 관리를 결합하여 Nginx에서 영감을 받아 확장 가능하고 효율적인 시스템을 만들었습니다.
이 구현을 개선할 여지가 아직 남아 있습니다. 더 나은 로드 밸런싱, 캐싱 또는 WebSocket 지원은 탐구해야 할 몇 가지 아이디어일 뿐입니다. 그러나 현재 설계는 추가 실험과 확장을 위한 강력한 기반을 마련합니다. 따라하셨다면 이제 역방향 프록시에 대해 더 자세히 알아보거나 필요에 맞는 맞춤형 솔루션 구축을 시작할 수 있습니다.
연결하거나 내 작업을 더 보려면 내 GitHub, LinkedIn을 확인하세요.
이 프로젝트의 저장소는 여기에서 찾을 수 있습니다.
여러분의 생각, 피드백, 개선 아이디어를 듣고 싶습니다. 읽어주셔서 감사합니다. 즐거운 코딩 되세요! ?
위 내용은 Node.js 및 TypeScript를 사용하여 Nginx와 같은 확장 가능한 역방향 프록시 서버 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!