Redis는 Remote Dictionary Server의 약자로, 오픈소스인 인메모리 데이터 구조 저장소입니다.
기존 DB
로 Postgresql
를 사용하고 있었고 채팅로그를 저장하기 위해 Disk Storage
인 Postgresql
을 사용하기엔 너무 많은 DB I/O
가 발생하지 않을까란 생각에 In-memory DB
를 사용하게 되었다.
In-memory DB
를 선택하는 과정에서 Redis
와 Memcached
를 비교하게 되는데 Redis
는 다양한 데이터 타입, 확장성, 활발한 커뮤니티와 생태계 등 다양한 부분에서 우위를 가지고 있다.
실제로 알아본 Redis
는 자주 액세스하는 데이터를 메모리에 저장할 수 있으므로 디스크 기반 스토리지보다 훨씬 빠르게 액세스하여 걸리는 시간을 줄이고 백엔드 데이터베이스의 부하를 줄인다. 이는 Chat App
에서 실시간으로 대화를 주고받는 데 매우 중요한 요소이다. 또한 동일한 양의 트래픽을 처리하는 데 더 적은 리소스가 필요로 한다.
Redis
는 데이터를 쉽게 scale out
할 수 있어 Chat App
에서 대규모 트래픽을 처리하는 데 적합하다.
하나의 테이블에 저장되는 데이터를 2개 이상의 서버로 동시에 분산 저장 하는 방법을 샤딩이라고 합니다. 샤딩을 통해 데이터를 분산 저장하다보면 장애가 생겨되어 데이터 유실이 발생할 수 있는데 이를 방지하기 위해 분산 서버마다 복제 서버를 함께 구축해서 운영하게 됩니다. 이와 같이 데이터 분산 처리를 위한 샤딩과 안정성 확보를 위한 복제 시스템은 함께 사용될 수 밖에 없는데 이를 Redis 클러스터 라고 표현합니다.
Redis
를 공부하던 중 Redis Cluster
를 알게 되었고 좋아 보이는 것은 써봐야지 하는 호기심으로 Cluster를 구성하게 되었다.
Redis Custer
를 사용하여 위에서 나열한 Redis
의 이점을 활용하는 동시에 영속성과 확장성을 보장한다.
먼저 Redis cluster
는 Master-Slave
구조를 통해 영속성을 보장한다. slave
는 master
의 데이터를 복제하고 master
장애 발생 시 slave
가 master
로 승격하여 cluster
를 유지한다.
Redis cluster
는 데이터를 여러 Redis node
로 분할할 수 있도록 하여 수평으로 확장하는 방법을 제공한다. 서버의 자원을 업그레이드 하는 Scale-up
방식과 별도의 서버를 추가하는 Scale-out
방식이 있다. Scale-up
의 경우 서버의 조건에 따라 매우 제한적이므로 보통 Scale-out
을 사용한다.
완성된 설정은 Step.1과 Step.5에 있습니다. 나머지는 해결 과정을 담았습니다.
로컬에서 클러스터 구성에 성공하고 컨테이너화 하기 위해 로컬의 설정 그대로 .yaml
파일로 만들고 클러스터를 구성해 주는 과정에서 Connection refused
오류가 발생했다.
컨테이너에서 컨테이너로 연결이 되어 있지 않다라는 것을 확인했고 network
를 따로 설정해 주었다.
docker-compose.yaml
version: '3.7'
services:
node01:
image: redis:7.0.4
container_name: redis01
restart: always
ports:
- 7001:7001
volumes:
- ./cluster/node01.conf:/etc/redis/redis.conf
command:
redis-server /etc/redis/redis.conf
networks:
redis_cluster:
ipv4_address: 173.17.0.2
node02:
image: redis:7.0.4
.
.
.
node06:
image: redis:7.0.4
container_name: redis06
restart: always
ports:
- 7006:7006
volumes:
- ./cluster/node06.conf:/etc/redis/redis.conf
command:
redis-server /etc/redis/redis.conf
networks:
redis_cluster:
ipv4_address: 173.17.0.7
redis_cluster:
image: redis:7.0.4
container_name: redis_cluster
platform: linux/arm64/v8
command: redis-cli --cluster create 173.17.0.2:7001 173.17.0.3:7002
173.17.0.4:7003 173.17.0.5:7004 173.17.0.6:7005 173.17.0.7:7006
--cluster-yes --cluster-replicas 1
depends_on:
- node01
- node02
- node03
- node04
- node05
- node06
networks:
redis_cluster:
ipv4_address: 173.17.0.8
networks:
redis_cluster:
driver: bridge
ipam:
driver: default
config:
- subnet: 173.17.0.0/24
기본적으로 각 Docker Container
들은 Bridge Network
를 공유 한다. 위의 설정을 통해 network
를 따로 설정해 주었고 정상적으로 Cluster
를 구성 했다. 컨테이너간 연결도 확인 됐다.
구성한 Cluster
를 진행 중인 ChatApp
에 적용하기 위해 첫번째로 사용한 방법은 Redis caching
을 지원하는 NestJS
내장 모듈인 CacheModule
이다.
import { CacheModule, Module } from '@nestjs/common';
import * as redisStore from 'cache-manager-ioredis';
@Module({
imports: [CacheModule.register({
store: redisStore,
clusterConfig: {
nodes: [
{
port: 7001,
host: 'localhost'
},
{
port: 7002,
host: 'localhost'
},
{
port: 7003,
host: 'localhost'
}
],
},
})],
@Injectable()
export class ChatService {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache
) {}
async setCache(key: string, value: string) {
return await this.cacheManager.set(key,value)
}
}
@Controller('chat')
export class ChatController {
constructor(private readonly chatService: ChatService) {}
@Get('/setCache')
async setCache(): Promise<sting> {
await this.chatService.setCache("foo","bar")
return 'success'
}
}
정상적으로 서버는 실행. 작동을 확인하기 위해 간단한 요청을 보냈지만 sending이 지속되었고 메소드는 pending
되는 것을 확인했다.
CacheModule
의 설정들을 건들여가면서 연결 상태를 확인하고 싶었지만 명확하게 확인할 수 있는 방법 발견 못했다. 이후 Module
로 RedisCluster
을 지원하는 패키지를 찾아 설치하고 설정했다.
yarn add @liaoliaots/nestjs-redis ioredis
ClusterModule.forRoot({
readyLog: true,
errorLog: true,
config: {
scaleReads: 'slave',
nodes: [
{ host: '127.0.0.1', port: 7001 },
{ host: '127.0.0.1', port: 7002 },
{ host: '127.0.0.1', port: 7003 }
],
},
}),
scaleReads
의 경우 설정을 따로 해주지 않으면 모든node
에서 읽기가 가능해master node
의load
가 증가한다.
로그를 찍어 cluster
를 확인할 수 있었고 노드들이 정확히 들어갔고 슬롯도 정상적으로 보였다. 상태가 'connect'
인 모습을 보고 문제가 없어 보였지만 cacheModule
을 사용했을때와 같은 문제가 계속됐다.
다음은 Predixy
이다.
Redis Cluster의 Cluster Client는 Cluster를 구성하는 모든 Redis와 Network로 직접 연결되어 있어야 한다는 특징을 갖고 있습니다. 즉 Redis Cluster의 각 Redis는 Cluster Client를 위한 End-point를 반드시 하나이상 갖고 있어야 합니다. 이러한 특징 때문에 Cluster를 구성하는 Redis의 개수 또는 Cluster Client의 개수가 늘어날수록 Network Connection은 기하급수적으로 늘어납니다. 또한 Redis Master-slave의 Client에게 Master, Slave 2개의 End-point만을 제공하던 Network 환경에 Redis Cluster 구성을 힘들게하는 요인이 됩니다. 이러한 문제점들을 해결하기 위해서는 Cluster Proxy를 이용해야 합니다.
Predixy
를 구성하였지만 endpoint
에서 node
로 연결이 되지 않는 것을 확인했다.
ioredis
공식문서와 stackoverflow
를 전전하던 중 비슷한 경험을 한 질문을 발견하고 natmap
설정을 맞춰주었다
ClusterModule.forRoot({
readyLog: true,
errorLog: true,
config: {
scaleReads: 'slave',
nodes: [
{ host: '127.0.0.1', port: 7001 },
{ host: '127.0.0.1', port: 7002 },
{ host: '127.0.0.1', port: 7003 },
],
natMap: {
'173.17.0.2:7001': {
host: '127.0.0.1',
port: 7001,
},
'173.17.0.3:7002': {
host: '127.0.0.1',
port: 7002,
},
'173.17.0.4:7003': {
host: '127.0.0.1',
port: 7003,
},
'173.17.0.5:7004': {
host: '127.0.0.1',
port: 7004,
},
'173.17.0.6:7005': {
host: '127.0.0.1',
port: 7005,
},
'173.17.0.7:7006': {
host: '127.0.0.1',
port: 7006,
},
},
},
}),
ioredis
의natMap
옵션을 사용하면 클러스터의 각Redis
서버에 대한 퍼블릭 및 프라이빗 IP 주소와 포트 간의 매핑을 지정.ioredis
를 사용하여Redis
클러스터에 연결하면 클라이언트는natMap
을 사용하여 공용 IP 주소 및 포트를 개인 IP 주소 및 포트로 변환한 다음 개인 주소 및 포트를 사용하여 Redis 서버에 연결한다.
설정을 완료하고 로그를 찍어 Cluster
를 확인해보니 상태는 ready
로 나왔다. 정상적으로 메소드가 실행되는 것을 확인했다.
참고
NestJs의 Module과 CacheModule을 활용한 Redis 연동
Redis Cluster 사용하기 (Cluster Proxy)
Docker로 Redis 클러스터 구성하기