클러스터링 (Clustering/Scalability)

클러스터링 (Clustering/Scalability)

출처: Scale with Redis Cluster (Redis 공식 문서)

Redis는 Redis Cluster라는 배포 토폴로지로 수평 확장(horizontal scaling)을 합니다. 이 문서에서는 Redis Cluster를 운영 환경에서 어떻게 설정하고, 테스트하고, 운영하는지 배워볼게요. 동시에 Redis Cluster가 최종 사용자 관점에서 어떤 가용성(availability)과 일관성(consistency) 특성을 갖는지도 함께 살펴봐요.

운영 환경에서 Redis Cluster를 직접 돌리거나 내부 동작을 더 깊이 이해하고 싶다면 Redis Cluster specification을 참고하세요. Redis Enterprise가 스케일링을 어떻게 다루는지는 Linear Scaling with Redis Enterprise에서 확인할 수 있어요.

Redis Cluster 101

Redis Cluster는 데이터가 여러 Redis 노드에 자동으로 샤딩(sharding) 되는 방식으로 Redis 설치를 실행하는 방법이에요. Redis Cluster는 파티션 상황에서도 어느 정도 가용성을 보장하는데, 쉽게 말하면 일부 노드가 실패하거나 통신이 불가능해져도 계속 작업을 수행할 수 있는 능력을 말해요. 다만 더 큰 규모의 장애(예를 들어 마스터 대부분이 사용 불가능해지는 경우)가 발생하면 클러스터는 사용 불가능해져요.

정리하면 Redis Cluster를 사용하면 다음 두 가지가 가능해져요.

  • 데이터셋을 여러 노드에 자동으로 분산한다.
  • 일부 노드에서 장애가 발생하거나 나머지 클러스터와 통신할 수 없게 되어도 작업을 계속한다.

Redis Cluster TCP 포트

모든 Redis Cluster 노드는 두 개의 열린 TCP 연결이 필요해요. 하나는 클라이언트에 서비스를 제공하는 Redis TCP 포트(예: 6379)이고, 다른 하나는 **클러스터 버스 포트(cluster bus port)**라고 알려진 두 번째 포트예요.

기본적으로 클러스터 버스 포트는 데이터 포트에 10000을 더해서 정해져요(예: 16379). 다만 cluster-port 설정으로 이 값을 바꿀 수 있어요.

클러스터 버스는 노드 간 통신 채널로, 이진(binary) 프로토콜을 사용해요. 대역폭과 처리 시간이 적게 드는 방식이라 노드 간 정보 교환에 더 적합해요. 노드들은 클러스터 버스를 통해 장애 감지, 설정 업데이트, 페일오버 인가(failover authorization) 등을 수행해요.

클라이언트는 절대 클러스터 버스 포트로 통신하면 안 되고, Redis 명령 포트를 사용해야 해요. 다만 방화벽에서는 두 포트 모두 열어야 해요. 그렇지 않으면 Redis 클러스터 노드들이 서로 통신할 수 없어요.

Redis Cluster가 제대로 동작하려면 각 노드에 대해 다음이 필요해요.

  1. 클라이언트 통신 포트(보통 6379)는 클러스터에 접근해야 하는 모든 클라이언트에 열려 있어야 하고, 키 마이그레이션에 사용하는 다른 모든 클러스터 노드에도 열려 있어야 해요.
  2. 클러스터 버스 포트는 다른 모든 클러스터 노드에서 접근 가능해야 해요.

두 TCP 포트를 모두 열지 않으면 클러스터가 예상대로 동작하지 않아요.

Redis Cluster와 Docker

현재 Redis Cluster는 NAT된 환경, 그리고 일반적으로 IP 주소나 TCP 포트가 다시 매핑(remap)되는 환경을 지원하지 않아요.

Docker는 **포트 매핑(port mapping)**이라는 기법을 사용하는데요, Docker 컨테이너 안에서 실행되는 프로그램이 프로그램이 스스로 인식하는 포트와 다른 포트로 노출될 수 있어요. 이 방식은 같은 서버에서 여러 컨테이너가 같은 포트를 동시에 사용할 때 유용하죠.

Docker에서 Redis Cluster를 사용하려면 Docker의 **호스트 네트워킹 모드(host networking mode)**를 사용해야 해요. 자세한 내용은 Docker 문서의 --net=host 옵션을 참고하세요.

Redis Cluster 데이터 샤딩

Redis Cluster는 일관적 해싱(consistent hashing)을 사용하지 않고, 모든 키가 개념적으로 속하는 **해시 슬롯(hash slot)**이라는 다른 형태의 샤딩을 사용해요.

Redis Cluster에는 16384개의 해시 슬롯이 있고, 주어진 키의 해시 슬롯을 계산하려면 키의 CRC16을 구한 뒤 16384로 나눈 나머지를 취하면 돼요.

Redis Cluster의 모든 노드는 해시 슬롯의 일부를 담당해요. 예를 들어 3개의 노드로 이루어진 클러스터가 있다면 다음과 같이 나눌 수 있어요.

  • 노드 A: 해시 슬롯 0 ~ 5500
  • 노드 B: 해시 슬롯 5501 ~ 11000
  • 노드 C: 해시 슬롯 11001 ~ 16383

이런 구조 덕분에 클러스터 노드를 추가하고 제거하기가 쉬워져요. 예를 들어 새 노드 D를 추가하고 싶다면 노드 A, B, C에서 일부 해시 슬롯을 D로 옮기면 돼요. 마찬가지로 노드 A를 제거하고 싶다면 A가 서비스하는 해시 슬롯을 B와 C로 옮기면 되고, A가 비워지면 클러스터에서 완전히 제거할 수 있어요.

해시 슬롯을 한 노드에서 다른 노드로 옮기는 것은 어떤 작업도 중단할 필요가 없어요. 따라서 노드를 추가·제거하거나 노드가 보유한 해시 슬롯의 비율을 바꾸는 것은 다운타임이 전혀 필요 없어요.

Redis Cluster는 단일 명령 실행(또는 전체 트랜잭션, Lua 스크립트 실행)에 포함된 모든 키가 같은 해시 슬롯에 속하는 한, 여러 키에 대한 작업을 지원해요. **해시 태그(hash tags)**라는 기능을 사용하면 여러 키를 강제로 같은 해시 슬롯에 넣을 수 있어요.

해시 태그는 Redis Cluster specification에 자세히 문서화되어 있는데, 핵심만 말하면 키에 {} 중괄호 사이의 부분 문자열이 있다면 괄호 안쪽만 해싱돼요. 예를 들어 user:{123}:profileuser:{123}:account는 같은 해시 태그를 공유하므로 같은 해시 슬롯에 있다는 것이 보장돼요. 결과적으로 이 두 키를 같은 다중 키 작업에서 다룰 수 있어요.

Redis Cluster 마스터-레플리카 모델

마스터 노드 일부가 실패하거나 마스터 노드 대다수와 통신할 수 없을 때도 가용성을 유지하기 위해, Redis Cluster는 마스터-레플리카 모델을 사용해요. 여기서 모든 해시 슬롯은 1개(마스터 자신)부터 N개의 레플리카(N-1개의 추가 레플리카 노드)를 가질 수 있어요.

앞선 예시의 A, B, C 클러스터에서 노드 B가 실패하면 클러스터는 계속 동작할 수 없어요. 5501~11000 범위의 해시 슬롯을 서비스할 방법이 없어지기 때문이에요.

하지만 클러스터를 만들 때(또는 나중에) 모든 마스터에 레플리카 노드를 추가해서 A, B, C가 마스터 노드, A1, B1, C1이 레플리카 노드인 클러스터를 만들면, 노드 B가 실패해도 시스템은 계속 동작할 수 있어요.

노드 B1이 B를 복제하고 있는데 B가 실패하면, 클러스터는 B1을 새로운 마스터로 승격시켜서 정상적으로 계속 동작해요. 다만 B와 B1이 동시에 실패하면 Redis Cluster는 계속 동작할 수 없어요.

Redis Cluster 일관성 보장

Redis Cluster는 강한 일관성(strong consistency)을 보장하지 않아요. 실질적으로 말하면, 특정 조건에서 Redis Cluster가 클라이언트에게 확인(acknowledge)해 준 쓰기가 유실될 수 있어요.

Redis Cluster가 쓰기를 잃을 수 있는 첫 번째 이유는 **비동기 복제(asynchronous replication)**를 사용하기 때문이에요. 쓰기 과정에서 다음과 같은 일이 일어나요.

  1. 클라이언트가 마스터 B에 쓰기를 한다.
  2. 마스터 B가 클라이언트에게 OK로 응답한다.
  3. 마스터 B가 쓰기를 레플리카 B1, B2, B3에 전파한다.

보시다시피 B는 클라이언트에게 응답하기 전에 B1, B2, B3로부터 확인을 기다리지 않아요. 이렇게 하면 Redis에 엄청난 지연(latency) 패널티가 생기기 때문이에요. 그래서 클라이언트가 어떤 값을 쓰고 B가 그 쓰기를 확인해 줬는데, B가 레플리카에 쓰기를 보내기 전에 크래시하면, 쓰기를 받지 못한 레플리카 중 하나가 마스터로 승격되면서 그 쓰기는 영원히 유실될 수 있어요.

이것은 매초 데이터를 디스크에 플러시하도록 설정된 대부분의 데이터베이스에서 일어나는 일과 매우 비슷해요. 분산 시스템을 다루지 않는 기존 데이터베이스 경험에서 이미 생각해 볼 수 있는 시나리오죠. 마찬가지로 클라이언트에게 응답하기 전에 데이터를 디스크에 플러시하도록 강제하면 일관성을 높일 수 있지만, 보통 결과적으로 엄청나게 낮은 성능을 낳아요. Redis Cluster의 경우 이것이 동기 복제(synchronous replication)에 해당할 거예요.

기본적으로 여기에는 성능과 일관성 사이의 트레이드오프가 있어요.

Redis Cluster는 꼭 필요할 때 동기 쓰기를 WAIT 명령으로 지원해서 쓰기 유실 가능성을 크게 줄여줘요. 다만 동기 복제를 사용해도 Redis Cluster가 강한 일관성을 구현하는 것은 아니라는 점에 주의하세요. 더 복잡한 장애 시나리오에서는 쓰기를 받지 못한 레플리카가 마스터로 선출될 가능성이 항상 있어요.

Redis Cluster가 쓰기를 잃는 또 하나의 눈에 띄는 시나리오는, 클라이언트가 마스터를 포함한 소수(minority) 인스턴스와 격리되는 네트워크 파티션(network partition) 기간 동안 발생해요.

3개의 마스터와 3개의 레플리카로 구성된 A, B, C, A1, B1, C1 클러스터를 예로 들어볼게요. 여기에 Z1이라는 클라이언트도 있다고 해요.

파티션이 발생한 뒤, 파티션 한쪽에는 A, C, A1, B1, C1이 있고 다른 쪽에는 B와 Z1이 있다고 가정해요.

Z1은 여전히 B에 쓰기를 보낼 수 있고 B는 그 쓰기를 받아들여요. 만약 파티션이 아주 짧은 시간에 치유되면 클러스터는 정상적으로 계속 동작해요. 하지만 파티션이 파티션의 과반(majority) 쪽에서 B1이 마스터로 승격될 만큼 오래 지속되면, 그 동안 Z1이 B에 보낸 쓰기는 유실돼요.

참고할 점은 Z1이 B에 보낼 수 있는 쓰기량에는 **최대 창(window)**이 있다는 거예요. 파티션의 과반 쪽이 레플리카를 마스터로 선출할 만큼 충분한 시간이 지나면, 소수 쪽의 모든 마스터 노드는 쓰기 수락을 중단해요.

이 시간은 Redis Cluster의 매우 중요한 구성 지시어이며, **노드 타임아웃(node timeout)**이라고 불러요. 노드 타임아웃이 지나면 마스터 노드는 실패한 것으로 간주되고 레플리카 중 하나로 교체될 수 있어요. 마찬가지로 노드 타임아웃이 지나도록 마스터 노드가 다른 마스터 노드의 과반을 감지하지 못하면, 그 노드는 오류 상태에 들어가 쓰기 수락을 중단해요.

Redis Cluster 설정 파라미터

이제 예제 클러스터 배포를 만들 준비를 해볼게요. 그 전에 Redis Cluster가 redis.conf 파일에 도입하는 설정 파라미터들을 먼저 소개할게요.

  • cluster-enabled <yes/no>: yes면 특정 Redis 인스턴스에서 Redis Cluster 지원을 활성화해요. 그렇지 않으면 인스턴스는 평소처럼 독립(standalone) 인스턴스로 시작돼요.
  • cluster-config-file <filename>: 이름과 달리 사용자가 편집하는 설정 파일이 아니에요. Redis Cluster 노드가 클러스터 설정(기본적으로 상태)을 변경할 때마다 자동으로 저장해서 시작 시 다시 읽을 수 있게 하는 파일이에요. 이 파일에는 클러스터의 다른 노드, 그들의 상태, 영구 변수 같은 것들이 나열돼요. 보통 이 파일은 어떤 메시지를 받은 결과로 다시 쓰여지고 디스크에 플러시돼요.
  • cluster-node-timeout <milliseconds>: Redis Cluster 노드가 실패로 간주되지 않고 사용 불가능해질 수 있는 최대 시간이에요. 마스터 노드가 지정된 시간 이상 도달 불가능하면 레플리카에 의해 페일오버돼요. 이 파라미터는 Redis Cluster의 다른 중요한 것들도 제어해요. 특히 지정된 시간 동안 과반의 마스터 노드에 도달할 수 없는 모든 노드는 쿼리 수락을 중단해요.
  • cluster-slave-validity-factor <factor>: 0으로 설정하면 레플리카는 항상 자신을 유효하다고 간주해서, 마스터와 레플리카 사이의 링크가 끊긴 시간과 무관하게 항상 마스터를 페일오버하려고 시도해요. 값이 양수면 최대 연결 끊김 시간이 이 옵션으로 제공된 factor를 곱한 노드 타임아웃 값으로 계산되고, 노드가 레플리카라면 마스터 링크가 지정된 시간 이상 끊긴 경우 페일오버를 시작하지 않아요. 예를 들어 노드 타임아웃이 5초이고 validity factor가 10이라면, 마스터에서 50초 이상 연결이 끊긴 레플리카는 마스터를 페일오버하려고 하지 않아요. 0이 아닌 값은 마스터 장애 후 페일오버할 수 있는 레플리카가 없다면 Redis Cluster가 사용 불가능해질 수 있다는 점에 주의하세요. 이 경우 원래 마스터가 클러스터에 다시 합류해야만 클러스터가 다시 사용 가능해져요.
  • cluster-migration-barrier <count>: 마스터가 레플리카에 더는 덮이지 않는 경우(다른 레플리카가 마스터로 마이그레이션하기 위해) 다른 레플리카가 마이그레이션하기 위해 마스터가 계속 연결을 유지해야 하는 최소 레플리카 수예요. 이 튜토리얼의 레플리카 마이그레이션 섹션에서 자세한 내용을 확인할 수 있어요.
  • cluster-require-full-coverage <yes/no>: 기본값인 yes로 설정하면, 키 공간의 일정 비율이 어떤 노드로도 덮이지 않으면 클러스터는 쓰기 수락을 중단해요. no로 설정하면 키의 일부에 대한 요청만 처리할 수 있어도 클러스터는 여전히 쿼리를 서비스해요.
  • cluster-allow-reads-when-down <yes/no>: 기본값인 no로 설정하면, Redis Cluster의 노드는 클러스터가 실패로 표시된 경우(노드가 마스터의 쿼럼에 도달할 수 없거나 전체 커버리지가 충족되지 않을 때) 모든 트래픽 서비스를 중단해요. 이것은 클러스터의 변경 사항을 인지하지 못하는 노드에서 잠재적으로 일관성 없는 데이터를 읽는 것을 방지해요. 이 옵션을 yes로 설정하면 실패 상태 동안 노드에서 읽기를 허용하는데, 읽기 가용성을 우선시하면서도 일관성 없는 쓰기는 막으려는 애플리케이션에 유용해요. 또한 Redis Cluster를 하나나 두 개의 샤드로만 사용할 때 유용한데, 마스터가 실패해도 자동 페일오버가 불가능한 경우 노드가 쓰기 서비스를 계속할 수 있게 해줘요.

Redis Cluster 생성 및 사용

Redis Cluster를 만들고 사용하려면 다음 단계를 따라야 해요.

  1. Redis Cluster 생성
  2. 클러스터와 상호작용
  3. redis-rb-cluster로 예제 앱 작성
  4. 클러스터 리샤딩(reshard)
  5. 더 흥미로운 예제 애플리케이션
  6. 페일오버 테스트
  7. 수동 페일오버
  8. 새 노드 추가
  9. 노드 제거
  10. 레플리카 마이그레이션
  11. Redis Cluster 노드 업그레이드
  12. Redis Cluster로 마이그레이션

하지만 그 전에 클러스터 생성 요구 사항을 먼저 숙지해 두는 게 좋아요.

Redis Cluster 생성 요구 사항

클러스터를 만들 때 가장 먼저 필요한 것은 클러스터 모드로 실행 중인 빈 Redis 인스턴스 몇 개예요.

최소한 redis.conf 파일에 다음 지시어를 설정해야 해요.

port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

클러스터 모드를 활성화하려면 cluster-enabled 지시어를 yes로 설정해요. 모든 인스턴스는 이 노드의 설정이 저장될 파일 경로도 담고 있는데, 기본값은 nodes.conf예요. 이 파일은 사람이 절대 건드리지 않아요. Redis Cluster 인스턴스가 시작할 때 생성하고 필요할 때마다 업데이트할 뿐이에요.

정상적으로 동작하는 최소 클러스터는 최소 3개의 마스터 노드를 포함해야 한다는 점에 주의하세요. 배포에는 3개의 마스터와 3개의 레플리카로 구성된 6개 노드 클러스터를 강력히 권장해요.

로컬에서 테스트하려면 인스턴스를 실행할 포트 번호를 이름으로 한 디렉터리들을 만들면 돼요.

예를 들어:

mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005

7000부터 7005까지 각 디렉터리 안에 redis.conf 파일을 만들어요. 설정 파일의 템플릿으로 위의 작은 예제를 사용하되, 포트 번호 7000을 디렉터리 이름에 맞는 포트 번호로 바꿔야 해요.

각 인스턴스는 다음과 같이 별도의 터미널 탭에서 시작할 수 있어요.

cd 7000
redis-server ./redis.conf

로그에서 각 노드가 스스로 새로운 ID를 할당하는 것을 볼 수 있어요.

[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1

이 ID는 클러스터 맥락에서 인스턴스가 고유한 이름을 갖도록 이 특정 인스턴스가 영원히 사용해요. 모든 노드는 이 ID로 다른 모든 노드를 기억하며, IP나 포트가 아니라 이 ID로 기억해요. IP 주소와 포트는 바뀔 수 있지만, 고유한 노드 식별자는 노드가 살아있는 한 절대 바뀌지 않아요. 이 식별자를 **노드 ID(Node ID)**라고 불러요.

Redis Cluster 생성

이제 여러 인스턴스가 실행 중이니, 노드에 의미 있는 설정을 써서 클러스터를 만들어야 해요.

개별 인스턴스를 수동으로 구성하고 실행할 수도 있고, create-cluster 스크립트를 사용할 수도 있어요. 먼저 수동으로 하는 방법을 살펴볼게요.

클러스터를 만들려면 다음을 실행해요.

redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1

여기서 사용한 명령은 create인데, 새 클러스터를 만들기 때문이에요. --cluster-replicas 1 옵션은 생성되는 모든 마스터마다 레플리카를 하나씩 원한다는 뜻이에요.

나머지 인자는 새 클러스터를 만드는 데 사용하고 싶은 인스턴스 주소 목록이에요.

redis-cli가 구성을 제안할 텐데, yes를 입력해서 제안된 구성을 수락해요. 그러면 클러스터가 구성되고 조인되며, 인스턴스들이 서로 대화하도록 부트스트랩돼요. 마지막으로 모든 것이 잘 진행되었다면 다음과 같은 메시지를 볼 수 있어요.

[OK] All 16384 slots covered

이것은 16384개의 사용 가능한 슬롯 각각을 서비스하는 마스터 인스턴스가 최소 하나 이상 있다는 뜻이에요.

앞서 설명한 것처럼 개별 인스턴스를 수동으로 구성하고 실행해 Redis Cluster를 만들고 싶지 않다면, 훨씬 더 간단한 시스템이 있어요(다만 운영 세부 사항은 그만큼 배우지 못해요).

Redis 배포판에서 utils/create-cluster 디렉터리를 찾아보세요. 안에 (디렉터리와 같은 이름의) create-cluster라는 스크립트가 있는데, 간단한 bash 스크립트예요. 3개의 마스터와 3개의 레플리카로 구성된 6개 노드 클러스터를 시작하려면 다음 명령만 입력하면 돼요.

create-cluster start
create-cluster create

2번째 단계에서 redis-cli가 클러스터 레이아웃을 수락하라고 하면 yes라고 응답해요.

이제 클러스터와 상호작용할 수 있어요. 기본적으로 첫 번째 노드는 30001 포트에서 시작돼요. 작업이 끝나면 다음으로 클러스터를 중지해요.

create-cluster stop

스크립트 실행 방법에 대한 자세한 내용은 이 디렉터리 안의 README를 읽어보세요.

클러스터와 상호작용

Redis Cluster에 연결하려면 클러스터를 인지하는(cluster-aware) Redis 클라이언트가 필요해요. 클러스터 지원을 확인하려면 사용하는 클라이언트의 문서를 참고하세요.

redis-cli 명령줄 유틸리티로 Redis Cluster를 테스트할 수도 있어요.

$ redis-cli -c -p 7000
redis 127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
redis 127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
redis 127.0.0.1:7000> get foo
-> Redirected to slot [12182] located at 127.0.0.1:7002
"bar"
redis 127.0.0.1:7002> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"

참고: 스크립트로 클러스터를 만들었다면 노드들이 기본적으로 30001부터 시작하는 다른 포트에서 수신 대기할 수 있어요.

redis-cli의 클러스터 지원은 매우 기본적이어서, Redis Cluster 노드가 클라이언트를 올바른 노드로 리다이렉트할 수 있다는 사실을 항상 이용해요. 제대로 된 클라이언트라면 더 잘할 수 있는데, 해시 슬롯과 노드 주소 사이의 맵을 캐시해서 올바른 연결을 올바른 노드에 직접 사용해요. 이 맵은 페일오버 후나 관리자가 노드를 추가·제거해 클러스터 레이아웃을 바꾼 경우처럼 클러스터 설정이 변경될 때만 새로 고쳐져요.

redis-rb-cluster로 예제 앱 작성

Redis Cluster를 운영하는 방법(페일오버나 리샤딩 같은 것)을 보여주기 전에, 예제 애플리케이션을 만들거나 최소한 간단한 Redis Cluster 클라이언트 상호작용의 의미를 이해할 필요가 있어요.

이렇게 하면 예제를 실행하면서 동시에 노드를 실패시키거나 리샤딩을 시작해서 실제 조건에서 Redis Cluster가 어떻게 행동하는지 볼 수 있어요. 아무도 클러스터에 쓰지 않는 동안 어떤 일이 일어나는지 보는 것은 그다지 도움이 되지 않아요.

이 섹션에서는 redis-rb-cluster의 기본 사용법을 두 예제로 설명해요. 첫 번째는 다음과 같으며, redis-rb-cluster 배포판 안의 example.rb 파일이에요.

   1  require './cluster'
   2
   3  if ARGV.length != 2
   4      startup_nodes = [
   5          {:host => "127.0.0.1", :port => 7000},
   6          {:host => "127.0.0.1", :port => 7001}
   7      ]
   8  else
   9      startup_nodes = [
  10          {:host => ARGV[0], :port => ARGV[1].to_i}
  11      ]
  12  end
  13
  14  rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1)
  15
  16  last = false
  17
  18  while not last
  19      begin
  20          last = rc.get("__last__")
  21          last = 0 if !last
  22      rescue => e
  23          puts "error #{e.to_s}"
  24          sleep 1
  25      end
  26  end
  27
  28  ((last.to_i+1)..1000000000).each{|x|
  29      begin
  30          rc.set("foo#{x}",x)
  31          puts rc.get("foo#{x}")
  32          rc.set("__last__",x)
  33      rescue => e
  34          puts "error #{e.to_s}"
  35      end
  36      sleep 0.1
  37  }

애플리케이션은 아주 단순한 일을 해요. foo<number> 형태의 키를 number 값으로 차례로 설정해요. 그래서 프로그램을 실행하면 결과는 다음과 같은 명령 스트림이 돼요.

SET foo0 0
SET foo1 1
SET foo2 2

이런 식으로 계속돼요.

프로그램은 평소보다 복잡해 보이는데, 예외로 종료하는 대신 화면에 오류를 보여주도록 설계됐기 때문이에요. 그래서 클러스터로 수행하는 모든 작업이 begin rescue 블록으로 감싸져 있어요.

14번째 줄이 프로그램에서 첫 번째로 흥미로운 줄이에요. Redis Cluster 객체를 만드는데, 인자로 시작 노드(startup nodes) 목록, 이 객체가 다른 노드들에 대해 허용할 최대 연결 수, 마지막으로 주어진 작업이 실패로 간주되는 타임아웃을 사용해요.

시작 노드는 클러스터의 모든 노드일 필요가 없어요. 중요한 것은 최소한 하나의 노드에 도달 가능해야 한다는 거예요. 또 redis-rb-cluster는 첫 번째 노드에 연결할 수 있게 되자마자 시작 노드 목록을 업데이트한다는 점도 주목하세요. 다른 제대로 된 클라이언트에서도 이런 동작을 기대할 수 있어요.

이제 rc 변수에 저장된 Redis Cluster 객체 인스턴스가 있으니, 일반 Redis 객체 인스턴스처럼 사용할 준비가 됐어요.

18~26번째 줄에서 정확히 그런 일이 일어나요. 예제를 다시 시작할 때 foo0부터 다시 시작하고 싶지 않으니, 카운터를 Redis 자체에 저장해요. 위 코드는 이 카운터를 읽거나, 카운터가 존재하지 않으면 0을 할당하도록 설계돼 있어요.

다만 while 루프라는 점에 주목하세요. 클러스터가 다운되어 오류를 반환해도 계속 다시 시도하고 싶기 때문이에요. 일반 애플리케이션은 그렇게 주의할 필요가 없어요.

28~37번째 줄은 키가 설정되거나 오류가 표시되는 메인 루프를 시작해요.

루프 끝의 sleep 호출에 주목하세요. 테스트에서는 클러스터에 최대한 빨리 쓰고 싶다면 sleep을 제거해도 돼요. 물론 이건 실제 병렬 처리가 없는 busy loop라서 최상의 조건에서 보통 초당 10k ops 정도를 얻는 다는 점은 상대적인 얘기예요.

보통 쓰기는 예제 애플리케이션을 사람이 따라가기 쉽도록 느리게 만들어져 있어요.

애플리케이션을 시작하면 다음 출력이 만들어져요.

ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (I stopped the program here)

이 프로그램은 그다지 흥미롭지 않아요. 잠시 후 더 나은 프로그램을 사용할 테니, 지금은 프로그램이 실행되는 동안 리샤딩에서 어떤 일이 일어나는지 볼 수 있다는 점만 확인하면 돼요.

클러스터 리샤딩

이제 클러스터 리샤딩을 시도해 볼 준비가 됐어요. 이렇게 하려면 프로그램 실행에 영향이 있는지 볼 수 있도록 example.rb 프로그램을 계속 실행해 두세요. 또한 리샤딩 중에 더 진지한 쓰기 부하를 원한다면 sleep 호출을 주석 처리해도 돼요.

리샤딩은 기본적으로 해시 슬롯을 한 노드 집합에서 다른 노드 집합으로 옮기는 것이에요. 클러스터 생성처럼 redis-cli 유틸리티로 수행돼요.

리샤딩을 시작하려면 다음을 입력해요.

redis-cli --cluster reshard 127.0.0.1:7000

단일 노드만 지정하면 되는데, redis-cli가 다른 노드를 자동으로 찾아요.

현재 redis-cli는 관리자 지원으로만 리샤딩할 수 있어요. 이 노드에서 저 노드로 슬롯의 5%를 옮기라고 말할 수는 없어요(하지만 이건 구현하기 아주 간단해요). 그래서 질문으로 시작해요. 첫 번째 질문은 얼마나 리샤딩할 것인지예요.

How many slots do you want to move (from 1 to 16384)?

1000개의 해시 슬롯을 리샤딩해 볼 수 있는데, sleep 호출 없이 예제가 여전히 실행 중이라면 이미 사소하지 않은 양의 키가 들어 있을 거예요.

그런 다음 redis-cli가 리샤딩의 대상, 즉 해시 슬롯을 받을 노드가 무엇인지 알려줘야 해요. 첫 번째 마스터 노드인 127.0.0.1:7000을 사용해볼게요. 하지만 인스턴스의 노드 ID를 지정해야 해요. 이것은 이미 redis-cli가 목록으로 출력했는데, 필요하다면 다음 명령으로 언제든 노드의 ID를 찾을 수 있어요.

$ redis-cli -p 7000 cluster nodes | grep myself
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460

좋아요, 대상 노드는 97a3a64667477371c4479320d683e4c8db5858b1이에요.

이제 그 키들을 어떤 노드에서 가져올지 물어봐요. 다른 모든 마스터 노드에서 해시 슬롯을 조금씩 가져오려면 all을 입력할게요.

마지막 확인 후에는 redis-cli가 한 노드에서 다른 노드로 옮길 모든 슬롯에 대해 메시지를 보여주고, 한쪽에서 다른 쪽으로 실제로 옮겨진 키마다 점(dot)이 출력돼요.

리샤딩이 진행되는 동안 예제 프로그램이 영향을 받지 않고 실행되는 것을 볼 수 있어야 해요. 리샤딩 중에 원한다면 프로그램을 여러 번 중지하고 다시 시작해도 돼요.

리샤딩이 끝나면 다음 명령으로 클러스터의 상태를 테스트할 수 있어요.

redis-cli --cluster check 127.0.0.1:7000

평소처럼 모든 슬롯이 커버되겠지만, 이번에는 127.0.0.1:7000의 마스터가 더 많은 해시 슬롯(대략 6461개)을 갖게 돼요.

리샤딩은 대화형으로 파라미터를 수동으로 입력할 필요 없이 자동으로 수행될 수도 있어요. 다음과 같은 명령줄로 가능해요.

redis-cli --cluster reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes

이렇게 하면 자주 리샤딩할 경우 자동화를 구축할 수 있어요. 다만 현재 클러스터 노드 간 키 분포를 확인하고 필요에 따라 슬롯을 지능적으로 이동하는 클러스터 자동 리밸런싱을 redis-cli가 수행할 방법은 없어요. 이 기능은 나중에 추가될 예정이에요.

--cluster-yes 옵션은 클러스터 관리자가 명령의 프롬프트에 자동으로 "yes"라고 답하도록 해서 비대화형(non-interactive) 모드로 실행할 수 있게 해줘요. 이 옵션은 REDISCLI_CLUSTER_YES 환경 변수를 설정해도 활성화할 수 있다는 점에 주목하세요.

더 흥미로운 예제 애플리케이션

앞서 작성한 예제 애플리케이션은 그다지 좋지 않아요. 클러스터에 단순한 방식으로 쓰기를 하면서 쓰여진 것이 올바른지조차 확인하지 않아요.

우리 관점에서 보면 쓰기를 받는 클러스터가 모든 작업에 항상 키 foo를 42로만 써도 우리는 전혀 알아차리지 못할 거예요.

그래서 redis-rb-cluster 저장소에는 consistency-test.rb라는 더 흥미로운 애플리케이션이 있어요. 기본적으로 1000개의 카운터 집합을 사용하고, 카운터를 증가시키기 위해 INCR 명령을 보내요.

하지만 단순히 쓰기만 하는 대신 이 애플리케이션은 두 가지를 추가로 해요.

  • INCR로 카운터가 업데이트될 때 애플리케이션이 그 쓰기를 기억한다.
  • 모든 쓰기 전에 무작위 카운터를 읽고, 그 값이 메모리에 있는 값과 비교해서 우리가 기대한 값인지 확인한다.

이것이 의미하는 바는 이 애플리케이션이 간단한 일관성 검사기라는 거예요. 클러스터가 어떤 쓰기를 잃었는지, 또는 확인을 받지 못한 쓰기를 받아들였는지 알려줄 수 있어요. 전자의 경우 카운터 값이 우리가 기억하는 것보다 작은 값을 볼 수 있고, 후자의 경우 값이 더 클 거예요.

일관성 테스트 애플리케이션을 실행하면 매초 출력 한 줄이 만들어져요.

$ ruby consistency-test.rb
925 R (0 err) | 925 W (0 err) |
5030 R (0 err) | 5030 W (0 err) |
9261 R (0 err) | 9261 W (0 err) |
13517 R (0 err) | 13517 W (0 err) |
17780 R (0 err) | 17780 W (0 err) |
22025 R (0 err) | 22025 W (0 err) |
25818 R (0 err) | 25818 W (0 err) |

출력 줄은 수행된 읽기(Reads)와 쓰기(Writes)의 수, 그리고 오류 수(시스템이 사용 불가능해서 오류 때문에 수락되지 않은 쿼리)를 보여줘요.

일관성이 깨지는 것을 발견하면 출력에 새 줄이 추가돼요. 예를 들어 프로그램이 실행되는 동안 카운터를 수동으로 리셋하면 이런 일이 일어나요.

$ redis-cli -h 127.0.0.1 -p 7000 set key_217 0
OK

(다른 탭에서 볼 수 있는 것...)

94774 R (0 err) | 94774 W (0 err) |
98821 R (0 err) | 98821 W (0 err) |
102886 R (0 err) | 102886 W (0 err) | 114 lost |
107046 R (0 err) | 107046 W (0 err) | 114 lost |

카운터를 0으로 설정했을 때 실제 값은 114였으므로, 프로그램은 114개의 유실된 쓰기(클러스터가 기억하지 못한 INCR 명령)를 보고해요.

이 프로그램은 테스트 케이스로 훨씬 더 흥미로워서, Redis Cluster 페일오버를 테스트하는 데 사용할 거예요.

페일오버 테스트

페일오버를 트리거하는 가장 간단한 방법(분산 시스템에서 발생할 수 있는 의미적으로도 가장 단순한 장애)은 단일 프로세스, 즉 단일 마스터를 크래시시키는 것이에요.

참고: 이 테스트 동안 일관성 테스트 애플리케이션이 실행 중인 탭을 열어 두어야 해요.

다음 명령으로 마스터를 식별하고 크래시시킬 수 있어요.

$ redis-cli -p 7000 cluster nodes | grep master
3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385482984082 0 connected 5960-10921
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 master - 0 1385482983582 0 connected 11423-16383
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422

좋아요, 7000, 7001, 7002가 마스터예요. DEBUG SEGFAULT 명령으로 노드 7002를 크래시시켜볼게요.

$ redis-cli -p 7002 debug segfault
Error: Server closed the connection

이제 일관성 테스트의 출력을 보고 무엇을 보고했는지 살펴봐요.

18849 R (0 err) | 18849 W (0 err) |
23151 R (0 err) | 23151 W (0 err) |
27302 R (0 err) | 27302 W (0 err) |

... many error warnings here ...

29659 R (578 err) | 29660 W (577 err) |
33749 R (578 err) | 33750 W (577 err) |
37918 R (578 err) | 37919 W (577 err) |
42077 R (578 err) | 42078 W (577 err) |

보시다시피 페일오버 동안 시스템은 578개의 읽기와 577개의 쓰기를 수락하지 못했지만, 데이터베이스에는 일관성이 깨진 것이 만들어지지 않았어요. 이건 예상치 못하게 들릴 수 있어요. 튜토리얼의 첫 부분에서 Redis Cluster가 비동기 복제 때문에 페일오버 중에 쓰기를 잃을 수 있다고 했기 때문이에요. 우리가 말하지 않은 것은 이게 매우 일어나기 어렵다는 거예요. Redis는 클라이언트에게 응답을 보내고, 레플리카에 복제할 명령도 거의 같은 시각에 보내기 때문에 데이터를 잃을 창이 매우 작아요. 다만 트리거하기 어렵다고 해서 불가능하다는 뜻은 아니므로, 이것이 Redis Cluster가 제공하는 일관성 보장을 바꾸지는 않아요.

이제 페일오버 후 클러스터 설정을 확인할 수 있어요. (그 사이에 크래시된 인스턴스를 다시 시작해서 레플리카로 클러스터에 다시 합류시켰다는 점에 주목하세요.)

$ redis-cli -p 7000 cluster nodes
3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385503418521 0 connected
a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385503419023 0 connected
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422
3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385503419023 3 connected 11423-16383
3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385503417005 0 connected 5960-10921
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385503418016 3 connected

이제 마스터는 7000, 7001, 7005 포트에서 실행 중이에요. 이전에 마스터였던 7002 포트의 Redis 인스턴스는 이제 7005의 레플리카예요.

CLUSTER NODES 명령의 출력은 겁나 보일 수도 있지만, 사실 아주 간단하며 다음 토큰으로 구성돼요.

  • 노드 ID
  • ip:port
  • flags: master, replica, myself, fail, ...
  • 레플리카라면 마스터의 노드 ID
  • 아직 응답을 기다리는 마지막 보류 중 PING의 시간
  • 마지막으로 받은 PONG의 시간
  • 이 노드의 설정 에포크(configuration epoch) (Cluster specification 참고)
  • 이 노드로의 링크 상태
  • 서비스하는 슬롯

수동 페일오버

때로는 마스터에 실제 문제를 일으키지 않고 페일오버를 강제하는 것이 유용해요. 예를 들어 마스터 노드 중 하나의 Redis 프로세스를 업그레이드할 때는 가용성에 미치는 영향을 최소화하면서 그 노드를 레플리카로 바꾸기 위해 페일오버하는 것이 좋아요.

Redis Cluster는 페일오버하려는 마스터의 레플리카 중 하나에서 실행해야 하는 CLUSTER FAILOVER 명령으로 수동 페일오버를 지원해요.

수동 페일오버는 특별하며 실제 마스터 장애로 인한 페일오버보다 더 안전해요. 시스템이 새 마스터가 이전 마스터의 복제 스트림을 모두 처리했다고 확신할 때만 클라이언트를 원래 마스터에서 새 마스터로 전환함으로써, 과정에서 데이터 유실을 피하는 방식으로 발생해요.

수동 페일오버를 수행할 때 레플리카 로그에서 보게 되는 것이 바로 이거예요.

# Manual failover user request accepted.
# Received replication offset for paused master manual failover: 347540
# All master replication stream processed, manual failover can start.
# Start of election delayed for 0 milliseconds (rank #0, offset 347540).
# Starting a failover election for epoch 7545.
# Failover election won: I'm the new master.

기본적으로 페일오버하는 마스터에 연결된 클라이언트는 중지돼요. 동시에 마스터는 자신의 복제 오프셋(replication offset)을 레플리카에 보내고, 레플리카는 자기 쪽에서 그 오프셋에 도달할 때까지 기다려요. 복제 오프셋에 도달하면 페일오버가 시작되고, 이전 마스터는 설정 전환에 대해 통보받아요. 클라이언트가 이전 마스터에서 차단 해제되면 새 마스터로 리다이렉트돼요.

참고: 레플리카를 마스터로 승격하려면 먼저 클러스터의 마스터 과반이 그 레플리카를 레플리카로 알고 있어야 해요. 그렇지 않으면 페일오버 선거에서 이길 수 없어요. 레플리카가 클러스터에 방금 추가된 경우(새 노드를 레플리카로 추가 참고), CLUSTER FAILOVER 명령을 보내기 전에 클러스터 마스터들이 새 레플리카를 인지하도록 잠시 기다려야 할 수도 있어요.

새 노드 추가

새 노드를 추가하는 것은 기본적으로 빈 노드를 추가한 다음, 새 마스터인 경우 그 안으로 일부 데이터를 옮기거나, 레플리카인 경우 알려진 노드의 레플리카로 설정하라고 지시하는 과정이에요.

둘 다 보여줄 텐데, 새 마스터 인스턴스 추가부터 시작할게요.

두 경우 모두 첫 단계는 빈 노드를 추가하는 거예요.

이것은 포트 7006에 새 노드를 시작하는 것만큼 간단해요(기존 6개 노드에 7000부터 7005까지 이미 사용했어요). 다른 노드에 사용한 것과 같은 설정을 포트 번호만 빼고 사용해요. 이전 노드에 사용한 설정에 맞추려면 다음을 하면 돼요.

  1. 터미널 애플리케이션에 새 탭을 만든다.
  2. cluster-test 디렉터리로 들어간다.
  3. 7006이라는 이름의 디렉터리를 만든다.
  4. 그 안에 다른 노드용과 비슷하되 포트 번호로 7006을 사용하는 redis.conf 파일을 만든다.
  5. 마지막으로 ../redis-server ./redis.conf로 서버를 시작한다.

이 시점에 서버가 실행 중이어야 해요.

이제 평소처럼 redis-cli를 사용해서 노드를 기존 클러스터에 추가할 수 있어요.

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

보시다시피 첫 번째 인자로 새 노드의 주소를, 두 번째 인자로 클러스터의 임의의 기존 노드 주소를 지정해서 add-node 명령을 사용했어요.

실질적으로 redis-cli는 여기서 우리를 거의 돕지 않았어요. 노드에 CLUSTER MEET 메시지를 보냈을 뿐인데, 이것은 수동으로도 가능해요. 다만 redis-cli는 작업 전에 클러스터 상태도 확인하므로, 내부 동작을 알고 있어도 클러스터 작업은 항상 redis-cli로 수행하는 것이 좋아요.

이제 새 노드에 연결해서 정말 클러스터에 합류했는지 확인할 수 있어요.

redis 127.0.0.1:7006> cluster nodes
3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385543178575 0 connected 5960-10921
3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385543179583 0 connected
f093c80dde814da99c5cf72a7dd01590792b783b :0 myself,master - 0 0 0 connected
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543178072 3 connected
a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385543178575 0 connected
97a3a64667477371c4479320d683e4c8db5858b1 127.0.0.1:7000 master - 0 1385543179080 0 connected 0-5959 10922-11422
3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385543177568 3 connected 11423-16383

이 노드는 이미 클러스터에 연결되어 있으므로 클라이언트 쿼리를 올바르게 리다이렉트할 수 있고 일반적으로 클러스터의 일부라는 점에 주목하세요. 다만 다른 마스터와 비교해 두 가지 특징이 있어요.

  • 할당된 해시 슬롯이 없어서 데이터를 보유하지 않는다.
  • 슬롯이 없는 마스터이기 때문에 레플리카가 마스터가 되고 싶을 때 선거 과정에 참여하지 않는다.

이제 redis-cli의 리샤딩 기능으로 이 노드에 해시 슬롯을 할당할 수 있어요. 앞선 섹션에서 이미 했으므로 이를 보여주는 것은 기본적으로 쓸모없어요. 차이가 전혀 없고, 대상이 빈 노드인 리샤딩일 뿐이에요.

새 레플리카 노드 추가

새 레플리카를 추가하는 방법은 두 가지가 있어요. 명백한 방법은 다시 redis-cli를 사용하되 --cluster-slave 옵션을 붙이는 거예요.

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave

여기서 명령줄은 새 마스터를 추가할 때 사용한 것과 정확히 같으므로, 어떤 마스터에 레플리카를 추가할지 지정하지 않는다는 점에 주목하세요. 이 경우 redis-cli는 레플리카가 가장 적은 마스터 중 임의의 마스터의 레플리카로 새 노드를 추가해요.

다만 다음 명령줄로 새 레플리카로 정확히 어떤 마스터를 대상으로 할지 지정할 수 있어요.

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

이렇게 하면 새 레플리카를 특정 마스터에 할당해요.

특정 마스터에 레플리카를 추가하는 더 수동적인 방법은 새 노드를 빈 마스터로 추가한 다음 CLUSTER REPLICATE 명령으로 레플리카로 바꾸는 것이에요. 이 방법은 노드가 레플리카로 추가됐지만 다른 마스터의 레플리카로 옮기고 싶을 때도 동작해요.

예를 들어 해시 슬롯 범위 11423~16383을 서비스하고 있고 노드 ID가 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e인 127.0.0.1:7005 노드의 레플리카를 추가하려면, (이미 빈 마스터로 추가된) 새 노드에 연결해서 다음 명령을 보내면 돼요.

redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

그게 전부예요. 이제 이 해시 슬롯 집합에 새 레플리카가 생겼고, 클러스터의 다른 모든 노드도 (설정을 업데이트하는 데 필요한 몇 초 후에는) 이미 알고 있어요. 다음 명령으로 확인할 수 있어요.

$ redis-cli -p 7000 cluster nodes | grep slave | grep 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
f093c80dde814da99c5cf72a7dd01590792b783b 127.0.0.1:7006 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617702 3 connected
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617198 3 connected

3c3a0c... 노드는 이제 7002 포트(기존)와 7006 포트(새 것)에서 실행 중인 레플리카 두 개를 갖게 됐어요.

노드 제거

레플리카 노드를 제거하려면 redis-clidel-node 명령을 사용해요.

redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

첫 번째 인자는 클러스터의 임의의 노드이고, 두 번째 인자는 제거하려는 노드의 ID예요.

마스터 노드도 같은 방식으로 제거할 수 있어요. 다만 마스터 노드를 제거하려면 그 마스터가 비어 있어야 해요. 마스터가 비어 있지 않다면 그 데이터를 다른 모든 마스터 노드로 리샤딩해서 비워야 해요.

마스터 노드를 제거하는 또 다른 방법은 그 마스터를 레플리카 중 하나로 수동 페일오버하고, 노드가 새 마스터의 레플리카가 된 뒤 제거하는 거예요. 물론 이것은 클러스터의 실제 마스터 수를 줄이려고 할 때는 도움이 되지 않아요. 그 경우에는 리샤딩이 필요해요.

실패한 노드를 제거하려는 특별한 시나리오가 있어요. del-node 명령을 사용하면 안 되는데, 모든 노드에 연결하려고 시도하다가 "connection refused" 오류를 만나기 때문이에요. 대신 call 명령을 사용할 수 있어요.

redis-cli --cluster call 127.0.0.1:7000 cluster forget `<node-id>`

이 명령은 모든 노드에서 CLUSTER FORGET 명령을 실행해요.

레플리카 마이그레이션

Redis Cluster에서는 언제든 다음 명령만으로 레플리카를 다른 마스터를 복제하도록 재구성할 수 있어요.

CLUSTER REPLICATE <master-node-id>

다만 시스템 관리자의 도움 없이 레플리카가 자동으로 한 마스터에서 다른 마스터로 이동하길 원하는 특별한 시나리오가 있어요. 레플리카의 자동 재구성을 **레플리카 마이그레이션(replicas migration)**이라고 하며, Redis Cluster의 신뢰성을 향상시킬 수 있어요.

참고: 레플리카 마이그레이션의 세부 사항은 Redis Cluster Specification에서 읽을 수 있는데, 여기서는 일반적인 아이디어와 혜택을 얻기 위해 무엇을 해야 하는지에 대한 정보만 제공할게요.

특정 조건에서 클러스터 레플리카가 한 마스터에서 다른 마스터로 이동하게 두고 싶은 이유는, 보통 Redis Cluster가 특정 마스터에 붙어 있는 레플리카 수만큼 장애에 저항적이기 때문이에요.

예를 들어 모든 마스터가 단일 레플리카를 가진 클러스터는 마스터와 그 레플리카가 동시에 실패하면 계속 운영할 수 없어요. 마스터가 서비스하던 해시 슬롯의 사본을 가진 다른 인스턴스가 없기 때문이에요. 다만 네트워크 분할은 여러 노드를 동시에 격리할 가능성이 있지만, 단일 노드에 국한된 하드웨어나 소프트웨어 장애 같은 많은 다른 종류의 장애는 동시에 일어날 가능성이 낮은 매우 주목할 만한 장애 부류예요. 그래서 모든 마스터가 레플리카를 가진 클러스터에서 레플리카가 새벽 4시에 죽고 마스터가 새벽 6시에 죽는 일도 가능해요. 이것 역시 더는 운영할 수 없는 클러스터를 낳아요.

시스템 신뢰성을 높이기 위해 모든 마스터에 추가 레플리카를 더하는 옵션이 있지만, 이건 비용이 많이 들어요. 레플리카 마이그레이션은 소수의 마스터에만 레플리카를 더 추가할 수 있게 해줘요. 그래서 10개의 마스터에 각각 1개의 레플리카, 총 20개의 인스턴스가 있다고 해요. 하지만 예를 들어 일부 마스터의 레플리카로 3개의 인스턴스를 더 추가하면, 특정 마스터는 레플리카를 두 개 이상 갖게 돼요.

레플리카 마이그레이션으로 일어나는 일은, 마스터가 레플리카 없이 남겨지면 여러 레플리카를 가진 마스터의 레플리카 하나가 고아가 된(orphaned) 마스터로 마이그레이션된다는 거예요. 그래서 위 예처럼 레플리카가 새벽 4시에 다운되면 다른 레플리카가 그 자리를 차지하고, 마스터가 새벽 5시에 실패해도 여전히 선출될 레플리카가 있어서 클러스터가 계속 운영될 수 있어요.

그럼 레플리카 마이그레이션에 대해 짧게 알아야 할 것은 뭘까요?

  • 클러스터는 주어진 순간에 레플리카가 가장 많은 마스터에서 레플리카를 마이그레이션하려고 시도해요.
  • 레플리카 마이그레이션의 혜택을 보려면 클러스터의 단일 마스터에 몇 개의 레플리카를 더 추가하기만 하면 돼요. 어떤 마스터인지는 상관없어요.
  • 레플리카 마이그레이션 기능을 제어하는 cluster-migration-barrier라는 설정 파라미터가 있어요. Redis Cluster와 함께 제공되는 예제 redis.conf 파일에서 더 자세히 읽을 수 있어요.

Redis Cluster 노드 업그레이드

레플리카 노드를 업그레이드하는 것은 쉽다. 노드를 중지하고 업데이트된 Redis 버전으로 다시 시작하기만 하면 되기 때문이에요. 레플리카 노드를 사용해 읽기를 확장하는 클라이언트가 있다면, 특정 레플리카를 사용할 수 없을 때 다른 레플리카에 다시 연결할 수 있어야 해요.

마스터 업그레이드는 조금 더 복잡하며, 권장 절차는 다음과 같아요.

  1. CLUSTER FAILOVER로 마스터를 레플리카 중 하나로 수동 페일오버를 트리거한다. (이 주제의 수동 페일오버 참고)
  2. 마스터가 레플리카로 바뀔 때까지 기다린다.
  3. 마지막으로 레플리카처럼 노드를 업그레이드한다.
  4. 업그레이드한 노드가 마스터가 되길 원한다면, 새 수동 페일오버를 트리거해서 업그레이드된 노드를 다시 마스터로 바꾼다.

이 절차를 따라 모든 노드가 업그레이드될 때까지 한 번에 하나씩 업그레이드해야 해요.

Redis Cluster로 마이그레이션

Redis Cluster로 마이그레이션하려는 사용자는 단일 마스터만 가지고 있을 수도 있고, 이미 사전 샤딩(presharding) 설정을 사용 중일 수도 있어요. 후자는 키가 어떤 사내(in-house) 알고리즘 또는 클라이언트 라이브러리나 Redis 프록시가 구현한 샤딩 알고리즘으로 N개 노드에 분할된 경우예요.

두 경우 모두 Redis Cluster로 쉽게 마이그레이션할 수 있어요. 다만 가장 중요한 세부 사항은 애플리케이션이 다중 키 작업을 사용하는지, 어떻게 사용하는지예요. 세 가지 경우가 있어요.

  1. 다중 키 작업, 또는 여러 키를 포함하는 트랜잭션이나 Lua 스크립트를 사용하지 않는다. 키는 독립적으로 접근된다(같은 키에 대해 여러 명령을 묶는 트랜잭션이나 Lua 스크립트로 접근하더라도).
  2. 다중 키 작업, 또는 여러 키를 포함하는 트랜잭션이나 Lua 스크립트를 사용하되 같은 해시 태그를 가진 키로만 사용한다. 즉, 함께 사용되는 키들이 모두 동일한 {...} 부분 문자열을 갖는다. 예를 들어 다음 다중 키 작업은 같은 해시 태그 맥락에서 정의된다: SUNION {user:1000}.foo {user:1000}.bar.
  3. 다중 키 작업, 또는 여러 키를 포함하는 트랜잭션이나 Lua 스크립트를 명시적이거나 같은 해시 태그가 없는 키 이름으로 사용한다.

세 번째 경우는 Redis Cluster가 처리하지 못해요. 애플리케이션이 다중 키 작업을 사용하지 않거나 같은 해시 태그 맥락에서만 사용하도록 수정해야 해요.

1번과 2번은 다루기 때문에, 그 두 경우에 집중할게요. 두 경우는 같은 방식으로 처리되므로 문서에서 구분하지 않을 거예요.

사전 샤딩이 없다면 N=1인 N개의 마스터로 기존 데이터셋이 분할되어 있다고 가정할 때, 데이터셋을 Redis Cluster로 마이그레이션하려면 다음 단계가 필요해요.

  1. 클라이언트를 중지한다. 현재 Redis Cluster로의 자동 라이브 마이그레이션은 불가능해요. 애플리케이션/환경 맥락에서 라이브 마이그레이션을 조율해서 수행할 수도 있을 거예요.
  2. BGREWRITEAOF 명령으로 N개 마스터 모두에 대한 append only file(AOF 파일)을 생성하고, AOF 파일이 완전히 생성될 때까지 기다린다.
  3. AOF 파일을 aof-1부터 aof-N까지 어딘가에 저장한다. 이 시점에 원한다면 기존 인스턴스를 중지할 수 있어요(가상화되지 않은 배포에서는 보통 같은 컴퓨터를 재사용해야 하므로 유용해요).
  4. N개의 마스터와 0개의 레플리카로 구성된 Redis Cluster를 만든다. 레플리카는 나중에 추가할 거예요. 모든 노드가 지속성(persistence)에 append only file을 사용하는지 확인한다.
  5. 모든 클러스터 노드를 중지하고, 그들의 append only file을 기존 append only file로 교체한다. 첫 번째 노드에는 aof-1, 두 번째 노드에는 aof-2, 이런 식으로 aof-N까지.
  6. 새 AOF 파일로 Redis Cluster 노드를 다시 시작한다. 노드들은 설정에 따라 있으면 안 되는 키들이 있다고 불평할 거예요.
  7. redis-cli --cluster fix 명령으로 각 노드가 권한이 있거나 없는 해시 슬롯에 따라 키가 마이그레이션되도록 클러스터를 고친다.
  8. 마지막에 redis-cli --cluster check로 클러스터가 괜찮은지 확인한다.
  9. Redis Cluster 인식 클라이언트 라이브러리를 사용하도록 수정된 클라이언트를 다시 시작한다.

외부 인스턴스에서 Redis Cluster로 데이터를 가져오는 대체 방법이 있는데, redis-cli --cluster import 명령을 사용하는 거예요.

이 명령은 실행 중인 인스턴스의 모든 키를 (소스 인스턴스에서 키를 삭제하면서) 지정된 기존 Redis Cluster로 옮겨요. 다만 소스 인스턴스로 Redis 2.8을 사용하면 마이그레이션 연결 캐싱을 구현하지 않아서 작업이 느릴 수 있어요. 그러니 이 작업을 수행하기 전에 소스 인스턴스를 Redis 3.x 버전으로 다시 시작하는 것이 좋을 거예요.

참고: Redis 5부터, 하위 호환성이 아니라면 Redis 프로젝트는 더는 slave라는 단어를 사용하지 않아요. 안타깝게도 이 명령에서는 slave가 프로토콜의 일부라서, 이 API가 자연스럽게 지원 중단(deprecated)될 때만 그런 발생을 제거할 수 있을 거예요.

더 알아보기