>웹 프론트엔드 >JS 튜토리얼 >Node.js 및 TypeScript를 사용하여 Nginx와 같은 확장 가능한 역방향 프록시 서버 구축

Node.js 및 TypeScript를 사용하여 Nginx와 같은 확장 가능한 역방향 프록시 서버 구축

Susan Sarandon
Susan Sarandon원래의
2025-01-05 08:48:41647검색

영감

오늘날의 마이크로서비스 아키텍처에서 역방향 프록시는 들어오는 요청을 관리하고 다양한 백엔드 서비스로 라우팅하는 데 중요한 역할을 합니다.

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

역방향 프록시는 애플리케이션의 웹 서버 앞에 위치하며 클라이언트 시스템에서 오는 요청을 가로챕니다. 여기에는 로드 밸런싱, 보안 강화로 이어지는 숨겨진 원본 서버 IP 주소, 캐싱, 속도 제한 등 많은 이점이 있습니다.

분산 및 마이크로서비스 아키텍처에서는 단일 진입점이 필요합니다. Nginx와 같은 역방향 프록시 서버는 이러한 시나리오에 도움이 됩니다. 서버의 여러 인스턴스가 실행 중인 경우 효율적인 요청 라우팅을 관리하고 보장하는 것이 까다로워집니다. 이 경우에는 Nginx와 같은 역방향 프록시가 완벽한 솔루션입니다. 도메인을 Nginx 서버의 IP 주소로 지정할 수 있으며 Nginx는 구성에 따라 들어오는 요청을 인스턴스 중 하나로 라우팅하는 동시에 각 인스턴스에서 처리되는 로드를 처리합니다.

Nginx가 그렇게 좋은가요?

Nginx가 뛰어난 안정성과 속도로 어떻게 대규모 요청을 지원할 수 있는지 자세히 설명하는 Nginx의 기사인 Nginx Architecture를 읽어 보시기 바랍니다.

간단히 말하면 Nginx에는 마스터 프로세스와 여러 작업자 프로세스가 있습니다. 또한 캐시 로더 및 캐시 관리자와 같은 도우미 프로세스도 있습니다. 힘든 작업은 모두 마스터와 워커 프로세스가 담당합니다.

  • 마스터 프로세스: 구성을 관리하고 하위 프로세스를 생성합니다.
  • 캐시 로더/관리자: 최소한의 리소스로 캐시 로드 및 정리를 처리합니다.
  • 작업자 프로세스: 비차단 및 독립적 실행으로 연결, 디스크 I/O 및 업스트림 통신을 관리합니다.

작업자 프로세스는 여러 연결을 비차단으로 처리하여 컨텍스트 전환을 줄입니다. 단일 스레드이고 독립적으로 실행되며 캐시 및 세션 데이터와 같은 공유 리소스에 공유 메모리를 사용합니다. 이 아키텍처는 Nginx가 컨텍스트 전환 수를 줄이고 차단, 다중 프로세스 아키텍처보다 더 빠르게 속도를 높이는 데 도움이 됩니다.

이로부터 영감을 받아 마스터와 작업자 프로세스의 동일한 개념을 사용하고 작업자 프로세스당 수천 개의 연결을 처리할 수 있는 자체 이벤트 기반 역방향 프록시 서버를 구현하겠습니다.

프로젝트 아키텍처

우리의 역방향 프록시 구현은 다음과 같은 주요 설계 원칙을 따릅니다.

  1. 구성 기반: 모든 프록시 동작은 YAML 구성 파일에 정의되어 있어 라우팅 규칙을 쉽게 수정할 수 있습니다.
  2. 유형 안전성: TypeScript 및 Zod 스키마는 구성 유효성과 런타임 유형 안전성을 보장합니다.
  3. 확장성: Node.js 클러스터 모듈을 사용하면 여러 CPU 코어를 활용하여 성능을 높일 수 있습니다.
  4. 모듈화: 구성, 서버 로직 및 스키마 검증을 위한 고유한 모듈을 통해 문제를 명확하게 분리합니다.

프로젝트 구조

├── 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

주요 구성 요소

  1. config.yaml: 포트, 작업자 프로세스, 업스트림 서버, 헤더 및 라우팅 규칙을 포함한 서버 구성을 정의합니다.
  2. config-schema.ts: 구성 구조가 올바른지 확인하기 위해 Zod 라이브러리를 사용하여 유효성 검사 스키마를 정의합니다.
  3. server-schema.ts: 마스터 프로세스와 작업자 프로세스 간에 교환되는 메시지 형식을 지정합니다.
  4. config.ts: YAML 구성 파일을 구문 분석하고 검증하는 기능을 제공합니다.
  5. server.ts: 클러스터 설정, HTTP 처리 및 요청 전달을 포함한 역방향 프록시 서버 논리를 구현합니다.
  6. index.ts: 명령줄 옵션을 구문 분석하고 서버를 시작하는 진입점 역할을 합니다.

구성 관리

구성 시스템은 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".

들어오는 요청은 규칙에 따라 평가됩니다. 경로에 따라 역방향 프록시는 요청을 전달할 업스트림 서버를 결정합니다.

구성 검증(config-schema.ts)

우리는 구성 검증을 위한 엄격한 스키마를 정의하기 위해 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)

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;
}

역방향 프록시 서버 논리(server.ts)

서버는 확장성을 위해 Node.js 클러스터 모듈을 활용하고 요청 처리를 위해 http 모듈을 활용합니다. 마스터 프로세스는 요청을 작업자 프로세스에 배포하고 이를 업스트림 서버로 전달합니다. 역방향 프록시 서버의 핵심 논리가 포함된 server.ts 파일을 자세히 살펴보겠습니다. 우리는 각 구성 요소를 분석하고 이들이 어떻게 함께 작동하여 확장 가능한 프록시 서버를 만드는지 이해합니다.

서버 구현은 Node.js의 클러스터 모듈을 사용하는 마스터-워커 아키텍처를 따릅니다. 이 디자인을 통해 다음을 수행할 수 있습니다.

  • 여러 CPU 코어 활용
  • 요청 동시 처리
  • 고가용성 유지
  • 격리 요청 처리
  1. 마스터 프로세스:

    • 작업자 프로세스 생성
    • 수신 요청을 여러 작업자에게 분산
    • 작업자 Pool을 관리합니다
    • 작업자 충돌 및 재시작 처리
  2. 작업자 프로세스:

    • 개별 HTTP 요청 처리
    • 라우팅 규칙에 따른 일치 요청
    • 업스트림 서버로 요청 전달
    • 응답을 처리하고 고객에게 다시 보냅니다

마스터 프로세스 설정

├── 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 스키마를 사용하여 메시지 구조를 검증하여 필요한 모든 요청 정보를 포함한 표준화된 메시지 페이로드를 구성하여 작업자와 통신합니다.

작업자는 실제 요청 처리 및 프록시를 처리합니다. 각 작업자:

  • 환경 변수에서 구성을 로드합니다
  • Zod 스키마를 사용하여 구성 유효성을 검사합니다
  • 자체 구성 복사본을 유지합니다

작업자는 다음을 기준으로 업스트림 서버를 선택합니다.

  • 규칙에서 적절한 업스트림 ID 찾기
  • 업스트림 서버 구성 찾기
  • 업스트림 서버가 존재하는지 확인하는 중

요청 전달 메커니즘:

  • 업스트림 서버에 대한 새로운 HTTP 요청을 생성합니다
  • 응답 데이터 스트리밍
  • 응답 본문 집계
  • 응답을 마스터 프로세스로 다시 보냅니다

서버 실행

서버를 실행하려면 다음 단계를 따르세요.

  1. 프로젝트 빌드:
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;
}
  1. 서버 시작:
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});
    });
}
  1. 개발 모드:
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));
});

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

위 스크린샷에서는 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".

이것을 테스트해 보겠습니다!

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

와, 멋지네요! localhost:8080으로 이동 중이지만 응답으로 jsonplaceholder.typicode.com 홈페이지를 수신한 것을 볼 수 있습니다. 최종 사용자는 별도의 서버에서 응답을 보고 있다는 사실조차 모릅니다. 이것이 바로 리버스 프록시 서버가 중요한 이유입니다. 동일한 코드를 실행하는 여러 서버가 있고 모든 포트를 최종 사용자에게 노출하고 싶지 않은 경우 역방향 프록시를 추상화 계층으로 사용하십시오. 사용자는 매우 강력하고 빠른 서버인 역방향 프록시 서버에 접속하게 되며 요청을 라우팅할 서버를 결정하게 됩니다.

이제 localhost:8080/todos를 눌러 무슨 일이 일어나는지 살펴보겠습니다.

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

요청이 다시 jsonplaceholder 서버로 역방향 프록시되고 확인된 URL인 jsonplaceholder.typicode.com/todos에서 JSON 응답을 받았습니다.

커뮤니케이션 흐름

전체 요청 흐름을 시각화해 보겠습니다.

클라이언트가 요청 보내기 → 마스터 프로세스
마스터 프로세스 → 작업자 선택
작업자 → 업스트림 서버
업스트림 서버 → 워커
작업자 → 마스터 프로세스
마스터 프로세스 → 클라이언트

성능 고려 사항

다중 프로세스 아키텍처는 여러 가지 성능상의 이점을 제공합니다.

  1. CPU 활용도: 작업자 프로세스는 사용 가능한 하드웨어 리소스를 활용하여 다양한 CPU 코어에서 실행될 수 있습니다.
  2. 프로세스 격리: 한 작업자의 충돌이 다른 작업자에게 영향을 주지 않아 안정성이 향상됩니다.
  3. 부하 분산: 요청을 무작위로 분산하면 단일 작업자가 부담을 느끼는 것을 방지할 수 있습니다.

향후 개선 사항

기능적이지만 현재 구현은 다음을 통해 향상될 수 있습니다.

  1. 더 나은 로드 밸런싱: 라운드 로빈 또는 최소 연결과 같은 보다 정교한 알고리즘을 구현합니다.
  2. 상태 확인: 업스트림 서버에 대한 정기적인 상태 확인을 추가합니다.
  3. 캐싱: 업스트림 서버 로드를 줄이기 위해 응답 캐싱을 구현합니다.
  4. 측정항목: 모니터링을 위해 프로메테우스 스타일 측정항목을 추가합니다.
  5. WebSocket 지원: WebSocket 연결을 처리하도록 프록시를 확장합니다.
  6. HTTPS 지원: SSL/TLS 종료 기능을 추가합니다.

마무리

역방향 프록시 서버를 처음부터 구축하는 것은 처음에는 겁이 날 수도 있지만, 살펴본 것처럼 보람 있는 경험입니다. Node.js 클러스터, TypeScript 및 YAML 기반 구성 관리를 결합하여 Nginx에서 영감을 받아 확장 가능하고 효율적인 시스템을 만들었습니다.

이 구현을 개선할 여지가 아직 남아 있습니다. 더 나은 로드 밸런싱, 캐싱 또는 WebSocket 지원은 탐구해야 할 몇 가지 아이디어일 뿐입니다. 그러나 현재 설계는 추가 실험과 확장을 위한 강력한 기반을 마련합니다. 따라하셨다면 이제 역방향 프록시에 대해 더 자세히 알아보거나 필요에 맞는 맞춤형 솔루션 구축을 시작할 수 있습니다.

연결하거나 내 작업을 더 보려면 내 GitHub, LinkedIn을 확인하세요.
이 프로젝트의 저장소는 여기에서 찾을 수 있습니다.

여러분의 생각, 피드백, 개선 아이디어를 듣고 싶습니다. 읽어주셔서 감사합니다. 즐거운 코딩 되세요! ?

위 내용은 Node.js 및 TypeScript를 사용하여 Nginx와 같은 확장 가능한 역방향 프록시 서버 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.