[번역] Redis Cluster Specification: (1) Main properties and rationales of the design

ma2sql·2021년 11월 12일
1

원문: https://redis.io/topics/cluster-spec

Redis Cluster goals

레디스 클러스터는 설계에서 중요한 순서대로, 아래와 같은 목표를 가지는 레디스의 분산 형태의 구현이다.

  • 고성능, 그리고 선형으로 최대 1000개까지 확장이 가능하다. 프록시가 존재하지 않으며, 비동기 리플리케이션이 사용된다. 그리고 값(value)에 대한 병합 오퍼레이션이 수행되지 않는다.
  • 허용 가능한 쓰기 안전도: 시스템은 과반수 이상의 마스터 노드와 연결된 클라이언트로부터 온 모든 쓰기를 (최선의 방식으로) 유지하려고 한다. 보통 승인된 (acknowledged) 쓰기가 손실될 수 있는 작은 시간대(window)가 존재한다. 클라이언트가 소수 파티션에 있을 때, 승인된 쓰기를 손실할 수 있는 시간대가 더 크다.
  • 가용성: 레디스 클러스터는 과반수의 마스터 노드가 연결 가능한 상태이고, 더 이상 접속이 불가능한 각각의 마스터 노드에 대해서는 적어도 하나의 연결 가능한 리플리카가 있는 파티션에서는 살아남을 수 있다. 게다가 리플리카 마이그레이션(replica migration)을 이용해서, 어떤 리플라키에 의해서도 리플레케이션되고 있지 않은 마스터 노드는 다수의 리플리카를 보유하고 있는 마스터로부터 리플리카를 하나 받을 수 있다.

이 문서에서 설명된 것은 Redis 3.0 이상에서 구현된다.

Implemented subset

레디스 클러스터는 분산되지 않은 레디스의 버전에서 사용가능한 모든 단일 키 커맨드를 구현한다. 키가 모두 동일한 슬롯으로 해시된다면, 셋(Set) 타입의 합집합이나 교집합 연산과 같은 복잡한 멀티 키(multi-key) 오퍼레이션을 수행하는 커맨드도 구현된다.

레디스 클러스터는 특정 키들을 동일한 해시 슬롯에 저장되게 하기 위해서 사용될 수 있는 hash tags라고 불리는 개념을 구현한다. 그러나 메뉴얼 리샤딩 중에, 단일 키 오퍼레이션은 항상 사용이 가능한 것에 반해, 멀티 키(multi-key) 오퍼레이션은 특정 시간 동안은 사용할 수 없게 될 수도 있다.

레디스 클러스터는 레디스의 싱글 버전처럼 다중 데이터베이스를 지원하지는 않는다. 데이터베이스는 0번만 있으며, SELECT 커맨드는 허용되지 않는다.

Clients and Servers roles in the Redis Cluster protocol

레디스 클러스터에서 노드는 데이터를 보관하고, 올바른 노드로 키를 맵핑하는 것을 포함하여 클러스터의 상태를 가져오는 역할을 한다. 또한 클러스터 노드는 자동으로 다른 노드를 발견할 수 있으며, 동작하지 않는 노드를 발견하고, 장애가 발생한 상황에서 계속 동작하기 위해 필요할 때, 리플리카 노드를 마스터로 승격시킬 수 있다.

이러한 작업들을 실행하기 위해서 모든 클러스터 노드들은레디스 클러스터 버스(Redis Cluster Bus)라고 불리는 TCP 버스와 바이너리 프로토콜을 이용해서 연결되어 있다. 모든 노드는 클러스터 내의 다른 모든 노드와 클러스터 버스를 이용해서 연결되어 있다. 노드들은 새로운 노드를 찾기 위해서 클러스터에 관한 정보를 전파하거나, 다른 모든 노드가 적절히 동작하고 있는지를 확인하기 위해서 ping패킷을 보내거나, 그리고 특정한 컨디션을 알리기 위해서 필요한 클러스터 메시지를 보내기 위해서, 가십 프로토콜(gossip protocol)을 사용한다. 클러스터 버스는 또한 Pub/Sub 메시지를 클러스터 전체에 전파하기 위해서도 사용되고, 유저의 요청에 의한 메뉴얼 페일오버를 조정하기 위해서도 사용된다. (메뉴얼 페일오버는 레디스 클러스터의 장애 디텍터가 아닌, 시스템 관리자에 의해서 직접 시작하는 페일오버이다.)

클러스터 노드는 요청을 프록시(대신 전파)할 수 없기 때문에, 클라이언트는 -MOVED-ASK 리다이렉션 에러를 이용해서 다른 노드로 리다이렉트될 것이다. 클라이언트는 이론적으로 클러스터 내의 모든 노드로 자유롭게 요청을 보낼 수 있고, 필요하다면 리다이렉트되므로, 클라이언트는 클러스터의 상태를 유지할 필요가 없다. 하지만 키와 노드의 맵핑을 캐시할 수 있는 클라이언트는 합리적인 방식으로 성능을 향상시킬 수 있다.

Write safety

레디스 클러스터는 노드 간에 비동기 리플리케이션과 last failover wins이라는 묵시적 병합 기능을 사용한다. 이것은 마지막으로 선출된 마스터 데이터 셋이 결국 모든 리플리카를 대체하게 되는 것을 의미한다. 파티션동안 쓰기 데이터를 손실할 수 있는 시간대는 항상 있다. 그러나 과반수의 마스터와 연결된 클라이언트와 소수의 마스터와 연결된 클라이언트 경우, 이러한 시간의 크기는 매우 다르다.

레디스 클러스터는 소수의 마스터 측에서 실행된 쓰기와 비교해서 과반수의 마스터에 연결된 클라이언트로부터 실행된 쓰기를 유지하려고 더 열심히 노력한다. 다음은 클러스터가 실패하는 동안 과반수의 파티션에서의 수신한 승인된 쓰기의 손실이 이어질 수 있는 시나리오의 예이다.

  1. 쓰기(writes)는 마스터에 도달할 수 있지만, 마스터가 클라이언트에 응답하는 동안, 쓰기는 마스터와 리플리카 노드 사이에서 사용되는 비동기 리플리케이션을 통해서 리플리카로 전파되지 못할 수도 있다. 만약 쓰기를 리플리카로 전달하지 못하고 마스터가 죽게 되고, 리플리카 중 하나가 승격될만큼 긴 시간동안 접근할 수 없다면, 쓰기는 영원히 잃게 될 것이다. 마스터는 클라이언트에게 쓰기의 승인에 대해서 응답하는 것과 리플리카에게 쓰기를 전파하는 것을 거의 동시에 하려고 하기 때문에, 갑작스럽게 마스터가 완전히 실패하는 경우에는 이러한 것은 관측하기가 어렵다. 하지만 이것은 현실 세계에서의 실패 케이스다.

  2. 이론으로 쓰기가 손실될 수 있는 또 다른 실패 케이스는 다음과 같다.

  • 마스터가 파티션으로 연결되지 않는 상태이다.
  • 리플라키 중 하나로 페일오버 된다.
  • 일정 시간 후에, 마스터는 다시 연결할 수 있는 상태가 된다.
  • 갱신되지 않은 (out-of-date) 라우팅 테이블을 가진 클라이언트는 새로운 마스터의 리플리카로 변경되기 이전의 오래된 마스터로 쓰기를 시도할 수도 있다.

두 번째 실패 케이스는 발생하기 어려운데, 충분히 페일오버가 될만큼의 시간동안 과반수의 다른 마스터와 통신할 수 없는 마스터는 더 이상 쓰기를 받아들이지 않을 것이고, 파티션이 해소될 때에도 다른 노드들이 구성 변경에 대해서 알릴 수 있도록 짧은 시간 동안에도 여전히 쓰기는 거절될 것이기 때문이다. 또한, 이 실패 케이스는 또한 클라이언트의 라우팅 테이블이 아직 업데이트 되어 있지 않았다는 조건도 필요하다.

파티션의 소수 측을 대상으로하는 쓰기는 쓰기를 손실할 수 있는 시간대가 더 크다. 예를 들어, 레디스 클러스터는 소수의 마스터와 적어도 하나 이상의 클라이언트가 있는 파티션에서 적지 않은 수의 쓰기를 읽게 되는데, 마스터가 과반수 쪽에서 페일오버가 된다면 마스터로 전송된 모든 쓰기가 잠재적으로 손실될 수 있기 때문이다.

특히, 마스터가 페일오버되기 위해서는 적어도 NODE_TIMEOUT동안 과반수의 마스터에 의해서 접근할 수 없는 상태가 되어야 하고, 그래서 만약 파티션이 그 시간 이전에 해소되면, 쓰기의 손실은 없다. 파티션이 NODE_TIMEOUT 이상 지속될 때, NODE_TIMEOUT 시간까지 소수 측에서 실행된 모든 쓰기는 손실될 수도 있다. 그러나 소수 측의 레디스 클러스터는 NODE_TIMEOUT이 경과하자마자, 과반수 측과의 연락없이 쓰기를 거절하기 시작하므로, 최대의 시간이 존재하며, 그 이후에는 소수 쪽은 더 이상 사용할 수 없는 상태가 된다. 이런 이유로 이 시간 이후에는 쓰기는 받아들여지거나, 손실되지도 않는다.

Availability

레디스 클러스터는 소수 측의 파티션에서는 사용할 수 없다. 적어도 과반수의 마스터와 연결 불가능한 모든 마스터 노드에 리플리카가 있는 과반수의 파티션을 가정할 때, 클러스터는 NODE_TIMEOUT과 추가로 리플리카가 마스터로 승격되고 자신의 마스터를 페일오버 하기 위해 필요한 2초 정도의 시간 후에, 다시 사용 가능해지는 상태가 된다. (페일오버는 보통 1에서 2초안에 실행된다.)

이것은 레디스 클러스터가 클러스터 내의 몇 개의 노드의 실패에 살아남기 위해 디자인되었지만, 대규모 네트워크 스플릿과 같은 것에서 가용성이 필요한 어플리케이션에 대해서는 적합한 솔루션이 아니라는 것을 의미한다.

각각 하나의 리플리카를 가지는 N개의 마스터 노드로 구성된 클러스터의 예에서, 클러스터의 과반수는 노드 하나가 파티션되어 있는 한은 가용성을 유지할 것이다. 그리고 2개의 노드가 파티션된다면, 1-(1/(N*2-1))의 확률로 가용성을 유지할 것이다. (첫 번째 노드가 실패한 후에, 총 N*2-1개의 노드가 남아있고, 리플리카가 없는 마스터가 실패하게 될 확률은 1/(N*2-1)이다)

예를 들어, 각각 하나의 리플리카를 가지는 노드 5개의 클러스터에서, 2개의 마스터가 과반수에서 다시 파티션된 이후에 클러스터는 더 이상 사용할 수 없게 될 확률은 1/(5*2-1) = 11.11%이다.

리플리카 마이그레이션(replicas migration)으로 불리는 레디스 클러스터의 기능은 리플리카를 고아(orphaned) 마스터(더 이상 리플리카를 가지고 있지 않은)로 마이그레이션한다는 점에서 현실 세계의 시나리오에서 클러스터 가용성을 향상시키는데 도움을 준다. 그래서 모든 성공적인 실패 이벤트에서, 클러스터는 다음 실패에 더 잘 대처하기 위해서 리플리카 배치를 재구성한다.

Performance

레디스 클러스터에서 노드는 커맨드를 주어진 키를 담당하는 올바른 노드로 전달하는 프록시로서의 역할을 하지 않는다. 대신 클라이언트에게 주어진 키 스페이스의 특정 부분을 서빙하는 올바른 노드로 다시 보내게 한다.

결국 클라이언트는 최신의 클러스터의 상태와 키의 서브셋을 어떤 노드가 담당하는지에 대한 정보를 얻고, 그래서 정상적인 작업중에 클라이언트는 주어진 커맨드를 전송하기 위해서 직접 올바른 노드로 접근한다.

비동기 리플리케이션이 사용하므로, (만약 WAIT 커맨드를 사용해서 명시적으로 요청하지 않았을 때) 노드는 다른 노드의 쓰기에 대한 승인(acknowledgment)를 기다리지 않는다.

또한, 멀티 키(multi-key) 커맨드는 근처(near)의 키에 대해서만 제한되기 때문에, 리샤딩을 제외하고 데이터는 절대 노드 사이에서 이동되지 않는다.

일반적인 오퍼레이션들은 정확히 단일 레디스 인스턴스의 경우처럼 다루어진다. 이것은 N개의 마스터를 가지는 레디스 클러스터에서는 구조가 선형적으로 확장됨에 따라 단일 레디스 인스턴스가 N배만큼 늘어난 것과 같은 퍼포먼스를 예상할 수 있다는 것을 의미한다. 동시에 쿼리는 보통 한 번의 왕복(round-trip)으로 처리되는데, 클라이언트가 보통 노드와 영구적인 커넥션을 유지하기 때문으로, 따라서 레이턴시 수치 또한 단일 레디스 노드의 경우와 같다.

약하지만 합리적인 형태의 데이터 안정성과 가용성을 유지하면서, 매우 높은 성능과 확장성을 제공하는 것이 레디스 클러스터의 주요 목표이다.

Why merge operations are avoided

레디스 클러스터 디자인은 (항상 바람직한 것은 아닌) 레디스 데이터 모델의 경우처럼 동일한 키-값 쌍의 버전이 여러 노드에서 충돌되지 않도록 한다. 레디스의 값은 종종 매우 크다. 수백만개의 엘리먼트를 가진 리스트나 정렬된 셋에서 보이는 것이 일반적이다. 데이터 타입은 의미적으로도 매우 복잡하다. 이러한 종류의 값들을 전송하고 병합하는 것은 매우 큰 병목 현상이 될 수 있고, 또 어플리케이션 측의 로직의 적지않은 개입과, 메타 데이터를 저장하기 위한 추가적인 메모리 등이 필요할 수 있다.

여기에 엄격한 기술적 제한은 없다. CRDTs나 동기식으로 복제되는 상태 머신은 레디스와 유사한 복잡한 데이터 타입을 모델링할 수 있다. 그러나 그러한 시스템의 실제 런타임 동작은 레디스 클러스터와 비슷하지 않다. 레디스 클러스터는 논클러스터드 레디스 버전의 완전한 사용 케이스를 커버하기 위해서 설계되었다.

0개의 댓글