Nestjs - Redis Cluster

atesi·2023년 2월 17일
0

Redis

Redis는 Remote Dictionary Server의 약자로, 오픈소스인 인메모리 데이터 구조 저장소입니다.

Why?

기존 DBPostgresql를 사용하고 있었고 채팅로그를 저장하기 위해 Disk StoragePostgresql을 사용하기엔 너무 많은 DB I/O가 발생하지 않을까란 생각에 In-memory DB를 사용하게 되었다.

In-memory DB를 선택하는 과정에서 RedisMemcached를 비교하게 되는데 Redis는 다양한 데이터 타입, 확장성, 활발한 커뮤니티와 생태계 등 다양한 부분에서 우위를 가지고 있다.

실제로 알아본 Redis는 자주 액세스하는 데이터를 메모리에 저장할 수 있으므로 디스크 기반 스토리지보다 훨씬 빠르게 액세스하여 걸리는 시간을 줄이고 백엔드 데이터베이스의 부하를 줄인다. 이는 Chat App에서 실시간으로 대화를 주고받는 데 매우 중요한 요소이다. 또한 동일한 양의 트래픽을 처리하는 데 더 적은 리소스가 필요로 한다.

Redis는 데이터를 쉽게 scale out할 수 있어 Chat App에서 대규모 트래픽을 처리하는 데 적합하다.

Redis Cluster

하나의 테이블에 저장되는 데이터를 2개 이상의 서버로 동시에 분산 저장 하는 방법을 샤딩이라고 합니다. 샤딩을 통해 데이터를 분산 저장하다보면 장애가 생겨되어 데이터 유실이 발생할 수 있는데 이를 방지하기 위해 분산 서버마다 복제 서버를 함께 구축해서 운영하게 됩니다. 이와 같이 데이터 분산 처리를 위한 샤딩과 안정성 확보를 위한 복제 시스템은 함께 사용될 수 밖에 없는데 이를 Redis 클러스터 라고 표현합니다.

Why?

Redis를 공부하던 중 Redis Cluster를 알게 되었고 좋아 보이는 것은 써봐야지 하는 호기심으로 Cluster를 구성하게 되었다.

Redis Custer를 사용하여 위에서 나열한 Redis의 이점을 활용하는 동시에 영속성과 확장성을 보장한다.

먼저 Redis clusterMaster-Slave구조를 통해 영속성을 보장한다. slavemaster의 데이터를 복제하고 master 장애 발생 시 slavemaster로 승격하여 cluster를 유지한다.

Redis cluster는 데이터를 여러 Redis node로 분할할 수 있도록 하여 수평으로 확장하는 방법을 제공한다. 서버의 자원을 업그레이드 하는 Scale-up 방식과 별도의 서버를 추가하는 Scale-out 방식이 있다. Scale-up의 경우 서버의 조건에 따라 매우 제한적이므로 보통 Scale-out을 사용한다.

Set Up

완성된 설정은 Step.1과 Step.5에 있습니다. 나머지는 해결 과정을 담았습니다.

Step.1

로컬에서 클러스터 구성에 성공하고 컨테이너화 하기 위해 로컬의 설정 그대로 .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를 구성 했다. 컨테이너간 연결도 확인 됐다.

Step.2

구성한 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 되는 것을 확인했다.

Step.3

CacheModule의 설정들을 건들여가면서 연결 상태를 확인하고 싶었지만 명확하게 확인할 수 있는 방법 발견 못했다. 이후 ModuleRedisCluster을 지원하는 패키지를 찾아 설치하고 설정했다.

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 nodeload가 증가한다.

로그를 찍어 cluster를 확인할 수 있었고 노드들이 정확히 들어갔고 슬롯도 정상적으로 보였다. 상태가 'connect'인 모습을 보고 문제가 없어 보였지만 cacheModule을 사용했을때와 같은 문제가 계속됐다.

Step.4

다음은 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로 연결이 되지 않는 것을 확인했다.

Step.5

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,
          },
        },
      },
    }),

ioredisnatMap 옵션을 사용하면 클러스터의 각 Redis 서버에 대한 퍼블릭 및 프라이빗 IP 주소와 포트 간의 매핑을 지정. ioredis를 사용하여 Redis 클러스터에 연결하면 클라이언트는 natMap을 사용하여 공용 IP 주소 및 포트를 개인 IP 주소 및 포트로 변환한 다음 개인 주소 및 포트를 사용하여 Redis 서버에 연결한다.

설정을 완료하고 로그를 찍어 Cluster를 확인해보니 상태는 ready로 나왔다. 정상적으로 메소드가 실행되는 것을 확인했다.

참고
NestJs의 Module과 CacheModule을 활용한 Redis 연동
Redis Cluster 사용하기 (Cluster Proxy)
Docker로 Redis 클러스터 구성하기

profile
Action!

0개의 댓글