redis cluster 사용해보기

개요

오늘은 저번 시간에 말했던 redis cluster를 적용해보는 시간을 가졌다. 사실 대략적으로 사용 방법만 살펴보고 빠르게 적용이 완료될 줄 알았는데... 생각보다 오래 걸렸다...

 

Redis Cluster

먼저 redis cluster가 무엇인지 다시 한 번 살펴보자. Redis ClusterRedis의 분산처리 및 고가용성을 지원하는 구성 방식으로, 데이터를 여러 Redis 노드에 분산 저장하고 관리할 수 있도록 설계되었다. 이는 단일 Redis 인스턴스의 한계를 극복하고 확장성과 장애 복구 능력을 강화하기 위해 사용된다.

 

주요 특징

  • 데이터 분산 (Sharding): 데이터를 키 범위(Hash Slot)로 나누고, 이를 클러스터의 여러 노드에 분배. Redis는 키에 대해 해시(Hash) 값을 계산하고 이를 16384개의 슬롯 중 하나에 매핑하여 저장한다. 클러스터 내의 각 노드는 이러한 슬롯 중 일부를 관리한다.
  • 고가용성: 각 Redis 노드는 마스터-슬레이브 구조를 통해 장애가 발생하더라도 자동으로 복구(failover)할 수 있다. 마스터 노드가 다운되면 클러스터 내의 슬레이브 노드가 이를 대신하여 마스터 역할을 수행한다.
  • 확장성: 노드를 추가하거나 제거하여 클러스터의 용량을 동적으로 확장할 수 있다. 데이터는 자동으로 재분배된다.
  • 분산된 요청 처리: 클라이언트는 클러스터의 어느 노드에 접속하더라도, 요청에 따라 적절한 노드로 자동으로 리다이렉트된다.

 

 

대표적인 활용

  • 캐싱 시스템: 대규모 데이터를 처리하는 웹 애플리케이션에서 빠른 데이터 액세스를 위해 사용된다.
  • 세션 저장소: 분산된 환경에서 사용자 세션 데이터를 저장하고 관리.
  • 실시간 데이터 분석: 실시간 통계, 리더보드 관리, 실시간 로그 분석 등.
  • 메시지 큐: 작업 대기열(queue) 관리.

 

Redis Cluster / 단일 Redis 인스턴스

특성 일반 Redis Redis Cluster
데이터 분산 단일 인스턴스 관리 여러 노드로 데이터 자동 분산
고가용성 지원 별도 설정 필요 (Sentinel 등) 기본적으로 지원 (자동 Failover)
확장성 한계 있음 (단일 노드 용량 제한) 노드 추가를 통한 확장 가능
성능 단일 노드의 성능에 의존 분산 노드로 성능 분배 가능

 

 

프로젝트에 적용해보기

먼저 아래는 원래 단일 redis 인스턴스를 사용하던 코드다.

import Redis from 'ioredis';
import { REDIS_HOST, REDIS_PORT } from '../../constants/env.js';

const redis = new Redis({
  host: REDIS_HOST,
  port: REDIS_PORT,
});

redis.on('connect', () => {
  console.log('Redis 연결.');
});

redis.on('ready', () => {
  console.log('Redis 준비.');
});

redis.on('error', (err) => {
  console.error('Redis 연결 에러:', err);
});

redis.on('end', () => {
  console.log('Redis 연결 종료.');
});

export default redis;

프로젝트를 구상하던 시점부터 단일 redis 대신 redis cluster를 사용해보자고 해서 redis 패키지 대신 ioredis를 이용하여 세팅해놨었다. 물론 현재 우리의 프로젝트에서는 샤딩하여 분산할 만큼의 많은 양의 데이터를 사용하지는 않을 것이지만, 공부 단계니까 이것저것 사용해보며 경험하는 것이 좋지 아니한가!!!

 

알아보니 레디스 클러스터는 노드를 설정하고 옵션을 셋팅해주면 된단다. 그럼 한 번 적용해보자.

import Redis from 'ioredis';
import { REDIS_HOST, REDIS_PORT } from '../../constants/env.js';

const nodes = [
  { host: REDIS_HOST, port: REDIS_PORT },
  { host: REDIS_HOST, port: REDIS_PORT + 1 },
  { host: REDIS_HOST, port: REDIS_PORT + 2 }
];

const redis = new Redis.Cluster(nodes, {
  redisOptions: {
    connectTimeout: 10000,
    maxRetriesPerRequest: 3,
  },
  scaleReads: 'all',
  clusterRetryStrategy: (times) => {
    const delay = Math.min(times * 50, 2000);
    return delay;
  },
});

redis.on('connect', () => {
  console.log('Redis 연결.');
});

redis.on('ready', () => {
  console.log('Redis 준비.');
});

redis.on('error', (err) => {
  console.error('Redis 연결 에러:', err);
});

redis.on('end', () => {
  console.log('Redis 연결 종료.');
});

export default redis;

당연하게도 실무적인 환경에서는 노드의 물리 주소가 각각 다를 것이다. 하지만 지금은 같은 주소에 포트만 다르게 하여 레디스 클러스터의 사용에 의의를 두도록 하였다. 이 때는 금방 실행할 수 있을 것 같았다...

도커 (Docker)

컨테이너(Container) 기술을 이용하여 애플리케이션과 그 실행 환경을 손쉽게 배포, 관리, 실행할 수 있게 해주는 플랫폼으로, 컨테이너는 애플리케이션이 실행되는 데 필요한 코드, 라이브러리, 설정 파일 등을 독립적으로 패키징하여, 어디서든 동일한 환경에서 실행될 수 있도록 보장한다.

 

주요 개념

  • 컨테이너(Container): 컨테이너는 애플리케이션과 필요한 모든 실행 환경(라이브러리, 종속성 등)을 격리된 환경에서 실행한다. 가상머신과 달리 운영 체제 전체를 포함하지 않아 가볍고 빠르다.
  • 이미지(Image): 컨테이너를 생성하기 위한 템플릿(설정 파일). Dockerfile을 작성하여 이미지를 빌드할 수 있으며, 한 번 빌드된 이미지는 여러 환경에서 재사용 가능하다.
  • Dockerfile: 이미지 생성에 필요한 명령어를 정의한 스크립트 파일. ex. 특정 OS 기반으로 애플리케이션 설치, 포트 설정, 실행 명령어 정의 등.
  • 레지스트리(Registry): 이미지를 저장하고 배포할 수 있는 저장소. Docker Hub는 도커의 기본 이미지 저장소이며, 사용자 정의 레지스트리도 설정할 수 있다.
  • 도커 엔진(Docker Engine): 도커 컨테이너를 빌드, 실행, 관리하기 위한 핵심 엔진. 도커 데몬(Docker Daemon)이 컨테이너 실행 및 관리를 담당한다.

 

Redis는 공식적으로 Windows 환경을 직접 지원하지 않기 때문에, Docker를 사용하면 Windows에서도 Redis를 간편하게 실행할 수 있다. Docker는 Redis와 같은 리눅스 기반 애플리케이션을 Windows 환경에서도 실행 가능하게 만들어주는 중요한 도구다.

 

도커로 레디스 클러스터 실행

# 실행 중인 컨테이너 확인
docker ps
# 모든 컨테이너 확인 (중지된 것 포함)
docker ps -a

 

도커에 실행 중인 컨테이너를 확인하고 레디스 클러스터를 추가한다.

docker exec -it redis-1 redis-cli --cluster create redis-1:6379 redis-2:6379 redis-3:6379 --cluster-yes

 

그리고 다시 서버 실행...

더보기
...
Redis 연결 에러: ClusterAllFailedError: Failed to refresh slots cache.
at tryNode
...

 

찾아보니 레디스 클러스터가 제대로 설정되지 않으면 위의 오류가 발생한다고 한다. 이 때부터 고난이 시작됐다. 연결 HOST를 도커 컨테이너 이름으로 변경도 해보고, 클러스터를 이용하는 서버로 바꾸기도 해보고... 수많은 방법을 시도하다 결국 성공했다.

import Redis from 'ioredis';
import { REDIS_HOST, REDIS_PORT } from '../../constants/env.js';

let redisClient = null;

function createRedisClient() {
  console.log('Redis 클러스터 연결 시도...');

  if (redisClient) return redisClient;

  try {
    const nodes = [
      { host: REDIS_HOST, port: REDIS_PORT },
      { host: REDIS_HOST, port: REDIS_PORT + 1 },
      { host: REDIS_HOST, port: REDIS_PORT + 2 },
    ];

    console.log('Redis 클러스터 노드 설정:', nodes);

    redisClient = new Redis.Cluster(nodes, {
      redisOptions: {
        connectTimeout: 30000,
        maxRetriesPerRequest: 3,
        retryStrategy(times) {
          const delay = Math.min(times * 100, 3000);
          return delay;
        },
      },
      clusterRetryStrategy(times) {
        console.log(`클러스터 재연결 시도 ${times}번째...`);
        if (times >= 3) {
          console.error('클러스터 연결 실패, 재시도 중단');
          return null;
        }
        return Math.min(times * 100, 3000);
      },
      scaleReads: 'master',
      enableReadyCheck: true,
      maxRedirections: 16,
      natMap: {
        '172.20.0.2:6379': { host: REDIS_HOST, port: REDIS_PORT },
        '172.20.0.3:6379': { host: REDIS_HOST, port: REDIS_PORT + 1 },
        '172.20.0.4:6379': { host: REDIS_HOST, port: REDIS_PORT + 2 },
      },
    });

    console.log('Redis 클라이언트 인스턴스 생성됨');

    redisClient.on('connect', () => {
      console.log('Redis 클러스터 노드에 연결됨');
    });

    redisClient.on('ready', () => {
      console.log('Redis 클러스터 준비 완료!');
      // 연결 테스트
      redisClient
        .ping()
        .then(() => {
          console.log('Redis 클러스터 PING 성공');
        })
        .catch((err) => {
          console.error('Redis 클러스터 PING 실패:', err);
        });
    });

    redisClient.on('error', (err) => {
      console.error('Redis 클러스터 에러:', err.message);
    });

    redisClient.on('end', () => {
      console.log('Redis 클러스터 연결 종료');
      redisClient = null;
    });

    redisClient.on('node error', (err, node) => {
      if (node && node.options) {
        console.error(`노드 ${node.options.host}:${node.options.port} 에러:`, err);
      } else {
        console.error('Redis 노드 에러:', err);
      }
    });

    return redisClient;
  } catch (error) {
    console.error('Redis 클라이언트 생성 중 에러:', error);
    throw error;
  }
}

const redis = createRedisClient();
export default redis;

코드의 많은 console 로그를 보면 느껴지겠지만, 연결 실패 시 재시도, 재매핑, 핑퐁 테스트 등 디버그를 일일이 살피며 로직을 짰다. 이후 도커를 이용해서 레디스 클러스터 실행, 데이터를 확인하면 해시 값에 따라 샤딩되어 데이터가 저장됨을 확인 가능했다.

 

 

'Side Projects' 카테고리의 다른 글

Fluent Validation? Bcrypt?  (0) 2024.12.08
Bull Queue?  (0) 2024.12.03
redis / ioredis 패키지 비교  (1) 2024.11.18
TCP Multi-Player - 트러블 슈팅  (0) 2024.11.05
ORM / Low-Level Query  (0) 2024.10.29