Backend/Redis

레디스 복제(Replication)와 센티널(Sentinel)

olsohee 2024. 5. 30. 22:28

고가용성

가용성이란 일정 기간 동안 서비스를 정상적으로 사용할 수 있는 시간의 비율을 뜻하며, 이 값이 클수록 가용성이 높다고 한다. 서비스를 안정적으로 운영하기 위해서는 가용성을 높일 수 있는 방안을 도입해야 한다. 

 

레디스는 고가용성을 확보하기 위해 다음 두 가지 기능을 제공한다.

  • 복제: 마스터 노드의 데이터를 복제본 노드로 실시간 복사하는 기능이다. 따라서 마스터 노드의 서버에 장애가 생겨 데이터가 유실된다고 해도 복제본 노드에서 데이터를 확인할 수 있다.
  • 자동 페일오버: 마스터 노드에서 발생한 장애를 감지해 레디스로 들어오는 클라이언트 연결을 자동으로 복제본 노드로 리다이렉션하는 기능이다. 따라서 마스터 노드의 서버에 장애가 발생했을 때 빠른 조치가 가능하다.

복제(Replication)

복제를 하는 이유

레디스 서버를 한 개만 두지 않고 복제본을 함께 두는 이유는 무엇일까? 복제를 통해 다음과 같은 이점을 얻을 수 있다.

  • 장애 대응: 서비스를 안정적으로 운영하기 위해서는 마스터 데이터베이스가 다운됐을 때 대신 사용할 복제본이 필요하다.
  • 부하 분산: 대규모 서비스에서 복제본은 트래픽을 감소시키는 역할을 수행할 수 있다. 실시간으로 마스터 노드에 접근해 데이터를 가져가는 서비스가 많을 때, 일부 트래픽이 복제본을 바라보게 한다면 부하 분산을 통해 마스터 노드로의 트래픽을 줄일 수 있다.
  • 데이터 백업시 운영 서버와의 분리: 운영 중인 마스터 노드에서 매번 데이터의 백업을 받는 것은 부담스러운 작업이다. 백업을 복제본에서 수행하면 백업 작업이 서비스에 미치는 영향을 최소화할 수 있다.

참고로 레디스 버전 2.6 이상부터 복제본 노드는 기본으로 읽기 전용으로 동작하기 때문에 데이터를 읽는 커맨드만 수행할 수 있다. 따라서 모든 데이터 입력은 마스터 노드에서 이뤄지는 게 일반적이다.

복제 구조

레디스의 복제 구조는 다음과 같다.

  • 레디스는 마스터/슬레이브 형태의 복제 모델을 제공한다.
  • 마스터에는 여러 개의 복제본이 연결될 수 있으며, 슬레이브에 새로운 복제본을 추가하는 것도 가능하다.
  • 하나의 복제 그룹에서는 항상 한 개의 마스터 노드만 존재한다.
  • 마스터 노드만 데이터를 업데이트하는 커맨드를 수행할 수 있으며, 하위 슬레이브는 모두 데이터를 읽는 커맨드만 수행할 수 있다.

복제 구조 구성

레디스에서 복제를 사용하는 방법은 다음과 같다.

REPLICAOF <마스터 ip> <마스터 port>

위와 같이 설정하면 레디스의 데이터를 업데이트하는 모든 커맨드는 노드 A에서 실행되기 때문에 서비스 애플리케이션은 마스터 노드인 A의 정보를 바라봐야 한다. 그리고 마스터 A가 장애로 인해 사용하지 못하게 됐을 때는 애플리케이션의 연결 설정을 B로 변경하면 서비스를 계속할 수 있다. 

복제 매커니즘

지금부터 레디스의 복제가 이뤄지는 매커니즘을 설명한다. 그리고 이 모든 과정은 자동으로 이뤄지며 개발자가 별도의 설정을 하지 않아도 된다.

버전 7 이전에서의 복제 매커니즘

버전 7 이전에서는 repl-diskless-sync 옵션의 기본값이 no이며, 다음과 같은 방식으로 복제가 이뤄졌다.

  1. REPLICAOF 커맨드로 복제 연결을 시도한다.
  2. 마스터 노드에서는 fork로 자식 프로세스를 새로 만든 뒤 RDB 스냅샷을 생성한다.
  3. 2번 과정이 이뤄지는 동안 마스터 노드에서 수행된 모든 데이터 변경 작업은 레디스 프로토콜(RESP) 형태로 마스터의 복제 버퍼에 저장된다.
  4. RDB 파일이 생성 완료되면 파일은 복제본 노드로 복사된다.
  5. 복제본에 저장됐던 모든 내용을 모두 삭제한 뒤 RDB 파일을 이용해 데이터를 로딩한다.
  6. 복제 과정 동안 버퍼링됐던 복제 버퍼의 데이터를 복제본으로 전달해 수행시킨다.

마스터에서 RDB 파일을 생성하는 과정과 복제본에서 RDB 파일을 읽어오는 과정은 디스크 I/O 작업이다. 따라서 이와 같은 복제 과정에서 복제 속도는 디스크 I/O 처리량에 영향을 받는다. 

 

그러나 버전 7 이후부터는 repl-diskless-sync 옵션의 기본값이 yes로, 디스크를 사용하지 않는 방식으로 복제가 이뤄진다.

버전 7 이후에서의 복제 매커니즘

  1. REPLICAOF 커맨드로 복제 연결을 시도한다.
  2. 마스터 노드는 소켓 통신을 이용해 복제본 노드에 바로 연결하며, RDB 파일은 생성됨과 동시에 점진적으로 복제본의 소켓에 전송된다.
  3. 2번 과정이 이뤄지는 동안 마스터 노드에서 수행된 모든 데이터 변경 작업은 레디스 프로토콜(RESP) 형태로 마스터의 복제 버퍼에 저장된다.
  4. 소켓에서 읽어온 RDB 파일을 복제본의 디스크에 저장한다. (복제본의 repl-diskless-load 옵션은 기본값이 disabled로, 소켓에서 읽어온 RDB 스냅샷 데이터를 바로 메모리에 로드하지 않고, 일반 복제본 노드의 디스크에 저장하는 과정을 거친다.)
  5. 복제본에 저장된 모든 데이터를 모두 삭제한 뒤 RDB 파일 내용을 메모리에 로딩한다.
  6. 복제 버퍼의 데이터를 복제본으로 전달해 수행시킨다.

비동기 방식으로 동작하는 복제 연결

복제 연결이 된 상태에서 마스터에서 복제본으로의 데이터 전달은 비동기로 동작한다.

위 그림에서 마스터에 데이터를 입력하는 커맨드가 수행되면 마스터 노드에서 커맨드를 처리한 이후 복제 과정과 별개로 클라이언트에게 OK 응답을 보낸다. 따라서 클라이언트는 복제 시간을 기다리지 않고 빠른 응답을 받을 수 있다. 

복제 ID

모든 레디스 인스턴스는 복제 ID를 가지고 있다. 복제 기능을 사용하지 않는 인스턴스라도 모두 랜덤 스트링 값의 복제 ID를 가지며, 복제 ID는 오프셋과 쌍으로 존재한다. 그리고 레디스 내부의 데이터가 수정되는 커맨드를 수행할 때마다 오프셋이 증가한다.

 

INFO REPLICATION 커맨드를 통해 복제 연결 상태를 확인할 수 있다. 

> INFO Replication
# Replication
role:master # 해당 인스턴스는 master이다.
connected_slaves:0 # 연결된 슬레이브가 없다.
master_failover_state:no-failover 
master_replid:e3b06d3eba522894a240a8a9ce3e808dd5ccfd7a # 복제 ID를 의미한다.
master_replid2:0000000000000000000000000000000000000000 
master_repl_offset:709 # 오프셋을 의미한다.
second_repl_offset:-1 repl_backlog_active:1 
repl_backlog_size:67108864 
repl_backlog_first_byte_offset:1 repl_backlog_histlen:709

 

이 인스턴스에 슬레이브를 연결한 뒤 다시 INFO REPLICATION 커맨드를 사용해 정보를 확인해보자.

> INFO Replication
# Replication
role:master
connected_slaves:1 # 1개의 슬레이브를 갖는다.
slave0:ip=127.0.0.1,port=6002,state=online,offset=709,lag=0 
master_failover_state:no-failover 
master_replid:e3b06d3eba522894a240a8a9ce3e808dd5ccfd7a 
master_replid2:0000000000000000000000000000000000000000 
master_repl_offset:807
second_repl_offset:-1 repl_backlog_active:1 
repl_backlog_size:67108864 
repl_backlog_first_byte_offset:1 repl_backlog_histlen:709

 

그리고 새로 연결된 슬레이브에서  INFO REPLICATION 커맨드를 사용해 정보를 확인하면 다음과 같다.

> info replication
# Replication
role:slave # 해당 인스턴스는 슬레이브이다.
master_host:127.0.0.1
master_port:6001
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:723 
slave_priority:100
slave_read_only:1
connected_slaves:0
master_failover_state:no-failover 
master_replid:e3b06d3eba522894a240a8a9ce3e808dd5ccfd7a # 복제 ID를 의미한다.
master_replid2:0000000000000000000000000000000000000000 
master_repl_offset:801 # 오프셋을 의미한다.
second_repl_offset:-1 repl_backlog_active:1 
repl_backlog_size:67108864 
repl_backlog_first_byte_offset:710 
repl_backlog_histlen:14

 

복제 연결을 시작하면 슬레이브의 복제 ID는 마스터의 복제 ID와 같아진다. 그리고 오프셋은 슬레이브에서 마지막으로 수행된 마스터의 오프셋을 의미한다. 따라서 복제 ID와 오프셋이 같을 때 두 노드는 일치된 상태라는 것을 의미한다. 따라서 복제 ID와 오프셋을 통해 슬레이브가 마스터의 어디까지 복제됐는지 파악할 수 있다.

 

위 슬레이브 노드의 경우, 복제 ID는 마스터의 복제 ID와 같지만 오프셋은 다르다. 따라서 마스터 노드의 데이터를 일부 전달받지 못한 상태라는 것을 알 수 있다.

부분 재동기화

복제 연결이 끊길 때마다 마스터에서 RDB 파일을 새로 내려 복제본에 전달하는 과정을 거친다면, 네트워크가 불안정한 상황에서 복제 기능을 사용하는 레디스의 성능은 급격히 나빠질 것이다. 따라서 이를 방지하기 위해 레디스는 부분 재동기화 기능을 사용해서 안정적으로 복제 연결을 유지한다.

  • 마스터는 커넥션 유실을 대비해 백로그 버퍼라는 메모리 공간에 슬레이브에 전달한 커맨드 데이터들을 저장해둔다.
  • 하나의 복제 그룹에서 복제 ID와 오프셋을 이용하면 슬레이브가 마스터의 어느 시점까지의 데이터를 가지고 있는지 알 수 있다. 
  • 따라서 복제 연결이 잠시 끊긴 뒤 재연결되면, 슬레이브는 PSYNC 커맨드를 호출해 자신의 복제 ID와 오프셋을 마스터에게 전달한다.

  • 위 그림은 오프셋 900의 슬레이즈가 마스터 노드에 재연결을 시도하는 모습니다.
  • 만약 오프셋 901~915의 내용이 마스터의 백로그에 저장되어 있다면 마스터는 RDB 파일을 새로 저장할 필요 없이 백로그에 저장된 내용을 슬레이브에 전달함으로써 부분 재동기화를 진행할 수 있다.
  • 하지만 만약 마스터의 백로그 버퍼에 원하는 데이터가 남아 있지 않거나, 슬레이브가 보낸 복제 ID가 현재의 마스터와 일치하지 않다면 전체 재동기화를 시도한다.

세컨더리 복제 ID

한 개의 복제본 그룹 내의 모든 레디스 노드는 동일한 복제 ID를 갖는다.

 

만약 A 노드에 장애가 발생해 복제가 끊어지고, 복제본 B가 새로운 노드로 승격되는 상황을 생각해보자.

마스터 노드와의 복제가 끊어짐과 동시에 복제본은 새로운 복제 ID를 갖게 된다. 그 이유는 무엇일까? 복제 ID가 동일하다는 것은 동일한 데이터셋을 갖는다는 것을 의미한다. 만약 복제가 끊어진 뒤에도 노드 B가 기존의 복제 ID인 aaaaa를 유지한다면? 노드 B가 마스터로 동작하다가 장애가 해결되어 기존 마스터 노드였던 A와 다시 연결된다면 두 노드의 복제 ID가 같다. 따라서 동일한 오프셋이 동일한 데이터셋을 갖는다는 사실을 위반할 수 있다. 이러한 이유로 복제가 끊어지면 새로 마스터로 승격되는 노드는 새로운 복제 ID를 갖는 것이다.

 

위 그림에서 노드 B는 새로운 마스터로 승격됨과 동시에 새로운 복제 ID인 bbbbb를 갖게 됐으며, 기존 복제 ID는 master_replid2에 저장된다. 

 

그리고 기존 A의 복제본이었던 노드 C는 B에 연결되며, B의 복제 ID인 bbbbb를 복제 ID로 갖게 된다. 그리고 C도 기존에 A에 연결돼 있었기 때문에 기존 복제 ID는 master_replid2에 저장된다. 

 

그리고 노드 B와 C가 연결되는 과정에서는 재동기화를 수행한다. 두 노드의 master_replid2가 같기 때문에 C 노드는 B 노드에 부분 재동기화를 시도한다. 즉, 노드 B에서 RDB 파일을 백업받아서 C로 전송하는 전체 재동기화를 거치지 않아도 되기 때문에 불필요한 작업을 줄일 수 있으며 빠르게 복제 상태를 구축할 수 있다.

 

결과적으로 레디스가 2개의 복제 ID를 갖는 이유는 마스터로 승격되는 복제본 때문이며, 같은 복제 그룹 내에서 페일오버 이후 승격된 새로운 마스터에 연결된 복제본은 전체 재동기화를 수행하지 않을 수 있다.

읽기 전용 모드로 동작하는 슬레이브

버전 2.6 이후 레디스에서 복제를 구성하면 슬레이브는 기본으로 읽기 전용 모드로 동작한다(replica-read-only). 즉, 슬레이브에 새로운 데이터를 저장하는 것이 불가능하다. 클라이언트는 슬레이브에 연결되더라도 데이터를 읽는 커맨드만 수행 가능하며, SET과 같이 데이터를 조작하는 커맨드는 수행할 수 없다. 

 

  • 그러나 특정 상황에서 복제본의 replica-read-only 설정을 해제하고 싶을 수 있는데, 슬레이브를 대상으로 데이터를 조작할 경우 주의해야 한다.
  • 슬레이브가 재시작되거나 커넥션 유실로 마스터와 전체 재동기화를 수행하게 되면 슬레이브에서 수행한 데이터는 사라진다.
  • 슬레이브에 작성한 내용은 오직 로컬에서만 유지되며, 해당 노드에 연결된 다른 슬레이브로 전파되지는 않는다. 서브 복제본은 항상 최상위 마스터가 중간 복제본으로 보낸 것과 동일한 복제 프로토콜을 전달받기 때문이다. 따라서 다음 그림에서 B의 replica-read-only 설정을 끄고 클라이언트가 직접 B에 연결해 데이터를 조작하더라도, B에 복제 연결돼 있는 C는 항상 마스터 노드인 A와 동일한 데이터셋을 갖는다. B에서 변경된 데이터는 C에 전달되지 않는다.

백업을 사용하지 않는 경우에서의 데이터 복제

레디스에서 복제를 사용할 경우 마스터와 복제본은 백업 기능을 사용하는 것이 좋다. 그렇지 않으면 다음과 같은 장애 상황이 일어날 수 있다.

  1. 백업 기능을 사용하지 않는 마스터와 복제본이 존재한다.
  2. 마스터 노드가 장애로 인해 종료됐지만, 레디스 프로세스를 자동 재시작하는 시스템에 의해 노드가 재부팅된다. 그리고 이때 메모리 내용은 초기화된다.
  3. 복제본에는 데이터가 존재하지만 마스터로의 복제 연결을 시도한다.
  4. 마스터에서 복제본으로 빈 데이터셋을 전달한다.

만약 백업 기능을 사용했다면, 2번 상황에서 레디스가 재부팅될 때 백업 파일을 자동으로 읽어오기 때문에 데이터가 복원되며, 복원 내용이 복제본으로 전달된다.

 

또는 자동 재시작 기능을 사용하지 않았더라면, 복제본에는 데이터가 그대로 존재하기 때문에 복제본 데이터를 사용할 수 있다. 

 

따라서 데이터 안정성을 위해 복제 기능을 사용할 경우 백업 기능을 사용하는 것이 좋으며, 그렇지 않을 경우 마스터에서는 인스턴스 자동 재시작을 활성화하지 않는 것이 권장된다.

센티널

지금까지 레디스의 복제 기능에 대해 알아봤다. 그런데 마스터 노드가 죽었을 때 슬레이브 노드를 마스터로 승격시키고 애플리케이션이 새로 마스터로 승격된 슬레이브를 바라보게 하는 과정을 개발자가 직접 한다면 매우 번거롭다. 따라서 레디스에서는 센티널이라는 기능을 제공한다.

 

센티널은 데이터를 저장하는 기존 레디스 인스턴스와는 다른 역할을 하는 별도의 프로그램이며, 센티널의 자동 페일오버 기능을 사용하면 마스터 인스턴스에 장애가 발생하더라도 레디스를 계속 사용할 수 있도록 동작해 레디스의 다운타임을 최소화할 수 있다.

 

센티널은 다음 기능을 제공한다.

  • 모니터링: 마스터, 복제본 인스턴스의 상태를 실시간으로 확인한다.
  • 자동 페일오버: 마스터의 비정상 상태를 감지해 정상 상태의 복제본 중 하나를 마스터로 승격시킨다. 그리고 기본 마스터에 연결된 복제본은 새롭게 승격된 마스터에 연결된다.
  • 인스턴스 구성 정보 안내: 센티널은 클라이언트에게 현재 구성에서의 마스터 정보를 알려준다. 페일오버가 발생하면 변경된 마스터 정보를 재전달하기 때문에 페일오버가 발생하더라도 레디스의 엔드포인트 정보를 변경할 필요가 없다.

분산 시스템으로 동작하는 센티널

센티널은 자신이 SPOF(Single Point Of Failure)가 되는 것을 방지하기 위해 최소 3대 이상일 때 정상적으로 동작할 수 있도록 설계되었다. 따라서 하나의 센티널에 이상이 생기더라도 다른 센티널이 계속해서 역할을 수행할 수 있다.

 

다음 그림과 같이 클라이언트는 먼저 센티널에 연결해 마스터의 정보를 받아온다. 만약 센티널이 1개이고 그 하나의 센티널에 문제가 생겨 마스터 정보를 반환할 수 없게 된다면 클라이언트는 레디스로 새로운 커넥션을 맺을 수 없게 된다. 하지만 3개의 센티널이 동작하기 때문에 하나의 센티널에 문제가 생기더라도 클라이언트는 다른 센티널에서 마스터의 정보를 정상적으로 받아올 수 있다.

센티널은 쿼럼(quorum)이라는 개념을 사용한다. 쿼럼은 마스터가 비정상적이라는 것에 동의해야 하는 센티널의 수로, 쿼럼을 만족하면 페일오버를 시작한다. 일반적으로 센티널 인스턴스가 3개일 때 쿼럼은 2로 설정하며, 이 경우 최소 2개의 센티널 인스턴스가 마스터의 비정상 상태에 동의하면 페일오버를 시작한다.

 

센티널은 쿼럼을 이용한 과반수 선출 개념을 사용하기 때문에 3대 이상의 홀수로 구성하는 것이 좋다. 보통 3대로 구성하며, 좀 더 견고하게 장애에 대처하고자 한다면 5대로 구성할 수 있다.


Reference

  • 개발자를 위한 레디스, 김가림, 에이콘