<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 뿐만이 아니에요. 그 중 가장 많이 사용된다고 여겨지는 Kafka와 RabbitMQ, Redis를 간단하게 비교해보았어요.
메시징 플랫폼은 메시지 브로커와 이벤트 브로커로 나뉘어져요.
Redis, RabbitMQ
Kafka
+) Socket.IO에서 공식적으로 Redis Adapter을 지원해요. (이후 후술)
Redis 는 Topic(channel)을 따로 생성하지 않는다.
Subscribe가 Topic 을 구독하면 Topic 이 생성되는 방식임.
명령어(Command) | 사용 패턴 | 설명(Desc) |
---|---|---|
subscribe | channel [channel ...] | 특정 채널 구독, 메시지 수신받음(다중 채널 구독 가능) |
publish | channel message | 메시지를 특정 채널에 발송 |
pubsub | subcommand [argument [argument ...]] | Redis에 등록된 채널, 패턴 조회 |
psubscribe | pattern [pattern ...] | 채널 이름을 패턴으로 등록 |
unsubscribe | [channel [channel ...]] | 특정 채널 구독(sub) 해제 |
punsubscribe | [pattern [pattern ...]] | psubscribe로 구독한 패턴 채널 구독 해제 |
$ npm i --save redis socket.io @socket.io/redis-adapter
redis를 이용해 소켓 서버 클러스터를 구현하기 위해 redis-adapter를 설치하였어요.
Socket.IO는 Adapter라는 기능을 지원해요. Adapter는 다른 클라이언트나 클라이언트들의 하위집합에게 이벤트를 브로드캐스팅하는 책임을 가진 서버사이드 요소에요.
여러개의 소켓 서버 인스턴스로 클러스터를 구성하기 위해서는 각 인스턴스간에 적절한 라우팅을 해줘야 하기 때문에 디폴트 Adapter인 in-memory Adapter를 다른 Adapter로 변경시켜줘야 해요.
Socket.IO에서 지원하는 Adapter는 기본 어댑터인 in-memory Adapter 외에도 Redis Adapter, MongoDB Adpater, PostgresAdapter 등 여러 어댑터를 지원해요. 이 중에서 저희는 Redis Adapter을 이용하여 소켓 서버를 클러스터링 하려고 해요.
Redis Adapter
Redis Adapter는 Redis의 Pub/Sub 기능을 이용하여 구현되어 있어요.
io.to(”room1”).emit() or socket.broadcast.emit() 을 하게 되면
// 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;
}
}
NestJS는 Socket.IO 패키지를 IoAdapter 클래스에 래핑하여 사용해요. 웹 소켓 서버 클러스터를 구현하기 위해서는 IoAdapter를 상속해서 소켓 서버에 Redis Adapter을 적용해줘야 해요.
connectToRedis()
메서드를 통해 Redis Adapter을 생성하는 callback Function을 this.adapterConstructor에 할당해줘요.createIOServer()
메서드를 오버라이딩 해서 기존에 사용하던 server에 RedisAdapter을 적용해주었어요.