Redis의 HA(High Availability) 전략 알아보기!
Redis(이하 레디스)는 Key-Value 형태의 데이터를 저장할 수 있는 일종의 NoSQL 데이터베이스 중 하나이다.
레디스는 특히 일반적인 DBMS 시스템과 다르게 디스크가 아닌 메모리에 데이터를 저장하기 때문에 데이터 조회 성능에서 매우 빠른 성능을 보인다.
또한, 레디스 내부에서 값 삽입을 비롯한 Operation 자체를 모두 싱글 스레드로 처리하기 때문에 동일 자원에 대한 레이스 컨디션으로 발생할 수 있는 문제를 원천적으로 예방해준다. (이것이 가능한 이유는 역시나 레디스가 메모리 기반으로 데이터를 관리해 빠른 속도를 보장하기 때문일 것이다.)
이러한 특징 때문에 레디스는 캐싱의 용도로 많이 사용되며, 그 밖에도 동시성 이슈가 우려되는 자원에 대한 락을 구현하기 위해서도 많이 사용된다.
그러나, 레디스의 데이터가 메모리 위에서 관리된다는 특징 때문에 예상치 못한 문제로 레디스가 다운되거나 혹은 아예 서버 자체가 꺼져버리면 레디스에 적재된 모든 데이터 또한 휘발성으로 날아가버린다는 문제가 존재한다.
따라서 레디스를 사용하는 것만큼이나 레디스를 잘 관리하는 것이 매우 중요하다.
Redis의 고가용성(High Availability) 전략
Replication (복제)
레디스에선 Master - Slave 형태의 복제(Replication)를 제공한다.
Master 노드에 쓰이는 모든 데이터는 비동기로 Slave 노드에 실시간으로 복제된다.
이는 내부적으로 Master 노드가 데이터에 대한 덤프파일을 만들어 백그라운드로 Slave 노드에게 보내고, 이를 Slave 노드가 자신의 메모리에 데이터를 로드하는 방식으로 진행된다.
대략적인 플로우는 위와 같다고 볼 수 있다.
만약 마스터 노드가 어떠한 이유로 접근이 불가능해지거나 혹은 데이터가 모두 날아가는 상황이 발생하더라도, 데이터가 덤프된 상태의 슬레이브 노드를 새 마스터 노드로 승격시켜 사용하면 예기치 못한 장애 상황에 대처할 수 있다.
하나의 마스터 노드에 대해 여러 개의 슬레이브 노드를 두는 것은 가능하지만, 하나의 레플리카 그룹에서 마스터 노드는 하나만 존재할 수 있다.
즉, 다중의 마스터 노드를 구축하여 이를 모두 하나의 슬레이브 노드에 저장한다거나 하는 작업은 불가하다.
Sentinel (보초)
Replication을 통해 마스터 - 슬레이브 구조로 레디스 데이터를 복제하여 예상치 못한 장애 발생 시 복제된 노드 및 데이터를 사용할 수 있다는 것은 확인했다.
그러나 실제 장애 환경에서 마스터 노드 다운 감지 -> 마스터 노드의 마스터 연결 해제 -> 슬레이브 노드를 마스터 노드로 승격 -> 배포와 같은 일련의 작업들은 결코 어느 한 단계도 만만하지 않다.
마스터 노드가 다운이 되건말건 실제 운영 중인 서비스의 트래픽은 멈추지않고 들어올 것이고, 장애 대응이 취약할 수 있는 새벽시간대 문제가 발생하여 복구가 늦어지기라도 한다면 그만큼 장애의 영향도는 높아질 것이다.
결국엔 수동적인 복구 처리보단 시스템에 의한 자동화 방법이 필요하며, Sentinel을 구축하여 이러한 문제를 해결할 수 있다.
Sentinel의 사전적 의미는 보초이며, Sentienl은 말 그대로 마스터 노드와 슬레이브 노드를 실시간으로 모니터링하며 장애 상황 발생 시 슬레이브 노드를 마스터로 승격시키는 자동 failover 작업을 수행하는 역할을 한다.
Sentienl을 구축한 후의 레디스 작업 플로우는 아래와 같다.
애플리케이션은 더 이상 마스터 노드를 직접 바라보지 않고 Sentinel을 바라보게 되며, Sentinel을 통해 마스터 노드의 IP와 PORT를 획득해 마스터 노드로 접근하는 형태가 된다.
Sentinel이 마스터 노드로 접근하기 위한 일종의 프록시와 같이 작동한다고 보면 되겠다.
그림에도 명시한 것처럼, Sentinel이 failover을 합리적으로 진행하기 위해선 적어도 세 개 이상의 Sentinel 인스턴스를 사용하는 것이 권장된다.
그 이유는, Sentinel 인스턴스 중 정족수(Quorum)가 동의해야 failover를 진행시킬 수 있기 때문이다.
정족수는 사용자가 직접 설정할 수 있지만 Sentinel 인스턴스의 과반수로 설정하는 것이 일반적이며 따라서 세 개 이상의 홀수 개의 인스턴스를 사용하는 것이 권고된다.
Sentinel을 통한 failover는 아래의 단계를 거쳐 완료된다.
- Sentinel 중 하나가 마스터 노드의 장애를 감지한다.
- 다른 Sentinel들에게 failover 진행 여부를 투표한다.
- 정족수 이상의 Sentinel이 failover에 동의한 경우, failover를 진행한다.
- 마스터 노드에 대한 연결을 끊는다.
- 슬레이브 노드 중 하나를 마스터로 승격시킨다.
- 승격된 슬레이브 노드 외의 다른 모든 슬레이브 노드를 새로 승격된 마스터 노드에 연결한다.
- 애플리케이션의 요청에 대해 새로 승격된 마스터 노드의 IP, PORT 를 반환한다.
Sentinel의 중요한 특징 중 하나는 그것이 분산 시스템에서 작동한다는 것에 있다.
이는 failover 작동을 위한 정족수와 연관이 있는데, Sentinel의 failover 진행 투표는 정해진 정족수 이상의 Sentinel이 연결 가능하기만 하다면 수행이 가능하다.
즉, 만약 5개의 Sentinel 인스턴스가 띄워져있고 정족수가 2로 정해져있을 때 3개의 Sentinel 인스턴스가 예기치 못한 문제로 다운되어 통신이 불가능하다고 하더라도 정족수에 해당하는 2개의 인스턴스가 정상적으로 작동하고, failover 진행에 동의한다면 failover 시스템이 작동하게 된다.
이는 레디스 시스템의 장애 복구가 Sentinel 프로세스의 작동 여부 자체에 의존하지 않기 위함이며 궁극적으로는 Sentinel이 단일 장애 포인트가 되지 않도록 하기 위한 목적이라고 볼 수 있다.
Cluster (클러스터)
앞서 Replication에 대해 설명하며 하나의 복제 그룹에 존재할 수 있는 마스터 노드의 개수는 단 하나 뿐이라고 명시하였다.
그러나 해당 레디스에 부하가 지나치게 많아져 병목지점이 된다면 이를 수평적으로 확장해 스케일 아웃하는 방안이 필요하다.
레디스의 클러스터 기능은 Replication과 Sentinel의 기능에 더해 데이터를 수평적으로 확장하는 샤딩 기능까지 합쳐진 것이라 볼 수 있다.
레디스 클러스터에선 모든 마스터 노드와 슬레이브 노드가 서로 연결되어있으며, Gossip 프로토콜을 사용하여 서로 소통한다.
하나의 레디스 클러스터는 스토리지를 총 16384개의 슬롯이라는 단위로 이루고 있는데, 클러스터에 포함된 마스터 노드들은 이 슬롯들을 나눠갖게 된다.
즉, 위 그림과 같이 마스터 노드가 3개로 이루어진 클러스터라면 각 마스터 노드는 0 ~ 5460, 5461 ~ 10922, 10923 ~16383 구간의 슬롯을 나눠갖는다. (슬롯 번호는 0부터 시작)
따라서 이론상 클러스터 내에 포함될 수 있는 마스터 노드의 개수는 최대 16384개이지만, 이는 당연히 이론상 가능한 수치이며 실제론 최대 1000개의 상한선을 잡는 것이 권고된다고 한다.
이후, 해당 클러스터를 통해 레디스에 값을 삽입하게 되면 삽입하려는 값의 Key를 CRC(Cyclic Redundancy Check)-16 연산을 거친 값을 16384로 모듈러 연산하여 저장할 위치를 결정한다.
다시 말해, 레디스 클러스터는 모듈러 샤딩으로 여러 대의 마스터 노드들에 데이터를 분할 저장한다고 할 수 있다.
분산 데이터 조회 전략
그렇다면 이렇게 분산된 노드들에서 어떻게 클라이언트가 원하는 데이터를 찾아 작업을 수행할 수 있을까?
클라이언트는 마스터 / 슬레이브 여부와 상관없이 클러스터 내에 존재하는 모든 노드들에 자유롭게 쿼리를 보낼 수 있다.
만약 그 중 쿼리 요청이 실제 요청 데이터가 존재하는 노드로 이루어진다면 정상적으로 실행될 것이지만, 그렇지 않다면 요청 데이터가 존재하는 노드의 정보를 담은 리다이렉트 메시지를 반환한다.
이는 앞서 언급했듯 클러스터 내의 모든 노드들이 서로 상호연결된 상태를 유지하는 덕분에 가능하다.
물론 클라이언트에서 노드에 쿼리 실행 후, 리다이렉션 메시지가 응답으로 오는 경우 해당 위치로 다시 찾아가는 작업을 구현해주는 것이 필요하지만, 레디스 관련된 여러 라이브러리들에서 이를 이미 잘 지원해주고 있어 사실상 직접 구현할 필요는 없을 것으로 보인다.
레디스 클러스터의 리샤딩 전략
그렇다면 노드가 추가되거나 삭제되는 상황에선 어떻게 될까?
일반적인 데이터베이스의 모듈러 샤딩 방식에선 데이터베이스 증설 및 축소 시 리샤딩을 위한 재배치 작업이 꽤나 까다로운 작업이지만, 레디스 클러스터에서는 슬롯을 이동시키는 방식으로 간단하게 해결할 수 있다.
슬롯이라는 개념이 다소 낯설어 직관적으로 이해가 안된다면, 그냥 Key들을 저장한 Set 자료구조로 이해해도 좋다.
다시말해 슬롯이 3개의 구간으로 나누어져있다는 의미는 Key 세트가 3개 존재하는 것이며, 슬롯을 이동시킨다는 의미는 특정 Key 세트 중 일부를 다른 Key 세트로 그대로 이동시킨다는 것을 의미한다.
공식 문서를 보면 조금 더 이해가 쉽다.
From a practical point of view a hash slot is just a set of keys, so what Redis Cluster really does during resharding is to move keys from an instance to another instance. Moving a hash slot means moving all the keys that happen to hash into this hash slot.
https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/
이러한 작업은 클러스터 내에서 별도의 다운타운 없이 빠르게 진행될 수 있으며, 덕분에 클러스터의 마스터 노드들을 스케일 아웃하기 매우 유리하다.
failover 전략
마지막으로 레디스 클러스터에서의 failover 전략을 살펴보자.
클러스터 내부의 모든 노드들은 서로 연결된 상태를 유지하고 있으며, 주기적으로 헬스 체크를 위한 핑퐁(Ping & Pong)을 한다.
핑퐁을 하는 순서는 랜덤이지만, 모든 노드들은 서로 다른 모든 노드들에게 NODE_TIMEOUT 시간내에 한 번이라도 핑퐁을 하는 것이 보장된다.
그리고 해당 시간내에 과반수 이상의 마스터 노드가 특정 마스터 노드에 대해 NODE_TIMEOUT 이상의 시간동안 핑퐁에 실패하면 해당 노드가 다운되었다고 판단하고 슬레이브 노드를 마스터로 프로모션한다.
사실상 Sentiel이 하는 일을 서로 연결된 마스터 노드들이 대신 한다고 보면 이해가 편하다.
이러한 이유로, Sentiel에서 그러했듯 클러스터를 구성하는 마스터 노드의 최소 개수를 3개로 제한하고 있다.
레퍼런스
https://meetup.nhncloud.com/posts/226
https://nesoy.github.io/articles/2020-05/Redis-HA