복제

복제(replication) 은 웹 애플리케이션 개발시에 반드시 직면하게 되는 문제 중 하나이다. 이유는 두 가지인데

  1. 데이터의 무결성을 위해. 장애가 발생했을 시에도 데이터가 손실되지 않게 하기 위해서이다
  2. 퍼포먼스 향상을 위해. 대부분의 웹 애플리케이션은 읽기가 많고, 쓰기가 적다. 이런 구조에서는 복제를 통해서 애플리케이션의 성능을 향상시킬 수 있다.

리더와 팔로워

데이터를 복제하는 방법은 크게 세 가지가 있다. 단일 리더, 다중 리더, 리더 없는 복제이다. 이전부터 대표적으로 사용되던 복제는 단일 리더 복제이다. 단일 리더 복제는 다음과 같은 특징을 지닌다.

  1. 복제 서버 중 하나가 리더 서버이다. 클라이언트에서 쓰기를 할 때는 리더 서버로 요청을 보낸다.
  2. 다른 복제 서버는 팔로워 서버이다. (슬레이브, 핫 스텐바이 서버라고도 부른다) 리더가 로컬 저장소에 새로운 데이터를 기록할 때마다 데이터 변경을 복제 로그나 변경 스트림의 일부러 팔로워에거 전송한다.

그러면 다음과 같은 궁금증이 생길 수 있다. 슬레이브가 아주 많다면, 모든 슬레이브에 복제된 후에 클라이언트로 응답을 내리면 너무 느리지 않을까? 이를 보완하기 위해 복제 방식도 두 가지가 있다.

동기식 대 비동기식 복제

리더에서 팔로워로 모든 데이터를 복사한 후에, 클라이언트로 응답을 내리는 케이스가 있다. 이를 동기식 이라고 부른다. 동기식에서는 모든 팔로워들이 ok 를 보낼때까지 클라이언트로 응답하지 않는다. 이 방식의 장점은 팔로워와 리더가 같은 데이터를 지니게 된다는 점이다. 다만, 동기 팔로워가 응답하지 않는다면 쓰기가 처리될 수 없다. 즉, 하나의 팔로워라도 장애가 발생한다면 데이터베이스에 쓰기가 되지 않는 것이다.

이런 이유로 모든 팔로워와 동기식으로 동작하는것은 비현실적이다. 현실적으로는 단 하나의 팔로워하고만 동기식으로 동작하고, 나머지는 비동기식으로 동작하도록 구성한다.

보통 리더 기반 복제는 완전히 비동기식으로 동작하는데, 이 경우에 리더가 잘못되고 복구할 수 없다면 팔로워에 반영되지 않은 쓰기는 모두 유실된다. 그러나, 모든 팔로워가 잘못되더라도 어쨌든 쓰기는 가능하다는 큰 장점이 있다.

노드 중단 처리

시스템의 모든 노드는 장애로 인해 예기치 않게 중단될 수 있다. 혹은, 계획된 유지보수나 rolling upgrade 로 인하여 일부 노드의 동작이 멈출 수 있다. 따라서 개별 노드의 장애에도 전체 시스템이 영향받지 않아야 하고, 노드가 복구된 경우 리더와 데이터 동기화가 보장되어야 한다.

어떻게 이런 고가용성을 유지할 수 있을까?

팔로워 장애:따라잡기 복구

  1. 팔로워는 리더로부터 수신한 데이터 변경 로그를 로컬 디스크에 보관한다.
  2. 팔로워에 장애가 발생한 후 복구되면, 보관된 로그에서 결함이 발생하기 전에 처리한 마지막 트랜젝션을 알아낸다.
  3. 리더에 연결해 장애가 발생한 동안 받지 못한 데이터 변경을 요청한다.

리더 장애: 장애 복구

반면에 리더의 장애를 처리하는 일은 까다롭다.

  1. 리더에 장애가 발생했음을 알아차려야 한다.
  2. 팔로워 중 하나를 리더로 승격시켜야 한다.
  3. 다른 팔로워는 새로운 리더로부터 데이터 변경 소식을 받아야 한다.

이런 과정을 장애 복구(failover) 라 한다. 보통 다음과 같은 프로세스로 진행된다.

  1. 리더가 장애인지 판단한다
  2. 노드들은 서로 메시지를 주고받는다. 일정 시간 동안 노드가 응답하지 않으면(예를 들면 30초), 죽은 것으로 간주한다.
  3. 새로운 리더를 선택한다.
  4. 팔로워끼리 투표를 통해 선택하거나, 제어 노드에 의해 새로운 리더가 임명될 수 있다. 새로운 리더로 적합한 후보는 이전 리더의 최신 데이터 변경사항을 가진 복제 서버다
  5. 새로운 리더를 위해 시스템을 재설정한다.
  6. 클라이언트는 새로운 쓰기 요청을 새 리더에게 보내야 한다.
  7. 이전 리더가 복귀하더라도, 자신이 리더가 아니라 팔로워임을 눈치챌 수 있어야 한다.

이 과정은 잘못될 수 있는 가능성이 너무 많다.

  1. 비동기식 복제를 사용한다면 새로운 리더는 이전 리더가 실패하기 전에 이전 리더의 쓰기를 수신하지 못할 수 있다.
  2. 오래된 팔로워가 리더로 승격될 수 있다. 즉, 이전 리더의 최신 데이터 변경사항을 가진 팔로워가 리더가 되어야 하는데 그렇지 못할 수 있다. 이 경우 예전에 사용한 Auto Increment 키를 다시 사용하는 케이스가 발생할 수 있는데, 이 키를 가지고 다른 데이터베이스의 키로 삼는 경우 장애가 발생할 수 있다.
  3. 특정 경우에 두 개의 노드가 모두 자신이 리더라 믿을 수 있다. 이를 split brain 이라고 부르는데, 매우 위험한 상황이다.

이를 쉽게 해결할 수 있는 방법은 없다. 그렇기 때문에 일부 운영팀은 자동 복구를 지원하더라도, 수동 복구를 더 선호하기도 한다.

복제 지연 문제

대부분의 웹 애플리케이션은 읽기가 많고 쓰기가 적다. 이 경우에는 리더로 쓰기 요청을 보내고, 팔로워에서 읽어감으로써 애플리케이션의 성능을 향상시킬 수 있다. 그러나, 리더와 팔로워 사이에 데이터 복제에 딜레이가 있다면, 데이터베이스의 상태 불일치가 발생한다. 즉, 리더에 쓰고 팔로워에서 읽을 때 데이터가 없거나, 이전 데이터가 보이는 현상이 발생하는 것이다.

자신이 쓴 내용 읽기

자신이 리더에 쓰고 팔로워에서 읽을 때, 사용자가 쓰기를 수행한 직후 데이터를 읽으면 복제가 되지 않은 데이터를 읽을 수 있다. 즉, 자기가 쓴 내용이 보이지 않게 되는 것이다. 이런 상황을 쓰기 후 읽기 일관성 이 보장되지 않았다고 이야기한다. 어떻게 이를 해결할 수 있을까? 다양한 기법들이 가능하다.

  1. 사용자가 수정한 내용은 리더에서 읽는다. 그 밖의 내용은 팔로워에서 읽는다.
  2. 다른 기준을 마련한다. 예를 들면, 마지막 갱신 시간을 찾아서 마지막 갱신 후 1분 동안은 모두 리더에서 읽기를 수행한다.

동일한 사용자가 여러 디바이스를 사용할 때는, 디바이스 간(cross-device) 쓰기 후 읽기 일관성이 보장되어야 한다.

단조 읽기

비동기식 팔로워에서 발생할 수 있는 두 번째 이상 현상은 사용자가 시간이 거꾸로 흐르는 듯한 현상을 목격할 수 있다는 점이다. 이는 다음과 같은 현상이다.

monotonic_read

  1. 사용자가 최신 데이터가 반영된 팔로우 서버에서 읽는다.
  2. 그 다음에 아직 복제가 완료되지 않은 팔로우 서버에서 읽는다.

단조 읽기(monotonic read) 는 이런 종류의 이상현상이 일어나지 않게 보장한다. 즉, 이전에 새로운 데이터를 읽은 후에는 이전 데이터를 읽지 않게 보장하는 것이다.

단조 읽기를 달성하는 한 방법은 사용자가 항상 동일한 팔로우 서버에서 데이터를 읽어가게 하는 것이다. 이를 위해서는 임의 선택보다는 사용자 ID 를 기반으로 하는 해시를 사용하여 복제 서버를 선택하게 하면 된다. 다만 이 경우에 해당 팔로우 서버에 장애가 발생하면, 유저를 다른 팔로우 서버로 재라우팅 해 주어야 한다.

일관된 순서로 읽기

consistent_prefix_read

이 현상은 리더가 여러 개일 때 발생한다. 즉, 이전에 저장된 데이터가 먼저 노출되는 경우가 발생하는 것이다. 이런 종류의 이상 현상을 방지하려면 일관된 순서로 읽기를 보장하여야 한다. 즉, 일련의 쓰기가 특정 순서로 발생한다면 이 쓰기를 보는 유저들은 같은 순서로 쓰여진 내용을 읽을 수 있어야 한다는 것이다.

데이터베이스가 항상 같은 순서로 쓰기를 한다면 이는 보장될 수 있다. 그러나, 여러 개의 리더에 쓰기가 동시에 발생한다면 쓰기의 전역 순서가 없기 때문에, 사용자는 예전 상태와 새로운 상태를 동시에 확인할 수 있다.

복제 지연을 위한 해결책

만일 복제 지연이 몇 분이나 몇 시간으로 증가한다면, 위에서 이야기 한 문제를 해결하기 위해 대책을 세워야 한다. 먼저, 앞에서 설명한 것 처럼 애플리케이션단에서 지원해주는 방법이 있다(같은 팔로워에서 읽는다던지). 그러나, 애플리케이션 코드에서 이를 다루는 것은 너무 복잡하여 실수할 가능성이 높다. 단일 데이터베이스라면 트렌젝션 사용하는 것도 좋은 해결책이다. 그러나 분산 데이터베이스에서 트렌잭션을 사용하는 것은 좀 더 어렵기 때문에 많은 시스템이 분산 데이터베이스에서 트랜젝션 지원을 포기하였다.

다중 리더 복제

단일 리더 복제는 주요한 단점이 있다 : 단일 리더에 장애가 발생한다면 해당 시스템은 동작을 중지하게 된다. 이를 막기 위해 리더를 여러개 둘 수 있다. 복제는 여전히 같은 방식을 사용한다.

multi_leader

단일 리더와 비교하여 다음과 같은 장점이 있다.

  1. 성능
  • 다중 리더 설정에서는 모든 쓰기는 로컬 데이터센터에서 처리한 후 비동기 방식으로 다른 데이터센터로 복제한다. 즉, 물리적으로 가까이에 있는 데이터센터를 활용할 수 있기 때문에 사용자가 인지하는 성능은 더 좋다.
  1. 데이터센터 중단 내성
  • 리더가 고장나더라도 다른 데이터 센터에서 쓰기를 처리하고, 고장난 리더가 다시 온라인으로 돌아왔을 때 다른 리더를 따라잡으면 된다.

위와 같은 장점이 있음에도 다중 리더 사용에 주의해야 하는 이유는 큰 단점이 있기 때문이다. 동일한 데이터를 다른 두 개의 데이터 센터에서 동시에 변경할 수 있다. 이 때 발생하는 쓰기 충돌은 반드시 해소하여야 한다. 일부 데이터베이스는 다중 리더 설정을 지원한다. 하지만 새로 추가된 기능이기 때문에, 미묘한 설정상의 실수나 다른 데이터베이스 기능과 뜻밖의 상호작용이 있을 수 있다. 예를 들면 Auto Increment 키, 트리거, 무결성 제약은 문제가 될 소지가 많다. 이런 이유로 다중 리더 복제는 가능하면 피해야 하는 영역으로 간주되곤 한다.

쓰기 충돌 다루기

write_conflet

다중 리더 복제의 가장 큰 문제는 쓰기 충돌이다. 이는 같은 데이터를 각기 다른 리더에서 두 사용자가 변경하였을 때, 어떤 데이터로 덮어써야 할지 잘 모르게 되는 현상이다.

충돌 회피

가장 간단한 전략은 충돌을 회피하는 것이다. 특정 레코드의 모든 쓰기가 동일한 리더에서 처리되도록 하면 충돌은 발생하지 않는다. 많은 다중 리더 복제 구현 사례에서 충돌을 잘 처리하지 못하기 때문에 충돌을 피하는 전략을 취하는 것이 자주 권장된다.

그러나 만약에 한 데이터센터가 고장나 다른 데어터센터로 라우팅하거나 하는 현상이 발생할 수 있다. 이런 상황에서는 충돌 회피가 실패한다. 이런 경우도 대비하여야 한다.

일관된 상태 수렴

다중 리더 설정에서는 쓰기 순서가 정해지지 않아 최종 값이 무엇인지 명확하지 않다. 위의 그림에서 리더 1의 제목은 B > C 로 갱신되고, 리더 2의 제목은 C > B 로 갱신된다. 이 중 어떤 순서도 다른 순서보다 더 정확 하지 않다. 즉 그냥 쓰여진 순서대로 처리하면 두 리더의 상태가 충돌이 발생한다는 의미이다. 따라서 다른 전략이 필요하다.

  1. 각 쓰기에 고유 ID 를 부여하고, 가장 높은 ID 를 가진 쓰기를 고른다. 다른 쓰기는 버린다. 타임스템프를 사용하는 경우를 최종 쓰기 승리(last write wins, LWW) 라 부른다. 이 접근 방식은 대중적이지만 데이터 유실 위험이 있다.
  2. 각 복제 서버에 고유 ID 를 부여하고 높은 숫자의 복제 서버에서 생긴 쓰기를 가장 우선적으로 적용한다. 이 접근 방식 또한 데이터 유실의 위험성이 있다.
  3. 어떻게든 값을 병합한다. 예를 들면 사전 순으로 정렬한 후 연결한다.
  4. 명시적 데이터 구조에 충돌을 기록해 모든 정보를 보존한다. 나중에 사용자에게 충돌을 보여주는 애플리케이션 코드를 작성한다.

사용자 정의 충돌 해소 로직

충돌을 해소하는 가장 적합한 방법은 애플리케이션에 따라 다르다. 즉, 애플리케이션에서 충돌을 해소해주어야 한다는 의미이다.

  1. 쓰기 수행 중
  • 쓰기 수행 중에 충돌을 감지하면 충돌 핸들러를 호출한다. 이 핸들러는 충돌 내용을 사용자에게 보여주지는 않고 뒷단에서 빠르게 충돌을 해소한다.
  1. 읽기 수행 중
  • 충돌을 감지하면 모든 충돌 쓰기를 저장한다. 다음번 데이터를 읽으면 여러 버전의 데이터가 사용자에게 반환된다.

리더 없는 복제

일부 데이터 저장소 시스템은 리더의 개념을 버리고 모든 복제 서버가 쓰기를 직접 받을 수 있게 허용하기도 한다. 이는 아마존이 내부 다이나모DB 에서 활용한 이후, 데이터베이스 아키텍쳐로 유행했다. 이런 종류의 데이터베이스를 다이나모 스타일이라 한다.

일부 시스템에서는 클라이언트가 직접 여러 복제 서버에 쓰기를 전송하는 반면, 코디네이터 노드(주키퍼와 같은)가 클라이언트를 대신에 이를 수행하기도 한다. 리더 데이터베이스와 달리 코디네이터 노드는 특정 순서로 쓰기를 수행하지는 않는다.

노드가 다운되었을 때 데이터베이스에 쓰기

broken_node

세 개의 노드를 가진 데이터베이스가 있고, 그 중 하나를 사용할 수 없다고 해 보자. 클라이언트는 모든 노드 서버에 쓰기요청을 하니, 데이터는 유실되지 않는다. 다만 이 노드 서버가 다시 온라인이 되었을 때 이 서버는 오래된 데이터를 가지고 있어서 읽기요청 시에 오래된 데이터가 올 수 있다.

이를 해결하기 위해 읽기 요청을 병렬로 여러 노드에 전송한다. 그러면 클라이언트는 여러 노드에서 다른 응답을 받을 수 있다. 그러면 버전을 확인하여 최신 데이터를 사용하면 된다.

읽기 복구와 안티 엔트로피

위와 같은 경우에 복구된 노드의 오래된(outdated) 데이터를 업데이트하여야 한다. 누락된 쓰기 데이터를 어떻게 업데이트 할 수 있을까? 다이나모 스타일 데이터스토어는 다음 두 가지 메커니즘을 사용한다.

  1. 읽기 복구
  • 클라이언트가 여러 노드에서 병렬로 읽기를 수행하면 오래된 응답을 감지할 수 있다. 이 때 복제 서버에 새로운 값을 업데이트한다.
  1. 안티 엔트로피 처리
  • 백그라운드 프로세스를 두고 복제 서버 간 데이터 차이를 지속적으로 찾아, 누락된 데이터를 하나의 복제 서버에서 다른 복제 서버로 복사한다.

읽기와 쓰기를 위한 정족수

위의 그림에서 복제 서버 세 개 중 두 개에서만 쓰기를 처리하여도 성공한 것으로 간주하였다. 세 개의 복제 서버 중 하나만 쓰기를 허용한다면 어떻게 해야 할까?

n 개의 복제 서버가 있을 때 모든 쓰기는 w 개의 노드에서 성공해야 쓰기가 확정되고, r 개의 노드에 질의해야 최신의 데이터를 조회할 수 있다. (n = 3, w = 2, r = 2) w + r > n 이면 읽을 때 최신 값을 얻을 것으로 기대한다. 최소한 r 개의 노드 중 하나에서 최신 값을 읽을 수 있기 때문이다. 이런 w와 r 을 따르는 읽기와 쓰기를 정족수 읽기와 쓰기라고 부른다.

w + r > n 이면 다음과 같이 사용 불가능한 노드를 용인한다.

  • w < n 이면 노드 하나를 사용할 수 없어도 여전히 쓰기를 처리할 수 있다.
  • r < n 이면 노드 하나를 사용할 수 없어도 여전히 읽기를 처리할 수 있다.
  • n = 3, w = 2, r = 2 면 사용 불가능한 노드 하나를 용인한다.
  • n = 5, w = 3, r = 3 이면 사용 불가능한 노드 두개를 용인한다.

follower_broken

정리

복제는 다양한 용도로 사용될 수 있다.

  1. 고가용성
  • 한 장비가 다운될 때도 시스템이 계속 동작하게 한다.
  1. 연결이 끊긴 작업
  • 네트워크 중단이 있을 때도 애플리케이션이 계속 동작하게 한다.
  1. 지연시간
  • 지리적으로 사용자에게 데이터를 가까이 배치해 사용자가 더 빠르게 작업할 수 있게 한다.
  1. 확장성
  • 복제본에서 읽기를 수행해 단일 장비에서 다룰 수 있는 양보다 많은 양의 읽기 작업을 처리할 수 있따.

복제는 매우 까다로운 문제다. 동시성 그리고 잘못될 수 있는 모든 사항을 주의 깊게 생각하고 그 결함의 결과를 주의 깊게 다뤄야 한다. 아울러 복제에 대한 세 가지 주요 접근 방식을 살펴보았다.

  1. 단일 리더 복제
  • 클라이언트는 모든 쓰기를 단일 노드로 전송하고, 리더는 데이터 변경 스트림을 다른 복제 서버로 전송한다. 읽기는 모든 복제 서버가 할 수 있지만 오래된 값이 나올 수 있다.
  1. 다중 리더 복제
  • 클라이언트는 쓰기 액션을 받아들일 수 있는 노드로 전송한다. 리더는 데이터 변경 스트림을 다른 모든 리더와 팔로워에게 전송한다.
  1. 리더 없는 복제
  • 클라이언트는 각 쓰기를 여러 노드로 전송한다. 클라이언트는 오래된 데이터를 감지하고 이를 바로잡기 위해 병렬로 여러 노드에서 읽는다.

각 접근 방식마다 장단점이 있다. 단일 리더 복제는 이해하기 쉽고 충돌 해소에 대한 우려가 없어 널리 사용된다. 다중 리더 복제나 리더 없는 복제는 결함 노드, 네트워크 중단, 지연 시간 급증이 있는 상황에서 더욱 견고하다. 다만 설명하기 어렵고 일관성이 거의 보장되지 않는다는 것이 단점이다.

복제는 동기 혹은 비동기로 이뤄진다. 보통 동기로 이뤄지는 경우는 거의 없으며, 반 동기적으로 이뤄지곤 한다.

복제 지연으로 발생할 수 있는 몇 가지 이상 현상을 설명하였다. 그리고 애플리케이션이 복제 지연시 어떻게 동작해야 하는지 모델을 살펴보았다.

  1. 쓰기 후 읽기 일관성
  • 사용자는 자신이 제출한 데이터를 항상 볼 수 있어야 한다.
  1. 단조 읽기
  • 사용자가 어떤 시점의 데이터를 본 순간, 이전 시점의 데이터를 나중에 볼 수 없다.
  1. 일관된 순서로 읽기
  • 데이터를 인과에 맞게 보아야 한다. 쓴 순서대로 읽어야 한다.

마지막으로 다중 리더 복제와 리더 없는 복제 접근 방식에는 쓰기 충돌이 발생할 수 있음을 설명했다.