[공부정리] Redis의 흥미로운 사실들

jeyong·2024년 11월 30일
0

공부 / 생각 정리  

목록 보기
121/121

이번 게시글에서는 Redis를 공부하며 흥미롭게 접한 내용을 정리하고자 한다. 흔히 볼 수 있는 내용보다는 흥미로운 사실들만 모아 작성했으므로, 다소 가독성이 떨어질 수 있다.

1. 아키텍처

Redis는 고가용성이 필요하지 않은 시스템과 필요한 시스템을 위해 다양한 아키텍처를 제공하며, 사용자는 요구에 맞는 아키텍처를 선택할 수 있다.

레플리케이션 구조

Redis에서 레플리케이션 구조를 구성하는 방법은 매우 간단하다. 레플리카가 될 노드에서 REPLICAOF 커맨드를 사용해 마스터 노드의 정보를 입력하면 된다. 참고로 Redis의 레플리케이션 구조는 MySQL이나 PostgreSQL과 다르게 멀티 마스터 레플리케이션 구조를 지원하지 않는다. 따라서 마스터가 동시에 레플리카가 될 수는 없다.

  • 레플리케이션을 수행하는 방식?

    Redis는 레플리케이션을 수행할 때 RDB 스냅숏을 레플리카 노드에 전달하는 방식을 사용한다. RDB 스냅숏을 전송하는 동안 발생하는 새로운 데이터는 마스터 노드의 버퍼에 저장되며, 스냅숏 전송이 완료된 후 버퍼에 저장된 데이터를 추가로 전송해 동기화 작업을 진행한다.
    만약 NAS와 같은 원격 디스크를 사용해 디스크 I/O 속도가 느리다면, 디스크를 거치지 않고 RDB 스냅숏을 직접 메모리에서 전달하는 옵션도 사용할 수 있다.

  • 레플리카 노드 때문에 속도가 느려지진 않을까?

    마스터 노드에서 데이터가 변경될 때 이를 레플리카 노드에 전파해야 하므로 속도가 느려질 수 있다. 하지만 Redis는 이를 비동기 방식으로 처리해 성능 저하를 최소화한다. 클라이언트가 데이터를 입력할 때마다 레플리카로의 데이터 전송 여부를 확인하지 않기 때문에 클라이언트 성능에는 영향을 주지 않는다.
    다만, 이러한 비동기 방식 때문에 마스터 노드가 레플리카 노드로 데이터를 전송하기 전에 비정상적으로 종료된다면 데이터가 유실될 가능성이 있다. 이 문제를 해결하려면 백업 기능을 반드시 병행해서 사용해야 한다.

  • 연결할 때마다 레플리케이션을 수행하면 성능에 좋지 않을 텐데?

    레플리케이션을 수행할 때 RDB 스냅숏을 사용해 데이터를 전달한다고 했다. 따라서 레플리케이션 연결이 끊어졌다가 재연결될 때마다 마스터에서 새로운 RDB 파일을 생성하고 전송하면 성능 저하와 메모리 낭비가 발생할 것이다. Redis는 이를 방지하기 위해 부분 재동기화(PSYNC) 기능을 지원한다.
    부분 재동기화는 마스터 노드가 레플리카 노드의 데이터 동기화 상태를 기록해 두고, 끊어진 시점 이후의 데이터만 전달한다. 이를 통해 불필요한 데이터 전송을 방지하고 성능을 개선할 수 있다.

  • 레플리카 노드의 레플리카 노드?

    레플리카 노드가 또 다른 레플리카 노드를 가지는 구조를 상상해볼 수 있다. 예를 들어, 마스터 노드 → 레플리카 노드 → 레플리카 노드의 구조를 가졌을 때, 중간 레플리카 노드에서 데이터가 변경되면 하위 레플리카 노드로 전파될까?
    정답은 "아니다." 레플리카 노드의 데이터는 항상 마스터 노드와 동일해야 한다. 따라서 중간 레플리카 노드에서의 데이터 변경은 전파되지 않는다.

  • 레플리카 노드의 데이터가 준비되지 않았다면?

    레플리카 노드가 마스터 노드의 데이터와 정확하게 일치하지 않는 상황이 있을 수 있다. 예를 들어, 레플리카가 마스터와의 연결이 끊어진 상태이거나, 레플리케이션이 아직 완료되지 않은 경우가 이에 해당한다.
    이때 주의할 점은, 레플리카의 데이터가 유효하지 않은 상태에서도 클라이언트로부터 들어오는 모든 읽기 요청에 데이터를 반환한다는 것이다. 이로 인해 클라이언트는 잘못된 데이터를 받을 수 있으며, 이는 예기치 못한 문제를 유발할 수 있다.
    이를 방지하려면 replica-serve-stale-data 파라미터를 기본값인 yes에서 no로 변경해야 한다. no로 설정하면, 레플리카가 데이터가 유효하지 않은 상태에서는 INFO, CONFIG, PING 등 일부 기본 명령만 작동하며, 나머지 명령에 대해서는 오류를 반환한다. 이를 통해 클라이언트가 잘못된 데이터를 받는 것을 방지할 수 있다.

  • 주의점?

    Redis에서 레플리케이션을 사용하는 경우 백업 기능이 필요 없다고 생각할 수 있다. 그러나 이는 위험한 판단이다. 레플리케이션을 사용하더라도 백업은 별도로 설정해야 한다.
    예를 들어, 백업 기능이 없는 상태에서 마스터 노드가 재부팅되면 메모리가 초기화되고, 레플리카 노드는 초기화된 데이터를 레플리케이션하여 모든 데이터를 잃게 된다. 반면, 백업을 설정했다면 마스터 노드가 재부팅되더라도 백업 데이터를 로드해 자동으로 복구할 수 있고 데이터를 잃게 되는 문제가 발생하지 않을 것이다.
    따라서 레플리케이션은 데이터 가용성을 위한 기능일 뿐, 데이터 보존을 보장하지 않는다. 백업 기능은 레플리케이션과 별도로 설정해야 하며, 최소한 백업을 사용할 수 없는 경우에는 마스터 노드의 자동 재시작 옵션을 비활성화해 데이터 손실을 방지해야 한다.

  • 마스터 노드가 다운된다면?

    마스터 노드가 다운되면, 레플리케이션 구조에서는 다음과 같은 수작업이 필요하다. 먼저, 레플리카 노드에 접속해 읽기 전용 상태를 해제한 뒤, 애플리케이션 코드에서 Redis의 엔드포인트를 마스터 노드 대신 레플리카 노드의 IP로 변경해야 한다.
    이 과정은 수작업으로 수행되므로 복구에 시간이 소요된다. 특히, Redis를 캐시로 사용하고 있었던 경우, 복구 작업 동안 애플리케이션이 MySQL과 같은 원본 데이터 소스에 직접 접근하게 되어 부하가 몰리며 또 다른 문제가 발생할 수 있다.
    이러한 문제를 해결하려면 레플리케이션 구조 대신 센티널 구조를 사용하는 것이 적합하다.

센티널 구조

센티널 구조는 마스터 노드에 장애가 발생했을 때 자동으로 새로운 마스터를 선출하고, 애플리케이션이 새로운 마스터에 자동으로 연결되도록 지원하여 수작업 없이 빠르게 복구할 수 있는 구조이다. 이는 기존 Redis 인스턴스와는 다른 역할을 하는 별도의 프로그램인 센티널을 이용하는 방식이다.
센티널은 자동 페일오버 기능을 통해 마스터 노드의 장애를 감지하고, 레플리카 노드를 새로운 마스터로 승격시켜 Redis의 다운타임을 최소화한다. 클라이언트는 센티널에게 마스터 노드의 정보를 요청해 작동하며, 페일오버가 발생해도 센티널이 변경된 마스터 정보를 다시 전달하므로 Redis 엔드포인트 정보를 애플리케이션 코드에서 변경할 필요가 없다.

  • 센티널은 몇개가 적당할까?

    센티널은 MongoDB의 config 서버와 비슷하게 3개 이상의 홀수개로 구성할 것을 추천한다. 이는 센티널이 SPOF가 되는 것을 방지하기 위해 최소 3대 이상으로 구성하여 하나의 센티널이 이상이 생기더라도 다른 센티널이 계속해서 역할을 수행할 수 있게 하기 위함이다. 또한, 홀수개로 구성해야하는 이유는 센티널이 잘못된 판단을 할까봐 쿼럼이라는 개념을 사용하기 떄문인데, 간단하게 말해서 쿼럼의 수만큼 찬성한다면 페일 오버 프로세스를 실행하기 떄문이다. 즉, 과반수가 넘어야지 페일오버를 진행하기 떄문이다. 당연히 SPOF를 방지하기 위해 각 센티널은 서로 다른 가용 영역(Availability Zone에 배치하는 것을 추천한다. 이를 통해 특정 영역에서 장애가 발생하더라도 나머지 센티널이 정상적으로 작동할 수 있다.

  • 페일오버는 어떻게 진행될까?

    센티널은 마스터 노드에게 PING을 보낸 뒤, 응답이 일정 시간 안에 도착하지 않으면 마스터가 다운됐다고 판단한다. 이때 우선 'sdown'이라고 표시하여 주관적인 다운 상태로 간주한다. 이후 센티널 노드는 다른 센티널들에게 장애 사실을 전파해, 다른 센티널들도 장애를 인지했는지 확인한다. 만약 쿼럼 값 이상의 센티널이 장애를 인지했다고 판단되면 'odown'이라고 표시하며, 이는 객관적인 다운 상태로 간주된다.
    처음으로 마스터 노드를 'odown' 상태로 인지한 센티널이 페일오버를 진행한다. 이 과정에서 센티널은 '에포크(epoch)' 값을 이용하는데, 에포크는 일종의 버전 값으로, 페일오버 진행 중 모든 센티널이 동일한 작업을 시도하고 있음을 보장한다.
    우선 에포크 값을 기반으로 센티널들 중에서 '센티널 리더'를 선출한다. 이때 센티널 리더 선출에는 쿼럼 값이 아닌 과반수를 기준으로 한다. 마스터 노드가 'odown' 상태로 표시되고 센티널 리더 선출이 완료되면, 센티널 리더가 페일오버를 시도한다.
    페일오버 과정에서는 먼저 새로운 마스터가 될 레플리카 노드를 선정한다. 이때 레플리카 선정 기준은 다음과 같다.
    1. 'replica-priority' 값이 낮은 레플리카 노드
    2. 마스터 노드로부터 더 많은 데이터를 수신한 레플리카 노드
    3. RunID가 사전 순으로 가장 작은 레플리카 노드
    선정된 레플리카 노드는 마스터 노드로 승격되며, 해당 노드의 복제 설정이 해제된다. 이후 나머지 레플리카 노드들이 새롭게 선정된 마스터 노드를 바라보도록 설정을 변경하고, 센티널 노드들도 구성 정보를 업데이트하면 페일오버 과정이 완료된다.

  • 레플리카 노드에도 'odown' 표시를 할까?

    레플리카 노드에서 장애가 발생한 경우, 센티널은 'sdown' 상태로 표시한다. 하지만 마스터 노드와 달리 레플리카 노드에 대해서는 'odown' 상태로 전환하지 않는다. 이는 레플리카 노드의 장애 사실을 다른 센티널 노드에게 전파하지 않기 때문이다.
    즉, 마스터 노드에서만 장애 전파가 이루어지며, 'odown' 표시가 적용된다. 레플리카 노드에서 'sdown' 표시를 사용하는 주된 이유는, 해당 레플리카가 마스터 노드로 선출되지 않도록 하기 위함이다.

  • 마스터로 선출하기 싫은 레플리카 노드가 있다면?

    페일오버 과정에서 성능이 비교적 좋지 않은 인스턴스에서 작동하는 레플리카 노드가 마스터로 선출되지 않기를 원하는 경우가 있을 수 있다. Redis는 이를 위해 앞에서 언급한 'replica-priority'라는 파라미터를 제공한다.
    이 파라미터의 기본값은 '100'이며, 해당 값을 낮추면 선출 우선순위가 낮아진다. 만약 '0'으로 설정하면 해당 레플리카 노드는 절대 마스터로 선출되지 않는다. 이를 통해 특정 레플리카 노드가 마스터 역할을 맡지 않도록 제어할 수 있으며, 서비스의 안정성과 성능 요구 사항을 충족할 수 있다.

  • 주의점?

    센티널 구조를 사용할 때 특히 주의해야 할 점은 네트워크 단절로 인해 발생할 수 있는 문제이다.
    만약 네트워크 단절이 발생해 일부 센티널이 기존 마스터 노드에 접근할 수 없게 되면, 해당 센티널들은 마스터 노드가 다운되었다고 판단하고 새로운 마스터를 선출할 수 있다. 이로 인해 스플릿 브레인(Split Brain) 현상이 발생하여 두 개의 마스터 노드가 존재하게 된다.
    이 상황에서 네트워크 단절이 해소되면, 기존 마스터 노드는 새롭게 선출된 마스터 노드의 레플리카 노드로 강등된다. 이 과정에서 기존 마스터 노드가 처리하던 데이터가 새롭게 선출된 마스터와 동기화되지 않을 수 있어, 기존 마스터 노드로 보내졌던 데이터가 유실될 위험이 있다.
    이를 방지하기 위해 네트워크의 안정성을 보장하는 것이 중요하며, 센티널 설정 시 쿼럼 값과 타임아웃 값을 신중히 조정해야 한다.

  • 고가용성은 좋은데 확장성은?

    지금까지 살펴본 대로 센티널 구조는 레플리케이션 구조에 비해 고가용성을 제공한다. 하지만 시스템 트래픽이 점점 증가하면서 용량 및 성능을 확장해야 하는 순간이 올 수 있다.
    센티널 구조에서는 여전히 마스터 노드에서만 쓰기 작업이 이루어지기 때문에, 마스터 노드의 메모리가 가득 차면 데이터를 저장할 때 키 이빅션(eviction) 현상이 발생할 수 있다. 이를 해결하기 위해 스케일 업(scale-up) 또는 스케일 아웃(scale-out)을 고려해야 한다.
    스케일 업은 더 큰 메모리를 가진 인스턴스를 도입하는 방식으로 메모리 문제는 해결할 수 있다. 하지만 처리량을 늘리기 위해 CPU 코어 수를 증가시키는 경우, Redis가 단일 스레드로 동작하기 때문에 성능 향상 대비 비용 효과가 낮아 가성비가 떨어질 수 있다.
    이 때문에 스케일 아웃을 고려하는 것이 더 적합하다. Redis는 클러스터 구조를 통해 스케일 아웃을 지원하며, 데이터를 샤딩(sharding)하여 여러 노드에 분산 저장함으로써 처리량을 늘리고 시스템의 확장성을 확보할 수 있다.
    센티널 구조는 고가용성에 초점을 맞춘 솔루션이지만, 확장성을 필요로 하는 경우에는 Redis 클러스터 구조를 검토하는 것이 바람직하다.

클러스터 구조

클러스터 구조는 애플리케이션 아키텍처의 변경 없이 여러 Redis 인스턴스 간 수평 확장이 가능하도록 하며, 데이터의 분산 처리, 복제, 자동 페일오버 기능을 지원한다. 즉, 확장성과 고가용성을 모두 갖춘 구조이다.

  • 확장성을 보장하는 방법?

    Redis는 확장성을 위한 데이터 분산 처리를 위해 데이터 샤딩을 지원한다. 클러스터에서 데이터는 키를 기반으로 샤딩되며, 하나의 키는 항상 하나의 마스터 노드에 매핑된다. 클러스터에 속한 모든 노드는 키가 저장될 마스터 노드를 알고 있기 때문에, 다른 노드가 데이터를 읽거나 쓰려 할 때 해당 키가 할당된 마스터 노드로 연결을 리다이렉션한다.
    이러한 구조는 Redis 노드와 애플리케이션의 Redis 클라이언트만으로 처리되므로 추가적인 아키텍처가 필요하지 않으며, 애플리케이션의 소스 코드 로직을 변경할 필요도 없다. 참고로 Redis 클러스터 모드에서는 마스터 노드를 최대 1000개까지 확장할 수 있다.

  • 고가용성을 보장하는 방법?

    클러스터 구조는 센티널을 사용하지 않고도 자동 페일오버를 지원하여 고가용성을 보장한다. 클러스터 구성에서는 최소 3대의 마스터 노드와 각각의 복제본 노드를 구성하는 것을 추천한다.
    클러스터 내의 노드들은 서로를 모니터링하며, 마스터 노드에 장애가 발생할 경우 연결된 복제본 노드를 마스터로 자동 승격(페일오버)시키는 작업을 수행한다. 이는 센티널의 역할을 대신하며, 높은 가용성을 보장한다.
    또한, 마스터에 연결된 복제본이 없더라도 다른 마스터 노드의 남는 복제본을 새로운 마스터로 연결할 수 있는 기능도 가지고 있다.
    클러스터의 노드들은 서로를 모니터링하는 풀 메시 토폴로지 형태로 구성되며, 노드 간 통신을 위해 클러스터 버스라는 프로토콜을 사용한다. 간단하게 이야기하면 모든 레디스 클러스터 노드는 다른 레디스 클로스터 노드에서 들어오는 연결을 수신하기 위해 클라이언트의 연결과는 독립적인 추가 TCP 포트가 열려있고, 이를 이용해서 통신 한다.
    이러한 통신 구조에서 노드 간 메시지가 너무 많이 교환될 것을 걱정할 수도 있지만, 클러스터는 정상적인 상태에서는 노드 간 메시지 교환을 최소화하도록 설계되어 있다. 따라서 노드 수가 증가하더라도 메시지 교환량이 기하급수적으로 늘어나지 않아 확장성과 성능을 동시에 유지할 수 있다.

  • 데이터 샤딩은 어떻게 진행될까?

    클러스터 구조는 데이터 샤딩을 해시 슬롯을 이용해 지원한다. 총 16,384개의 해시 슬롯을 마스터 노드들이 나눠 갖고 있는 구조로, Redis에 입력되는 데이터를 CRC16 해시 함수로 처리한 뒤 16,384로 나눈 나머지 값을 이용해 해시 슬롯을 선정하고 데이터를 저장한다.
    이 방식은 데이터를 저장할 때뿐만 아니라 읽어올 때도 동일하게 해시 슬롯 값을 계산하여 해당 해시 슬롯을 소유한 마스터 노드에서 데이터를 읽어오는 방식으로 동작한다. 이러한 구조 덕분에 해시 슬롯만 이동시키면 마스터 노드의 추가나 삭제를 간단히 처리할 수 있어 손쉬운 확장과 축소가 가능하다.

  • 여러 개의 마스터 노드에서 한 번에 데이터를 읽어올 순 없을까?

    Redis는 'MGET' 명령을 사용해 여러 키에 대해 한 번에 데이터를 읽어오는 기능을 제공한다. 하지만 클러스터 모드에서는 이 기능을 지원하지 않는다. 이유는 서로 다른 해시 슬롯에 속한 키들에 대해 Redis 클라이언트가 내부적으로 리다이렉션을 수행해야 하며, 이로 인해 한 번에 접근이 불가능하기 때문이다.
    이 문제를 해결하려면 해시 태그를 사용할 수 있다. 해시 태그는 특정 값을 기반으로 해시를 수행해 관련 데이터를 동일한 해시 슬롯에 저장하는 방식이다. 이를 통해 특정 키들이 하나의 마스터 노드에 저장되는 것이 보장되며, 'MGET'을 사용할 때도 해시 태그를 통해 데이터를 조회할 수 있다.
    하지만 이 방법은 데이터가 특정 마스터 노드에 집중될 가능성을 높인다. 이로 인해 클러스터 모드의 본래 장점인 분산 처리가 약화될 수 있으며, 클러스터 모드를 사용하는 의미가 불분명해질 위험이 있다.

  • 페일오버는 어떻게 진행될까?

    Redis 클러스터 구조에서 페일오버는 두 가지 방식으로 이루어진다. 첫 번째는 자동 페일오버 기능이고, 두 번째는 레플리카 마이그레이션 기능이다.
    자동 페일오버는 마스터 노드에 장애가 발생했을 때, 해당 마스터 노드의 레플리카 노드를 마스터로 승격시키는 작업이다. 레플리카 마이그레이션은 마스터 노드의 레플리카 노드가 없을 경우, 다른 마스터 노드의 남는 레플리카 노드를 가져와 해당 마스터 노드에 연결하는 작업을 말한다.
    자동 페일오버는 마스터 노드에 장애가 발생했을 경우, 다른 마스터 노드들 간의 투표를 통해 진행된다. 이 과정에서 해당 마스터 노드에 레플리카 노드가 없다면 레플리카 마이그레이션 작업이 추가적으로 수행된다.
    이런 페일오버 기능들은 클러스터 구조의 특성상 매우 중요하다. 클러스터에서 마스터 노드가 하나라도 정상적으로 동작하지 않으면, 해당 마스터 노드가 담당하던 해시 슬롯을 사용할 수 없게 되고, 결국 클러스터 전체를 사용할 수 없게 된다. 참고로 이 설정은 'cluster-require-full-coverage' 옵션을 이용해서 문제가 된 해시슬롯을 가지고 있는 노드를 제외한 다른 노드들은 동작하도록 할 수 있지만 근본적인 문제인 고가용성을 확보하는 것을 추천한다.

  • 마스터 노드들은 어떻게 서로의 상태를 공유할까?

    클러스터 구조에서도 센티널 구조와 유사하게 PING과 PONG 패킷을 주고받아 노드 상태를 공유한다. 이러한 패킷을 하트비트 패킷이라고 부르며, 노드 간의 상태와 해시슬롯 같은 정보를 동기화하는 데 사용된다.
    하트비트 패킷은 단순히 본인의 정보만 담는 것이 아니라, 본인이 알고 있는 다른 노드들의 정보도 일부 랜덤하게 포함한다. 이를 통해 패킷을 수신한 노드가 자신이 알지 못했던 새로운 노드 정보를 획득할 수 있다.
    이 과정에서 클러스터는 에포크(epoch) 값을 사용한다. 에포크 값은 노드의 구성 상태를 나타내며, 값이 클수록 최신 구성을 가지고 있음을 의미한다. 노드들은 에포크 값을 기준으로 자신이 보유한 상태 정보를 업데이트한다.
    패킷을 보낸 노드뿐만 아니라, 패킷을 받은 노드도 자신의 에포크 값이 더 최신일 경우 업데이트 메시지를 포함해 정보를 다시 보낸다. 이를 통해 패킷을 보낸 노드도 최신 정보를 받을 수 있게 된다.

  • 마스터 노드들 간의 투표는 어떻게 진행될까?

    클러스터 구조도 센티널 구조와 마찬가지로 2가지 상태를 이용해 마스터 노드의 장애 상태를 표시한다. 만약 특정 노드에 PING을 보냈지만 일정 시간 동안 PONG 응답을 받지 못하면 'PFAIL'(Presumed Failure)로 표시한다.
    그리고 다른 노드들로부터 받은 하트비트 패킷에서 해당 노드가 'PFAIL' 상태로 표시되었다는 정보를 과반수 이상 수집하면, 이제 해당 노드를 'FAIL'(Failure) 상태로 확정한다.
    이제 레플리카 노드가 'FAIL' 상태인 마스터 노드가 1개 이상의 해시 슬롯을 가지고 있을 경우와 해당 레플리카가 마스터 노드와 레플리케이션이 끊어진 상태가 지속된 경우를 만족할 경우 페일오버를 시도한다.
    레플리카 노드는 자신이 새로운 마스터 노드로 선출되기 위해 다른 마스터 노드들에게 투표를 요청한다. 투표 요청을 받은 마스터 노드들은 해당 레플리카의 상태를 평가한 뒤, 조건을 만족할 경우 투표를 승인한다. 과반수 이상의 마스터 노드가 동의하면 해당 레플리카 노드는 새로운 마스터 노드로 승격된다.
    참고로 여러 개의 레플리카 노드가 동시에 마스터 노드로 선출되는 혼란을 방지하기 위해 에포크(epoch) 값을 사용한다. 에포크 값은 클러스터 상태의 버전 번호와 같으며, 더 높은 에포크 값을 가진 노드가 우선권을 갖는다. 이를 통해 동시다발적인 마스터 선출을 방지하고 클러스터의 안정성을 유지할 수 있다.

  • 레플리카 노드도 해시 슬롯을 할당받을까?

    마스터 노드와 동일한 데이터를 저장하기 위해 레플리카 노드도 해시 슬롯을 할당받아 사용하는지 궁금할 수 있다. 하지만 이는 아니다. 해시 슬롯은 마스터 노드에만 할당되며, 레플리카 노드는 마스터 노드의 데이터를 복제해 저장할 뿐 해시 슬롯을 직접 할당받지는 않는다.
    이러한 설계는 클러스터의 확장 및 축소 작업이나 레플리카 마이그레이션 과정에서 빠른 작업 수행을 가능하게 하기 위한 것으로 보인다.

  • 레플리카 노드에서 데이터를 읽으려면?

    일반적으로 레플리카 노드에서 데이터를 읽어와 성능을 향상시키고자 할 수 있다. 그러나 클러스터 모드에서는 기본적으로 레디스 클라이언트가 데이터를 요청하면 해당 키가 할당된 마스터 노드로 연결된다. 이로 인해 레플리카 노드로는 연결되지 않는다.
    심지어, 레플리카 노드에 접속하여 데이터를 직접 읽으려 시도할 경우에도, Redis는 오류를 반환하며 해당 키를 보유한 마스터 노드의 주소를 알려준다. 이는 클러스터 모드의 데이터 일관성을 보장하기 위한 동작이다.
    이 문제를 해결하려면 클라이언트와 레플리카 노드 간의 커넥션을 READONLY 모드로 전환해야 한다. READONLY 모드로 설정하면 클라이언트는 레플리카 노드의 데이터를 읽을 수 있으며, 이를 통해 읽기 작업을 분산하여 클러스터의 성능을 최적화할 수 있다.

  • 적절한 해시 슬롯을 가진 노드가 아닌 곳에 데이터를 저장한다면?

    특수한 상황에서 특정 마스터 노드에 직접 접속하여 데이터를 저장하려고 할 때, 해당 마스터 노드가 해당 데이터를 저장할 해시 슬롯을 소유하고 있지 않다면 어떻게 될까?
    이 경우 Redis는 오류를 반환하며, 데이터를 저장할 수 있는 적절한 해시 슬롯을 가진 노드의 주소를 알려준다. 이러한 동작은 클러스터의 일관성을 유지하기 위한 설계이다.
    이와 같은 불편함을 해결하기 위해 Redis 클라이언트에서 '-c' 옵션을 사용할 수 있다. '-c' 옵션을 활성화하면 Redis 클라이언트가 자동으로 리다이렉션을 수행하여 올바른 마스터 노드에서 명령을 실행하도록 한다.

  • 마스터 노드를 제거하려면?

    센티널 구조와 달리 클러스터 구조는 여러 개의 마스터 노드로 이루어져 있기 때문에, 마스터 노드를 제거할 경우 다른 마스터 노드들은 이를 자동으로 인지하지 못한다. 따라서 노드를 제거하는 작업뿐만 아니라, 클러스터 내 다른 마스터 노드들에게 해당 변경 사항을 알리는 작업도 반드시 수행해야 한다.
    또한, 마스터 노드를 제거하기 전에 해당 노드에 저장된 데이터가 없어야 한다. 이를 위해서는 해당 노드에 할당된 해시 슬롯을 모두 다른 마스터 노드로 이동시키는 리샤딩(resharding) 작업을 먼저 수행해야 한다. 해시 슬롯이 없는 상태에서만 안전하게 마스터 노드를 제거할 수 있다.

  • 클러스터를 추가 또는 제거하는 동안은 어떻게 동작할까?

    클러스터에 노드를 추가하거나 제거하는 작업은 기본적으로 해시 슬롯을 옮기는 작업으로 볼 수 있다. 예를 들어, 노드 A에서 노드 B로 해시 슬롯을 옮기는 작업을 수행한다고 가정하면 다음과 같은 단계로 진행된다. 참고로 이 작업은 원자적(Atomic)으로 동작하며, 마이그레이션 동안 락이 걸리기 때문에 경쟁 상황이 발생하지 않는다. 이러한 방식으로 데이터 이동 중에도 클러스터가 안정적으로 작동할 수 있다.

    1. 기존 키를 읽는 요청은 여전히 노드 A에서 처리하고, 새로운 키를 생성하는 요청은 노드 B에서 처리하도록 설정한다.
    2. 노드 A의 데이터를 노드 B로 마이그레이션한다. 마이그레이션은 하나의 키 단위로 진행되며, 데이터 전송이 완료된 키는 점차적으로 노드 A에서 삭제한다.
    3. 최종적으로 노드 A에는 해당 해시 슬롯과 관련된 데이터가 남지 않게 된다.
  • 마이그레이션 중에 클라이언트의 요청은 어떻게 처리할까?

    마이그레이션 중에도 클라이언트의 요청은 처리되어야 한다. 이때 기존 노드와 새로운 노드 중 어느 노드에 요청해야 할지 클라이언트는 알지 못하므로, 최악의 경우 키를 조회하기 위해 두 노드 모두에 요청을 보내야 할 수도 있다.
    이러한 비효율성을 해결하기 위해 Redis 클러스터는 MOVED와 ASK 리다이렉션 메커니즘을 지원한다. 이 메커니즘은 클러스터 추가 및 제거 작업 중에도 클라이언트와 클러스터 간의 통신을 효율적으로 유지하고, 데이터를 안정적으로 처리할 수 있도록 돕는다.

    • 만약 Redis 클라이언트가 새로운 노드 B로 데이터를 요청했지만 해당 키가 아직 노드 A에 남아있는 경우, 클러스터는 ASK 리다이렉션을 반환한다. 이는 클라이언트가 현재 요청은 노드 A로 보내도록 하지만, 클라이언트의 해시 슬롯 맵을 업데이트하지 않아 이후 요청은 다시 노드 B로 보내도록 한다.
    • 반대로, 클라이언트가 노드 A로 요청을 보냈지만 키가 이미 새로운 노드 B로 마이그레이션된 경우, MOVED 리다이렉션을 반환한다. 이 경우 클라이언트는 요청을 노드 B로 전송하며, 해시 슬롯 맵을 업데이트해 이후 요청도 노드 B로 보내도록 한다.

2. 캐시

Redis는 모든 데이터를 메모리에 저장하는 인메모리 데이터 저장소이기 때문에 매우 빠른 속도를 제공하며, 이 때문에 캐시로 자주 사용된다. 캐싱 전략에 대해서는 흔히 다뤄지는 주제이므로 여기서는 다른 주제에 대해 다뤄보겠다.

  • 캐시가 가득 찬다면?

    Redis는 데이터베이스의 모든 정보를 저장할 수 없으므로, 일부만을 저장해야 하며, 캐시가 가득 찰 경우를 대비한 전략이 필요하다. Redis에 데이터를 저장할 때 적절한 TTL 값을 설정하는 것이 좋다.
    하지만 TTL만으로는 충분하지 않다. 메모리가 가득 찰 경우, 데이터를 정리하는 전략이 필요하다. Redis는 기본적으로 Noeviction 설정을 사용하며, 이 경우 메모리가 가득 차면 오류를 발생시킨다. 따라서 LRU (Least Recently Used), LFU (Least Frequently Used), RANDOM과 같은 Eviction 정책을 활용해야 한다. 또한, 각각의 정책에서 volatile 옵션이 있는데, 이는 만료 시간이 설정된 키에 대해서만 정리 작업을 수행한다. 하지만 만료 시간이 없는 키가 존재하면 이 옵션에서도 Noeviction처럼 오류가 발생할 수 있으므로 주의해서 사용해야 한다.

  • 만료시간으로 인한 문제점?

    Redis의 키가 만료되었을 때, 여러 애플리케이션이 Redis를 동시에 바라보고 있다면, 키 만료로 인해 데이터베이스와 Redis로 트래픽이 몰리는 문제가 발생할 수 있다.
    이를 방지하기 위해 만료 시간을 적절히 설정해야 하며, 랜덤 확률로 캐시 갱신, PER (Predictive Expiration Refresh) 알고리즘 같은 방법을 고려할 수 있다.

3. 스트림

Redis Streams는 Kafka의 영향을 많이 받아 여러 가지 방면에서 비슷한 기능을 가지고 있다. 여기에서는 간단하게 차이점에 대해 다뤄보겠다.

  • 순서를 보장하려면?

    Kafka에서 메시지는 토픽에 저장될 때 해시 함수에 의해 여러 개의 파티션에 분배된다. 소비자가 토픽에서 데이터를 소비할 때에는 파티션의 존재를 알지 못하고, 토픽 내의 전체 파티션에서 데이터를 읽어오기 때문에 데이터의 순서가 보장되지 않는다. 이를 해결하기 위해 특정 파티션에 할당된 데이터를 읽는 방식으로 순서를 유지할 수 있지만, 이 경우 하나의 파티션밖에 사용하지 못하는 단점이 있다.
    반면 Redis Streams는 단일 스트림 내에서는 메시지의 순서를 기본적으로 보장하고 파티션이라는 개념도 없다. 때문에 소비자 그룹은 Kafka와는 다른 방식으로 작동하며, Redis Streams의 소비자 그룹 내 소비자는 다른 소비자가 아직 읽지 않은 데이터만을 읽어가도록 하여 병렬적으로 데이터를 처리할 수 있다.

  • 데이터를 처리할 때 시스템이 종료된다면?

    Redis Streams를 이용 중에 시스템이 종료된다면, 재처리를 위한 기능이 필요할 것이다. Redis Streams는 이를 위해 Pending Entries List(보류 리스트)를 지원한다. 소비자가 메시지를 성공적으로 처리했다는 ACK를 보내면, 보류 리스트에서 해당 메시지를 삭제한다. ACK를 보내지 못한 메시지는 계속 보류 리스트에 남아 있어 재처리가 가능하다.

  • 주의점?

    앞에서 이야기 했듯이, Redis Streams는 별도의 설정 없이도 단일 스트림에서는 메시지 순서를 보장한다는 장점이 있다. 하지만 클러스터 구조에서는 여러 개의 스트림을 사용해야 부하를 분산할 수 있다. 이 경우 메시지 순서를 보장할 수 없다.
    또한 Redis Streams는 메모리 기반이기 때문에 메모리 관리에도 신경을 써야 한다. ACK를 받지 못한 메시지가 보류 리스트에 계속 쌓이면 메모리에 부하가 가중될 수 있다. 이를 해결하려면 주기적으로 보류 리스트를 정리하는 로직을 구현해야 한다.

4. 백업

Redis는 복제와 별개로 백업 작업을 수행할 것을 적극 추천한다. 앞에서 이야기 했던 대로, 마스터 노드가 재부팅되면서 메모리 내용이 초기화된 후 이를 레플리카 노드가 복제한다면, 마스터와 레플리카 모두 데이터를 잃게 되고 복구할 방법이 없어질 수 있다. 그만큼 백업이 중요하므로 백업과 관련해 몇 가지를 살펴보자.

  • 백업하는 방식?

    Redis는 백업을 위해 AOF(Append Only File)와 RDB(Redis Database) 두 가지 방법을 지원한다. AOF는 Redis가 처리한 모든 쓰기 작업을 차례대로 기록하며, 이를 통해 실행된 명령을 재현해 데이터를 복구할 수 있다. 반면, RDB는 특정 시점의 메모리 데이터를 덤프(dump)하여 저장하는 방식으로, 시점 단위로 여러 백업본을 생성할 수 있다.
    RDB는 AOF보다 복구 속도가 빠르며 디스크 공간도 적게 차지한다는 장점이 있지만, 특정 시점 이후의 세부 데이터는 복구할 수 없는 단점이 있다. AOF는 이러한 한계를 극복할 수 있으나, 파일 크기가 커지고 주기적으로 재작성 작업이 필요하다. 두 가지 방식은 동시에 사용할 수도 있으며, 자신의 상황을 고려하여 적절한 백업 방식을 선택할 것을 추천한다.
    참고로, Amazon ElastiCache는 Redis 3.0 이상 버전부터 AOF를 지원하지 않으며, RDB만 지원한다.

  • RDB에 대해서

    RDB는 사용자가 원하는 시점에 데이터를 스냅샷처럼 저장하는 방식이다. 예를 들어, 한 시간에 한 번 RDB 파일을 생성하고 이를 장애 발생 시 복구에 사용하는 방식이다.
    RDB 백업을 수행할 때는 SAVE 커맨드보다 BGSAVE 커맨드를 사용하는 것이 좋다. SAVE 커맨드는 동기 방식으로 동작해 다른 클라이언트의 요청을 차단할 수 있는 반면, BGSAVE는 자식 프로세스에서 작업을 처리해 다른 클라이언트의 요청을 계속 처리할 수 있다.
    또한, RDB는 레플리카 노드가 마스터 노드로부터 데이터를 복제할 때 사용되기도 한다. 이 경우 RDB 파일을 전송하여 데이터 복제를 완료한다.

  • AOF에 대해서

    AOF는 Redis가 수행한 모든 쓰기 작업의 로그를 기록하며, 특정 시점으로 복구할 수 있다는 장점이 있다. 예를 들어 FLUSHALL 커맨드로 모든 데이터를 삭제했더라도, AOF 파일에서 해당 명령을 삭제하면 데이터를 복구할 수 있다.
    AOF의 단점은 파일 크기가 점점 커질 수 있다는 점이다. 이를 해결하기 위해 Redis는 주기적으로 데이터를 압축해 파일을 재작성하는 작업을 수행한다. 과거에는 RDB와 AOF 데이터를 하나의 파일에 통합해 관리했으나, 현재는 별도의 파일로 관리하도록 개선되었다.

  • 백업은 안전할까?

    RDB는 당연하게도 안전하지 않고, AOF도 RDB보다 데이터 복구 측면에서 안전하다고 평가되지만, 주의해야 할 점이 있다. AOF도 결국 write 시스템 콜을 사용하기 때문에 데이터가 커널 영역의 OS 버퍼에 임시 저장된 후 디스크에 기록된다. 이 과정에서 전원 장애가 발생하면 데이터를 유실할 가능성이 있다.

  • 어떻게 해결하지?

    이 문제를 해결하기 위해 Redis는 APPENDSYNC 옵션을 제공한다.

    • always: 모든 쓰기 작업 후 디스크에 동기화하며, 가장 안전하지만 성능이 가장 느리다.
    • everysec: 1초에 한 번 디스크에 동기화하며, 성능과 안정성의 균형을 제공한다. (기본값)
    • no: 동기화를 운영체제에 맡기며, 가장 빠르지만 데이터 유실 가능성이 크다.
  • 주의점?

    지금까지 강조했던 대로, 메모리에 신경 써야 한다. 백업은 대부분 자식 프로세스에서 수행되며, 이 과정에서 부모 프로세스의 메모리를 복사해 작업이 진행된다. 최악의 경우 기존 메모리의 두 배를 사용할 수 있기 때문에, 메모리가 부족해 장애가 발생하지 않도록 시스템 메모리 용량을 충분히 확보해야 한다. 이러한 점을 고려해 백업 주기와 메모리 사용량을 신중히 설계해야 한다.

5. Off The Record

Redis는 시대적 요구에 딱 맞게 태어난 데이터 저장소라고 생각한다. 최근 들어 속도가 중요한 시스템에서 Redis의 필요성이 분명히 드러났고, 이를 해결하기 위한 선택으로 Redis가 자리 잡았다. 물론, 이런 요구 속에서 Redis가 탄생했을 수도 있지만, 내가 주목하는 부분은 Redis가 기존의 틀을 깬 신선한 시도를 했다는 점이다. 데이터 저장소는 기본적으로 데이터를 영구적으로 저장해야 한다는 고정관념이 있었지만, Redis는 이를 과감히 벗어던지고도 현재 가장 인기 있는 데이터 저장소 중 하나로 자리 잡았다.

나로서는 이런 틀을 깨는 시도가 정말 신기하게 느껴진다. 개발자로서 오래 성장하고 살아남기 위해서는 편협한 시선을 버리고 새로운 접근을 받아들일 줄 아는 자세가 중요하다는 것을 다시금 느끼게 된다.

평소 Redis에 대해 많은 관심을 가져왔고, 직접 사용해보고 싶다는 생각도 있었지만, 개인적으로 Redis와 같은 도구는 성능 개선의 최후의 수단이라고 생각해왔다. 내가 말하고자 하는 것은, 문제 해결 과정에서 먼저 애플리케이션 단계에서의 최적화나 쿼리 개선 같은 접근을 충분히 시도해보고, 그래도 해결되지 않을 때 비로소 Redis와 같은 도구를 도입해야 한다는 것이다.

이러한 이유로 지금까지 진행한 프로젝트에서는 Redis까지 도입하지 않아도 문제를 해결할 수 있다고 판단해 적용한 적이 없다. 성능 개선의 필요성이 명확하지 않은 상황에서 Redis를 도입하는 것은 오히려 과도한 엔지니어링이라고 생각했기 때문이다.

그래서 빨리 현업에 나가 지금까지 공부한 내용을 실제로 적용해보고 싶은 마음이 크다. 물론 지금 당장 언제 그런 기회를 가질 수 있을지 모르기 때문에, 학습을 목적으로 Redis를 직접 적용해보는 것도 좋은 경험이 될 것 같다. 나는 왜 이렇게 욕심이 많을까 ㅎㅎ..

profile
노를 젓다 보면 언젠가는 물이 들어오겠지.

0개의 댓글