썸네일-002.png

✔️ NestJS 백엔드에서 환경에서 Redis의 PUB/SUB과 Socket.IO Adapter, PM2를 이용하여 소켓 클러스터 서버 구현하기


<aside> <img src="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1566913679/noticon/xlnsjihvjxllech0hawu.png" alt="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1566913679/noticon/xlnsjihvjxllech0hawu.png" width="40px" /> Redis 시리즈 [Redis] 0. 초 간단! Docker을 이용하여 Redis 설치하기 [Redis] 1. Redis에 대해 알아보기 [Redis] 2. NestJS 백엔드 환경에서 Redis를 데이터 저장소로 사용하기 [Redis] 3. NestJS 백엔드에서 환경에서 Redis의 PUB/SUB과 Socket.IO Adapter, PM2를 이용하여 소켓 클러스터 서버 구현하기

</aside>

✔️ 현재 상황


저희는 NestJS 프레임워크 기반의 백엔드 웹소켓 서버를 구성하고 있어요. 현재 WAS로 사용하고 있는 클라우드 인스턴스는 4개의 core을 가진 CPU를 가지고 있어요. NodeJS는 싱글스레드로 돌아가기에, 4개의 core을 전부 쓰기 위해 클러스터링 환경을 구성하는 것이 적절하다고 판단했어요.

저희는 실시간 게임 로직을 구현하기 위해 소켓을 사용하고 있어요. 따라서 클러스터링 환경을 구성하게 되면 서버 인스턴스마다 클라이언트와의 Socket Connection 상태가 다른 경우가 발생해요.

예를 들어 두개의 인스턴스로 구성된 클러스터 서버가 있다고 가정할게요. 유저A는 서버와 소켓 통신을 하기위해 서버인스턴스1 과 연결하였고, 유저 B는 서버인스턴스2와 연결했다고 가정할게요. 이럴 경우 유저 A가 유저 B에게 소켓 메시지 브로드캐스팅을 할 수 없는 문제가 생겨요.

저는 이 문제를 Redis의 PUB/SUB 기능을 이용해 해결하려고 했어요.

✔️ Redis vs Kafka vs RabbitMQ


서버간의 메시지를 전달하기 위한 메시징 플랫폼은 Redis 뿐만이 아니에요. 그 중 가장 많이 사용된다고 여겨지는 Kafka와 RabbitMQ, Redis를 간단하게 비교해보았어요.

메시징 플랫폼은 메시지 브로커와 이벤트 브로커로 나뉘어져요.

메시지 브로커와 이벤트 브로커

메시지 브로커

Redis, RabbitMQ

이벤트 브로커

Kafka

Redis vs RabbitMQ vs Kafka

Redis

RabbitMQ

Kafka

저희는 다음과 같은 이유로 Redis를 선택하였어요.

+) Socket.IO에서 공식적으로 Redis Adapter을 지원해요. (이후 후술)

Redis Pub/Sub 명령어 및 방식

Redis 는 Topic(channel)을 따로 생성하지 않는다.

  1. Subscriber가 특정 Topic 구독
  2. 클라이언트가 특정 Topic에 pub
  3. Topic 을 구독하는 Subscriber 들은 메시지 수신.(sub)
명령어(Command) 사용 패턴 설명(Desc)
subscribe channel [channel ...] 특정 채널 구독, 메시지 수신받음(다중 채널 구독 가능)
publish channel message 메시지를 특정 채널에 발송
pubsub subcommand [argument [argument ...]] Redis에 등록된 채널, 패턴 조회
psubscribe pattern [pattern ...] 채널 이름을 패턴으로 등록
unsubscribe [channel [channel ...]] 특정 채널 구독(sub) 해제
punsubscribe [pattern [pattern ...]] psubscribe로 구독한 패턴 채널 구독 해제

✔️ Redis를 이용하여 NestJS 인스턴스를 클러스터링 환경으로 구성하기


1. 관련 패키지 설치하기

$ npm i --save redis socket.io @socket.io/redis-adapter

redis를 이용해 소켓 서버 클러스터를 구현하기 위해 redis-adapter를 설치하였어요.

Socket.IO Adapter

Socket.IOAdapter라는 기능을 지원해요. Adapter는 다른 클라이언트나 클라이언트들의 하위집합에게 이벤트를 브로드캐스팅하는 책임을 가진 서버사이드 요소에요.

여러개의 소켓 서버 인스턴스로 클러스터를 구성하기 위해서는 각 인스턴스간에 적절한 라우팅을 해줘야 하기 때문에 디폴트 Adapter인 in-memory Adapter를 다른 Adapter로 변경시켜줘야 해요.

Socket.IO에서 지원하는 Adapter는 기본 어댑터인 in-memory Adapter 외에도 Redis Adapter, MongoDB Adpater, PostgresAdapter 등 여러 어댑터를 지원해요. 이 중에서 저희는 Redis Adapter을 이용하여 소켓 서버를 클러스터링 하려고 해요.

작동 원리

Redis Adapter

Redis Adapter

Redis Adapter는 Redis의 Pub/Sub 기능을 이용하여 구현되어 있어요.

io.to(”room1”).emit() or socket.broadcast.emit() 을 하게 되면

  1. 현재 서버에 연결된 모든 클라이언트들에게 소켓 이벤트를 보내요.
  2. Redis channel에 Publishing 하고, 다른 Socket 서버(클러스터의 인스턴스)에서 이를 처리해요.

2. Redis Adapter를 적용한 RedisIoAdapter 구현하기

// redis.adapter.ts

import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

export class RedisIoAdapter extends IoAdapter {
    private adapterConstructor: ReturnType<typeof createAdapter>;

    async connectToRedis(): Promise<void> {
        const pubClient = createClient({ url: `redis://localhost:6379` });
        const subClient = pubClient.duplicate();

        await Promise.all([pubClient.connect(), subClient.connect()]);

        this.adapterConstructor = createAdapter(pubClient, subClient);
    }

    createIOServer(port: number, options?: ServerOptions): any {
        const server = super.createIOServer(port, options);
        server.adapter(this.adapterConstructor);
        return server;
    }
}

NestJSSocket.IO 패키지를 IoAdapter 클래스에 래핑하여 사용해요. 웹 소켓 서버 클러스터를 구현하기 위해서는 IoAdapter를 상속해서 소켓 서버에 Redis Adapter을 적용해줘야 해요.

NestJS를 사용하지 않고 Redis Adapter을 사용하는 코드는 아래와 같아요. 아래 코드를 통해 NestJS 에 의존하는 부분과 Socket.IO의 Redis Adapter에 의존하는 부분을 인지할 수 있어요. (원본은 여기)

// Redis Adapter 적용하는 Socket.IO 공식 예제

import { Server } from "socket.io";
import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const io = new Server();

const pubClient = createClient({ url: "redis://localhost:6379" });
const subClient = pubClient.duplicate();

Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
  io.adapter(createAdapter(pubClient, subClient));
  io.listen(3000);
});

3. RedisIoAdapter을 Nest bootstrap에 적용하기

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RedisIoAdapter } from './redis/redis.adapter';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);

		// 구현한 RedisIoAdapter을 인스턴스화 해주고, app에 적용하기
    const redisIoAdapter = new RedisIoAdapter(app);
    await redisIoAdapter.connectToRedis();
    app.useWebSocketAdapter(redisIoAdapter);

    await app.listen(8080);
}
bootstrap();

NestJS의 엔트리 포인트인 main.ts에서 RedisIoAdapter을 인스턴스화 하고 app에 적용하는 코드에요.

✔️ PM2를 이용해 Node 서버 클러스터링 하기


1. 패키지 설치

이제는 PM2를 이용해 NestJS 서버를 클러스터로 구성해볼 차례에요. PM2를 사용하기 위해 global로 패키지를 설치해요.

$ npm install -g pm2

2. PM2 환경설정 파일 생성

// ecosystem.config.js
module.exports = {
    apps: [
        {
            name: 'app',
            script: `${__dirname}/dist/main.js`,
            instances: 4,
            exec_mode: 'cluster',
        },
    ],
};

3. 실행하기

pm2 start ./ecosystem.config.js
pm2 monit

CLI를 이용해 pm2를 실행하였어요. monit 명령어로 pm2 인스턴스들의 현재 상태와 로그등을 확인할 수 있어요.

✔️ 적용 확인하기


스크린샷 2022-12-01 오전 1.06.43.png

스크린샷 2022-12-01 오전 1.06.34.png

스크린샷 2022-12-01 오전 1.06.21.png

클러스터링 된 각각의 서버에서 클라이언트의 요청을 나눠서 받지만, 같은 방에 입장할 수 있는 것을 확인할 수 있었어요. 소켓 서버 클러스터가 제대로 동작함을 확인할 수 있었어요! 😂

적용 PR

https://github.com/boostcampwm-2022/web37-ZelloDraw/pull/40

✔️ 끝!


저의 Redis 3+1 부작이 이렇게 끝나게 되었어요. 시리즈로 글을 쓰고 완결낸건 처음인데 너무 뿌듯하네요 🙂  다른 레디스 관련 글도 궁금하시면 살펴봐주세요!

<aside> <img src="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1566913679/noticon/xlnsjihvjxllech0hawu.png" alt="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1566913679/noticon/xlnsjihvjxllech0hawu.png" width="40px" /> Redis 시리즈 [Redis] 0. 초 간단! Docker을 이용하여 Redis 설치하기 [Redis] 1. Redis에 대해 알아보기 [Redis] 2. NestJS 백엔드 환경에서 Redis를 데이터 저장소로 사용하기 [Redis] 3. NestJS 백엔드에서 환경에서 Redis의 PUB/SUB과 Socket.IO Adapter, PM2를 이용하여 소켓 클러스터 서버 구현하기

</aside>

참고자료


카프카, 레빗엠큐, 레디스 큐의 큰 차이점! 이벤트 브로커와 메시지 브로커에 대해 알아봅시다.

RabbitMQ vs Kafka vs Redis

MQ 비교 (Kafka, RabbitMQ, Redis)

[Redis] Redis 자료구조 알아보기

PUB/SUB, 잘 알고 쓰자!

Redis Pub/Sub

Message Queues vs Pub/Sub

Redis + Socket.IO + NestJS Chat App

Documentation | NestJS - A progressive Node.js framework

Documentation | NestJS - A progressive Node.js framework

Adapter | Socket.IO

Redis adapter | Socket.IO

[Node.js] PM2를 활용한 서버 클러스터링