책 정리와 리뷰

[데이터중심 어플리케이션 설계] 5장 정리

maruoov 2023. 7. 19. 21:07

복제란 네트워크로 연결된 여러 장비에 동일한 데이터 복사본을 유지한다는 것이다. 
데이터 복제의 장점은 여러 가지가 있다.

  • 지리적으로 사용가와 가깝게 데이터를 유지해 지연 시간을 줄인다
  • 시스템 일부에 장애가 발생해도 지속적으로 동작할수 있게 해 가용성을 높인다
  • 읽기를 수행하는 장비의 수를 확장해 읽기 처리량을 늘린다.

복제에서 모든 어려움은 복제된 데이터의 변경 처리에 있다.
노드 간 변경을 복제하기 위한 세 가지  방법인 단일리더, 다중리더, 리더없는 복제를 살펴보자.

 

리더 기반 복제 - 단일 리더 복제

데이터베이스의 복사본을 저장하는 노드를 복제 서버(replica) 라 한다. 
모든 복제 서버에 데이터가 있다는 사실을 어떻게 보장할수 있을까?
데이터베이스의 모든 쓰기는 모든 복제 서버에서 처리되어야 동일한 데이터를 유지할 수 있다.
이를 위한 가장 일반적인 해결책은 리더기반 복제 (능동/수동, 마스터/슬레이브 복제) 이다.

복제 서버중 하나를 리더(마스터, 프라이머리)로 지정하고, 다른 복제 서버들은 팔로워(읽기 복제서버, 슬레이브 등)라고 한다.
클라이언트가 쓰기 요청을 할때는 리더에게 보내야 한다.
리더는 먼저 로컬저장소에 새 데이터를 기록한다.
데이터 변경을 복제 로그나 변경 스트림으로 팔로워에게 전송하면, 각 팔로워는 리더가 처리한 것과 동일한 순서로 쓰기를 적용해 저장소를 갱신한다.

클라이언트가 읽기 요청을 할때는 리더 또는 임의 팔로워에게 질의할 수 있다.

 

동기식 대 비동기식 복제

복제에서 중요한 세부 사항은 복제가 동기식인지 비동기식인지이다.
아래 그림에서, 리더-팔로워1은 동기식, 리더-팔로워2는 비동기식으로 복제를 수행한다.


동기식의 경우, 리더는 팔로워가 데이터를 갱신할때까지 기다린다.
비동기식은 팔로워에게 메시지를 전송하지만 응답을 기다리지 않는다.

동기식 복제의 장점은 팔로워가 일관성 있는 최신 데이터를 갖는 것을 보장한다.
리더가 동작하지 않아도 최신 데이터는 팔로워에서 사용할수 있다.
단점은, 동기 팔로워가 응답하지 않는다면 쓰기가 처리될수 없다는 것이다. 
이 경우 모든 쓰기를 차단하고 동기 복제 서버가 사용가능할때까지 기다려야 한다. 
이런 이유때문에 모든 팔로워가 동기식인 상황은 비현실 적이다.

비동기식의 경우, 모든 팔로워가 잘못되더라도 쓰기 처리를 계속 할 수 있는 장점이 있다.
단점은, 리더가 잘못되고 복구할 수 없으면 팔로워에 복제되지 않은 쓰기는 유실된다.
쓰기가 클라이언트에 쓰기 요청 확인을 하더라도, 지속성을 보장하지 않는단 의미다.

새로운 팔로워 추가

복제 서버 수를 늘리거나 장애 노드 대체를 위해 새로운 팔로워를 추가해야 할 경우가 있다.
새로운 팔로워가 리더의 최신 데이터 복제본을 갖는걸 어떻게 보장할까? 한 노드에서 다른 노드로 데이터 파일을 복사하는 것만으론 충분하지 않다.
대개 이 과정은 중단없이 수행되며, 다음과 같다.

1. 리더의 일정 시점의 스냅샷을 가져온다.
2. 스냅샷을 새로운 팔로워에 복사한다.
3. 팔로워는 리더에게 스냅샷 이후 발생한 데이터 변경을 요청한다.
4. 3번 변경분까지 따라잡으면, 리더에 발생하는 데이터 변화를 이어서 처리한다.

노드 중단 처리

모든 노드는 장애로 인해 중단될 수도 있고, 유지보수 작업으로 인해 중단될 수도 있다.
리더 기반 복제에서 고가용성을 어떻게 달성할까?

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

팔로워는 매우 쉽게 복구할 수 있다.
로컬 저장소에 저장된 로그에서 결함 발생 전 처리한 마지막 트랜잭션을 알아낸다.
리더에게 위 트랜잭션 이후 발생한 모든 데이터 변경을 요청한다.

리더 장애 : 장애 복구

리더 장애 처리는 까다롭다. 팔로워중 하나를 리더로 승격해야 하고 재설정이 필요하다.
장애 복구는 보통 다음과 같은 단계로 구성된다.

1. 리더가 장애인지 판단한다. 대부분 시스템은 단순히 타임아웃을 수용한다. 노드들은 서로 메시지를 주고 받으며 일정 시간 응답이 없으면 죽은 것으로 간주한다.
2. 새로운 리더를 선택한다. 리더 선출 과정을 통해 이뤄지거나, 이전에 선출된 제어노드에 의해 새로운 리더가 임명된다.
3. 새로운 리더 사용을 위해 시스템을 재설정 한다. 클라이언트는 이제 쓰기 요청을 새로운 리더에게 보내야 한다. 

장애 복구 과정은 잘못될 수 있는 일들이 많다.

- 이전 리더의 쓰기 일부가 유실될 수 있다.
- 쓰기를 폐기/유실되는 경우 외부 저장소와 데이터 연관이 있다면 특히 위험하다.
- 두 노드가 모두 자신이 리더라고 믿을 수 있다 (스플릿 브레인)
- 리더 장애 여부를 판단하는 타임아웃이 너무 짧으면 불필요한 장애 복구가 발생할 수 있다.

 

복제 로그 구현

리더 기반 복제는 어떤 복제 방법을 사용할까?

구문 기반 복제

리더는 모든 쓰기 요청을 기록하고 쓰기를 실행한 다음 팔로워에게 전송한다.
각 팔로워는 쓰기요청을 실행한다.
이 방식은 복제가 깨질 수 있는 다양한 사례가 있다

- NOW() 나 RAND() 같은 비결정적 함수 사용시 복제서버마다 다른값 생성 가능성
- 자동 증가 컬럼을 사용하는 구문이나 데이터에 의존
- 부수 효과를 가진 트리거나 프로시저 사용 등

쓰기 전 로그 배송

리더는 추가 전용 로그를 쌓고, 팔로워는 이 로그를 처리하여 복제본을 만든다.
이 방식의 가장 큰 단점은 로그가 제일 저수준의 데이터 (어떤 디스크 블록에서 어떤 바이트를 변경했는지 등)를 기술하기 때문에
저장소 엔진과 밀접하게 엮이는 것이다.

논리적(로우 기반) 로그 복제

복제 로그를 위의 저장소 엔진과 분리하기 위한 대안은 다른 로그 형식을 사용하는 것이고, 이를 논리적 로그라 부른다.
RDB의 논리적 로그는 대개 로우 단위로 쓰기를 기술한 레코드 열이다.

트리거 기반 복제

사용자 정의 코드를 등록하여 데이터가 변경되면 실행하게 한다.
트리거는 변경을 분리된 테이블에 기록하여 복제한다. 이 방식은 많은 오버헤드가 존재하고 버그가 더 많이 발생한다.

 

복제 지연 문제

리더 기반 복제는 읽기 확장 아키텍처이고, 팔로워를 더 추가함으로써 읽기 요청 처리 용량을 늘릴 수 있다.
하지만 비동기 팔로워에서 데이터를 읽을때 팔로워가 뒤쳐졌다면 이전 데이터를 볼수 있다.
이런 불일치는 일시적인 상태에 불과하고, 팔로워는 결국 리더를 따라잡고 데이터를 일치 시킨다.
이런 효과를 최종적 일관성이라 한다.

자신이 쓴 내용 읽기

비동기식 복제에서 사용자가 쓰기를 수행한 후 같은 데이터를 읽을때, 아직 최신 데이터를 따라잡지 못한 팔로워에서 읽는다면 자신이 쓴 데이터를 읽지 못할수 있다.
이런 상황에서는 쓰기 후 읽기 일관성이 필요하다.
사용자가 수정한 내용을 읽을 때는 리더, 그 외에는 팔로워에서 읽거나
마지막 갱신 시각 이후 N분 동안은 리더에서 읽거나
클라이언트가 최근 쓰기 타임스탬프를 보내 이 후의 데이터만 보거나 할수 있다.

 

단조 읽기

비동기식 팔로워에서 겪을 수 있는 두 번째 이상 현상은 시간이 거꾸로 흐르는 현상을 겪을 수 있다.

사용자는 쓰기 처리를 한뒤, 데이터가 갱신된 팔로워에서 데이터를 읽는다. 
이후에는 최신 데이터가 없는 팔로워에서 데이터를 읽고, 갱신 이전의 데이터를 읽는다.

단조 읽기는 이런 이상 현상을 방지한다.
각 사용자의 읽기를 항상 동일한 팔로워에게 수행되게 하면 방지할 수 있다.

 

리더 기반 복제 - 다중 리더 복제

단일 리더 기반 복제는 리더가 하나만 존재하고 모든 쓰기는 리더를 거쳐야 하기 때문에, 리더에 연결할 수 없다면 쓰기를 처리할 수 없다.
리더 노드를 더 늘리는 것으로 확장할 수 있고, 이를 다중 리더방식 이라 한다.
각 리더는 리더인 동시에 다른 리더의 팔로워 역할을 한다.

다중 리더 복제는 장점도 있지만 동일한 데이터를 다른 리더에서 동시에 변경할수 있다는 단점도 하나 있다.
이를 쓰기 충돌이라 하고, 이는 반드시 해소해야 한다.

다중 리더 복제 토폴로지

여러 리더들은 어떻게 다른 노드에 쓰기 요청을 전달할까?

기본적으로 전체 연결 토폴로지를 사용한다.
모든 리더가 각자의 쓰기를 다른 모든 리더에게 전송한다.
이보다 제한된 원형 토폴로지, 별모양 토폴로지도 있다.

원형과 별 모양 토폴로지에서 쓰기가 모든 복제 서버에 도달하려면 여러 노드를 거쳐야 한다.
노드들은 다른 노드로부터 받은 변경 사항을 전달해야 한다.
이 토폴로지의 문제점은 하나의 노드에 장애가 발생하면 다른 노드간 메시지 흐름에 방해를 준다는 것이다.

전체 연결 토폴로지에서는 일부 복제 메시지가 다른 메시지를 추월할수 있는 문제가 있다.
이런 이벤트를 정렬하기 위해 버전 벡터라는 기법을 사용할 수 있다.

많은 다중 리더 시스템에서 충돌 감지 기법은 제대로 구현되어 있지 않다. 
따라서 이런 문제를 인지하고 확인하는 편이 좋다.

쓰기 충돌 다루기

충돌 감지와 해소는 어떻게 할수 있을까?

동기 대 비동기 충돌 감지

이론적으로 충돌 감지를 동기식으로 할수 있다. 
사용자가 쓰기 요청을 하면, 모든 복제 서버가 복제하기를 기다린 후 응답한다.
하지만 이 방식은 다중 리더 복제의 주요 장점을 잃어버린다.

비동기식으로 한다면, 
두 쓰기 요청은 모두 성공하며 이후 특정 시점에 감지할수 있다.
이때 사용자에게 충돌 해소를 요청하면 너무 늦을 수 있다.

충돌 회피

가장 쉬운 충돌 처리 방법은 충돌을 피하는 것이다.
특정 레코드의 쓰기는 특정 리더를 거치도록 한다면 충돌은 발생하지 않는다.
많은 다중 리더 복제 구현체가 사용하는 방법이다.

일관된 상태 수렴

다중 리더는 쓰기 순서가 정해지지 않아 최종 값이 무엇인지 명확하지 않다.
각 복제 서버가 쓰기를 순서대로 적용한다면 결국 일관성 없는 상태가 될것이다.
따라서 데이터베이스는 수렴 방식으로 충돌을 해소해야 한다.
수렴 충돌 해소 방법은 다양하다

- 고유 ID (ex. 타임스탬프) 를 사용해 가장 높은 ID의 쓰기를 고르고 나머지는 버린다
- 어떤 방식으로든 값을 병합한다
- 충돌을 기록해 모든 정보를 보존하여 추후 처리한다.

 

 

리더 없는 복제

일부 저장소는 리더 개념을 버리고 모든 복제 서버가 쓰기를 직접 받을수 있게 허용한다.
아마존 Dynamo 시스템에서 사용한 후 유행했다. (DynamoDB 와는 다르다)
이런 종류의 데이터베이스를 다이나모 스타일이라 한다.

일부 노드가 다운됐을때 쓰기

세개의 복제 서버가 있고 하나를 사용할수 없다면, 리더 기반에서는 쓰기 처리를 하려면 장애 복구를 해야한다.
반면 리더 없는 설정에서는 장애 복구가 필요하지 않다.

클라이언트 쓰기는 모든 복제 서버에 병렬로 전송된다.
두 복제 서버는 쓰기를 처리하고, 사용 불가한 노드는 쓰기를 놓쳤다.
세개의 복제 서버 중 두개의 복제 서버의 쓰기를 확인하면 충분하다고 가정하면, 이 쓰기는 성공한 것으로 간주한다.
이후 읽을때, 사용 불가한 노드에서 이전 데이터를 얻을수 있다.
이는, 읽기 요청도 병렬로 보내고 응답에서 버전 숫자를 사용해 최신값을 얻어온다.

읽기 쓰기 정족수

위에서 n개 복제 서버가 있을때, 쓰기 요청이 w개의 노드에서 쓰기가 성공하면 쓰기가 성공했다고 가정했고, 읽기 요청은 r 개의 노드에게 보냈다.
이런 r,w 를 따르는 읽기 쓰기를 정족수 읽기, 정족수 쓰기라 부른다.
그렇다면 이 수치는 어느정도로 해야할까?

일반적으로 n 을 홀수로 하고, w = r = (n+1) / 2 로 설정한다.

 

동시 쓰기 감지

다이나모 스타일은 여러 클라이언트가 같은 키에 동시에 쓰는 것을 허용하기 때문에 엄격한 정족수를 사용하더라도 충돌이 발생한다.
다양한 네트워크 지연과 부분적 장애로 이벤트는 다른 노드에 다른 순서로 도착할 수 있다.

노드 1은 A로부터 쓰기를 받지만, 일시적인 장애로 B 쓰기를 받지 못한다
노드 2는 A쓰기, B쓰기 순으로 받는다.
노드 3은 B쓰기, A쓰기 순으로 받는다

최종적 일관성을 달성하려면 복제본들은 동일한 값이 돼야 한다.
대부분 데이터베이스는 이를 자동으로 처리해주지 못하므로 데이터 손실을 피하려면 내부에서 충돌을 어떻게 다루는지 알아야 한다.

최종 쓰기 승리

가장 최신값으로 덮어쓰는 방법이다.
어떤 쓰기가 "최신" 인지 명확하게 알수 있다면 모든 복제본은 최종적으로 동일한 값으로 수렴한다.
"최신" 이라는 의미는 단어와는 다르다.

각 노드는 어떤 이벤트가 먼저 발생했는지 확실하게 알수 없다. 
따라서 자연적인 순서 대신 임의로 순서를 정한다. 
예를 들어 쓰기에 타임스탬프를 붙여 이전 타임스탬프 쓰기는 무시한다.
이를 최종 쓰기 승리 (LWW) 라 한다.

LWW는 최종적 수렴 목표를 달성하지만 지속성을 희생한다.
동일한 키에 여러번 쓰기가 있을때 모두 성공으로 반환되더라도, 쓰기 중 하나만 남고 다른건 무시된다.

값 병합

데이터를 자동으로 삭제하는 대신, 클라이언트가 추가 작업을 수행하게 한다.
클라이언트는 동시에 쓴 값을 합쳐 정리해야 한다.
시스템은 병합할때 데이터베이스에서 단순히 삭제하는 것이 아닌 상품을 제거헀음을 나타내기 위한 표시를 남겨둔다.

버전 벡터

각 복제본은 자체 버전 번호를 사용한다.
쓰기를 처리할때 자체 버전 번호를 증가시키고 다른 복제본의 버전 번호도 추적한다.
모든 복제본의 버전 번호 모음을 버전 벡터라 부르고, 이를 활용해 동시 쓰기를 처리한다.