본문 바로가기

SearchDeveloper/데이터 중심 애플리케이션 설계

[5] 복제를 구성하는 방법과 동기화를 위한 노력

복제는 여러 서버에 데이터의 복사본을 유지한다는 뜻

복제가 필요한 이유?

  • 사용자 근처의 서버에 접근해 지연 시간을 줄일 수 있다
  • 장애가 발생해도 복제본으로 대체할 수 있으므로 가용성을 높인다.
  • 읽기 처리량을 높일 수 있다.

복제가 어려운 이유: 데이터의 변경이 일어나기 때문. 이 변경을 어떻게 복제본에서 처리하는지를 이 장에서 알려준다.

(elsboo): 특수 상황에 대해 하나씩 접근하여 처리하는 방법을 제공하는 듯

복제에서 변경을 처리하는 방법 3가지: 단일 리더(single leader) ,다중 리더(multi leader), 리더 없는(leaderless)

리더와 팔로워

리더 기반 복제(= 마스터 슬레이브 복제, 능동 수동 복제)

리더에게만 쓰기 요청을 할 수 있다. 리더가 클라로부터 쓰기 요청을 받으면 자기 데이터 변경 후 변경 내용이 담긴 복제로그(변경스트림)을 팔로워들한테 전송한다. 팔로워들은 그 로그를 보고 자기 데이터도 수정한다.

리더(=마스터, 프라이머리) 는 쓰기 읽기 요청 모두 커버하지만 팔로워(read replica, slave, secondary, hot standby) 는 읽기만 허용된다.

리더 기반 복제

MySQL, MongoDb 에서 사용하고 있다.

동기식 vs. 비동기식 복제

동기식 비동기식 복제

클라가 리더에게 변경 요청해서 리더는 자기 서버에 변경하고 팔로워에게도 변경하라고 보낸 상황

동기식 복제 - 팔로워가 변경 끝나고 ACK 보낼때까지 리더가 기다림. ACK 받으면 클라한테 OK 응답 보냄

  • 장점: 팔로워와 리더가 최신 데이터 가지고 있는거 보장하므로 리더가 작동 안해도 팔로워가 대체할 수 있음을 확신한다.
  • 단점: 팔로워가 응답하지 않으면 리더도 모든 쓰기 차단하고 작업을 처리할 수 없다. (비현실적)

비동기식 복제 - 팔로워가 변경 하든말든 안 기다리고 서버는 클라에게 OK 응답 보냄

  • 장점: 모든 팔로워가 잘못되도 리더는 쓰기 처리를 계속할 수 있다.
  • 단점: 리더가 잘못되면 아직 팔로워가 변경하지 못한 부분은 유실된다.

동기식이 너무 비현실적이니까 반동기식(semi-synchronous)이라고 팔로워 중 하나만 동기로 구성하고 나머지 팔로워는 비동기로 구성하는 방식이 있는데 보통 리더 기반 복제는 완전히 비동기식으로 구성한다.

ElasticSearch: 색인 요청에 대한건 동기식 이지만,
primary -> replica 복제 요청은 비동기식 병럴이다

Replication is sync by default, in other words: the primary waits for
indexing to happen on the replica before it returns to the user. That
said, lots of these processes happen at the same time, so sending the
document to the replica is asynchronous. It doesn't send a change then
wait for the response before sending the next one. This all happens in
parallel.

새 팔로워가 추가됐을 때 리더의 데이터를 정확히 복제하는 법

한 노드에서 다른 노드로 복제할 때 데이터 파일만 주고받는건 충분하지 않다. DB 에 기록되는 데이터는 유동적이기 때문에 복사 결과가 유효하지 않을 수 있다. 그래서 스냅숏 기능을 활용한다.

  1. 새 팔로워가 추가됐다.
  2. 새 팔로워는 리더 스냅숏을 일정 시점에 가져와 복사한다.
  3. 스냅샷 이후에 변경된 내용을 리더에게 요청해 따라잡는다.
  4. 이후 리더에게 발생한 변경 스트림을 처리한다.

노드가 죽었을 때 전체 시스템을 유지하는 법 (고가용성 달성하기)

팔로워가 죽었을 때

팔로워는 수신한 변경 로그를 로컬디스크에 보관한다. 팔로워가 죽다 살아나면 아래와 같이 장애 복구를 한다.

1.로그에서 결함 발생 전까지 처리한 마지막 트랜잭션 알아내기

2.리더한테 연결 끊어진 동안 발생한 데이터 변경 모두 요청

3.변경 사항 따라잡고 이전처럼 변경 스트림을 계속 받는다.

(elsboo) ElasticSearch 는 replica 가 복구될 때 변경된 데이터 요청을 primary shard 에게 한다.

리더가 죽었을 때

1.리더가 장애인지 판단

고장, 정전, 네트워크 문제 등 원인은 많지만 무엇이 잘못됐는지 확실히 알 수 없기 때문에 타임아웃으로 판단한다. 노드들이 메시지 주고받다가 일정시간 동안 응답하지 않으면 죽은 것으로 간주하고 장애라 판단한다.#

2.새로운 리더 선택하기

선출과정(나머지 복제 서버 투포를 통해 선택) 으로 선택되거나 제어노드(controller node) 가 콕 찝는다. 가장 적합한 후보는 최신 데이터 변경사항을 가진 복제 서버다.

3. 새 리더 사용을 위해 시스템 재설정하기

클라는 쓰기 요청을 새 리더에게 하도록 설정한다. 후에 이전 리더가 돌아오더라도 너는 리더 이제 아니고 팔로워라고 알려줄 수 있어야 한다.

장애 복구는 잘못될 수 있는 것 투성이!

비동기복제에서 새 리더가 이전 리더에게 써졌던 일부를 수신 못할 수도 있다.

일반적인 해결책은 이전 리더가 다시 돌아왔을 때 그 복제되지 않은 데이터는 폐기 하는 것이다. 내구성이 떨어지긴 한다.

쓰기를 폐기 하는건 저장소가 두 개라면 특히 위험

깃허브에서 유효하지 않은 (out-of-date) 팔로워가 승격된 사례가 있다. 새 리더가 뒤쳐져있었기 때문에 DB 기본키를 자동증가기법을 사용하는 상황에서 예전에 할단된 기본키를 재사용했고 레디스에 저장된 데이터와 불일치가 일어나 일부 사용자에게 잘못된 데이터를 사용했다.

두 노드가 자신이 리더 노드라고 믿을 수도 있다.

split brain 이라고 하는데 두 리더가 쓰기를 받으면서 충돌을 해소하지 않으면 데이터가 유실되거나 오염된다. 안전장치로 리더가 2개 감지되면 하나를 종료하는 메커니즘이 있지만 주의 깊게 설계 안하면 둘 다 죽을 수도 있다.

타임아웃값 설정이 애매하다.

너무 길면 장애 복구에 오랜 시간이 걸리게 되는 거고 너무 짧으면 높은 부하나 네트워크 문제인 상황에서 불필요한 방애복구를 해 상황을 더 악화시킬수도 있다.

복제 로그 구현

리더 기반 복제가 내부적으로 복제 로그를 어떻게 구현하였을까

구문 기반 복제

SQL 쿼리 같은 구문을 직접 팔로워에서 복제해 마치 클라가 요청한것처럼 팔로워에서 실행한다.

NOW() 처럼 서버마다 다른 값을 사용할 위험(고정 값을 반환하게 대체할수는있다)이 있고 쿼리 실행 순서가 정확히 같은 순서로 실행되지 않거나 다른 부수효과를 발생할 수 도 있다.

MySQL 5.1 이전 버전에서 구문 기반 복제가 사용됐다. 지금은 비결정성 구문은 로구 기반 복제로 변경한다.

일반적으로 다른 복제방법을 선호한다.

쓰기 전 로그 배송

LSM 트리나 B트리처럼 장애 복구를 위해 대개 쓰기 전 로그를 작성하고 리더는 팔로워들에게 그 로그를 전송한다.

POSTGRESQL, Oracle 에서 사용중이다.

(elsboo) WAL(Write Ahead Log): 트랜잭션 일어나기 전 미리 로그 기록해 undo, redo 할 수 있다.

근데 WAL 은 저장소와 밀접하게 엮여있어 리더와 팔로워 DB 버전을 다르게 실행할 수 없다. 근데 이러면 버전 업그레이드할 때 순차적으로 못하므로 운영 중단 시간이 필요할 수 있다.

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

복제 로그를 저장소 엔진 내부와 분리하기 위한 대안

DB용 논리적 로그는 로우 단위인 레코드 열이다.

[장점] 로그와 저장소가 분리됐기 때문에 하위 호환성을 더 쉽게 유지할 수 있고 리더나 팔로워에서 다른 버전의 저장소 쓸 수 있다.

트리거 기반 복제

서브셋만 복제하거나 다른 종류의 DB로 복제하거나 충돌 해소 로직이 필요하면 복제 잡업을 앺단에서 해야한다.

트리거는 데이터가 변경되면 자동으로 사용자 정의 코드를 실행할 수 있다.

복제 지연 문제

읽는 건 팔로워에서도 할 수 있기 때문에 팔로워 복제가 지연됐을 때 발생가능한 문제가 있다. (비동기에서)

자신이 쓴 내용 읽기

자신이 쓴 내용 읽기

자신이 쓴 글인데 팔로워에게 복제가 아직 완료 안돼 새로고침했을 때 결과 없음이 뜬다.

쓰기 후 읽기 일관성(read-after-write)이 필요하다 - 사용자가 재로딩했을 때 항상 자신이 제출한 모든 갱신을 볼 수 있음을 보장. 다른 사용자 것은 보장 안 함.

구현 방법

사용자가 수정한 내용 읽을 때는 리더에서 읽도록 한다.

근데 모든 내용을 변경할 수 있으면 리더만 읽을 것이므로 비효율적이다.(읽기 확장이 안됨)

이런 경우는 마지막 갱신 후 1분동안만 리더에서 모든 읽기 수행하고, 리더보다 1분이상 동기화 안된 팔로워는 읽기 금지를 할 수 있다.

다른 디바이스 간이라면 타임스탬프를 알 수 없으므로 중앙 집중식으로 관리해야 한다.

단조 읽기(Monotonic read)

단조 읽기

팔로워1 은 복제됐고 2는 덜된 상태에서 사용자가 새로고침을 2번 했을때, 처음엔 팔로워1 로 요청가서 결과가 있지만 다음엔 2로 요청이 가서 결과 없다고 뜰 수 있다.

단조읽기가 필요하다 - 새 데이터를 읽은 후에는 예전 데이터를 읽지 않는다.

(elsbo): monotonic 은 증가함수란 말도 있는데 계속 증가하므로 되돌아가지 않는다라는 의미에서 용어를 붙인 것 같다

구현방법

한 사용자의 읽기가 항상 동일한 복제 서버에서 수행되게끔한다.

일관된 순서로 읽기

원래는 대화가 이 순서여야하는데

두 데이터의 복제 시간이 꼬여 순서가 엇갈릴 수 있다.

일관된 순서로 읽기

일관된 순서로 읽기(Consistent Prefix Read) 가 필요하다 - 일련의 쓰기가 특정 순서로 발생한다면 읽는 모든 상요자는 같은 순서로 쓰여진 내용을 보게 됨을 보장한다.

구현방법 인과성이 있는 쓰기는 동일한 파티션에 기록한다.

다중 리더 복제 (=마스터 마스터, 액티브/액티브 복제)

※ 단일 리더의 단점: 모든 쓰기는 리더를 거치기 때문에 그 리더에 연결할 수 없으면 쓰기를 할 수 없다.

다중 리더

쓰기 역할 노드를 하나 이상 둔다. 리더는 다른 리더의 팔로워 역할도 한다.

근데 다중 리더로 인한 복잡도가 너무 크기 때문에 특수 상황 2가지에서만 합리적이다.

상황1: 다중 데이터 센터 운영

다중 리더

여러 다른 지역의 데이터센터에 DB 복제 서버가 있는 경우

※ 충돌 해소: 동일 데이터를 다른 데이터센터에서 동시에 변경하는 경우 충돌 해소 필요

이 경우에 다중 리더가 단일 리더 구성보다 좋은 이유?

성능 관점

  • 단일 리더로 하면 모든 쓰기 데이터가 인터넷을 통해 리더가 있는 데이터 센터로 이동해야 하고, 이는 쓰기 지연을 발생시킨다.
  • 다중 리더로 하면 쓰기는 로컬 DC(데이터센터에서)하고 다른 DC 간 복제는 비동기로 하면 되므로 네트워크 지연을 사용자가 인지하는 성능은 단일 리더보다 좋아진다.

중단 내성 관점

  • 단일 리더로 하면 리더 장애났을 때 다른 데이터센터의 팔로워를 리더로 승진시킨다.
  • 다중 리더로 하면 데이터센터마다 독립적으로 동작하고 복구됐을 때 다른 DC 와 복제를 동기화한다.

네트워크 문제 내성 관점

  • 단일 리더로 하면 쓰기는 동기로 하기 때문에 네트워크 연결 문제에 민감하다.
    • (elsboo) p.157에서 일반적으로 비동기로 한다면서 여기는 동기로 한다는 이유? 단일도 비동기로 하면 안 되나
  • 다중 리더로 하면 비동기로 dc 간 복제를 하기 때문에 일시적으로 네트워크 연결이 안되도 쓰기는 로컬 dc 에서 계속 할 수 있다.

상황2: 오프라인으로 작업하는 클라이언트

: 오프라인일 때 에버노트에 노트 작성하는 경우

오프라인이어도 데이터는 볼 수 있어야하고 혹여 수정해도 다시 온라인일땐 동기화가 돼야함

각 디바이스에 리더처럼 동작하는 로컬 DB(쓰기 요청 가능한) 가 있다. 디바이스 간 복제는 데이터센터 구성과 비슷하게 비동기로 동기화한다.

(elsboo): 에버노트는 핸드폰, 노트북, 데스크탑마다 로컬 디비가 있어서 여기에 쓰니 오프라인이어도 쓰기는 가능하구나. 온라인일때 동기화를 하구나

협업 편집

위키 컨플루언스나 구글 독스 같은 협업 편집 도구도 상황2와 비슷하다

사용자가 문서 편집할 때

① 변경 내용은 즉시 로컬 복제 서버에 적용하고

② 동일한 문서 편집하는 다른 사용자와 서버에 비동기 방식으로 복제한다.

편집 충돌이 없음을 보장하려면

모델1 편집 전 문서 잠금을 얻는다. 다른 사용자가 같은 문서 편집하려면 먼저 편집중인 사용자 변경이 커밋되고 잠금 해제될 때 까지 기다려야 한다. (단일 리더 복제와 동일)

모델2 빠른 협업을 위해 잠금 방식을 사용하지 않고 변경 단위를 가장 작게 해서 동시에 편집할 수 있다. 근데 충돌 해소 필요한 경우 포함해 리더 복제와 같은 모든 문제를 야기한다.

▶ 다중 리더 복제는 자동 증가 키, 트리거, 무결성 제약 등 문제될 소지가 많기 때문에 가능하면 피해야 하는 위험한 영역으로 간주된다.

다중 리더의 가장 큰 문제: 쓰기 충돌 해소

user1 는 A -> B 로 변경 요청했는데 다른 리더2에 의해 이미 C 로 바꿔버려 충돌

user2 는 A -> C 로 변경 요청했는데 다른 리더1에 의해 이미 B 로 바꿔버려 충돌

쓰기 충돌

단일 리더라면 두번째쓰기는 첫번째완료되기 까지 기다려서 순차 처리 날거라 충동해소가 날 일이 없다.

다중 리더에서는 다른 리더 상황은 모르니 둘 다 성공하며 충돌은 비동기 복제할 때 특정 시점에서 감지 한다.

충돌 해소 방법 3가지

① 충돌을 아예 회피

: 특정 레코드의 모든 쓰기 요청을 동일한 리더로 보냄. 그럼 다른 리더에서 수정될 일이 없으니 충돌은 일어나지 않을 것!

자주 권장되는 방법이다.

근데 데이터센터가 변경됐을 때(고장이나 사용자 지역 이동으로 인해), 즉 리더가 변경됐을 때 특정 레코드 변경을 담당할 리더를 변경하는것도 고려돼야한다.

② 어디에서 접근하든 같은 값을 반환하도록 일관된 상태 수렴

: 모든 복제 서버가 최종적으로 동일하다는 사실을 보장해야함 => 동시에 변경된 값들 중 하나만 고를 수 있어야함

하나만 고르는 방법

  • 각 쓰기에 고유 ID(타임스탬프, 해시, UUID 등)을 부여해 가장 높은 값 부여된 쓰기 채택 - 데이터 유실 위험 있음
  • 각 서버에 고정적으로 우선순위 부여 - 데이터 유실 위험 있음
  • 어떻게든 두 값을 병합 (ex. B/C)
  • 모든 충돌을 기록해 사용자가 정의한 충돌 해소 코드 실행

(elsboo) ES 는 샤드마다 리플리카를 가지고 있으므로 다중 리더 인가? 아님 단일 리더인가?아 샤드 당 리더는 하나니까 단일 리더 같다

(elsboo) 프라이머리, 리플리카 샤드 중 쓰기 요청을 처리할 수 있는 샤드는 프라이머리 뿐인가? chatGPT 는 프라이머리만 하는게 맞대

리더 없는 복제

: 모든 복제 서버가 클라이언트에서 쓰기 요청 직접 받을 수 있게 허용

  • 이 개념은 관계형 DB 가 우세한 시대에선 대부분 잊혀졌었는데 아마존에 내부 다이나모 시스템에서 하용하면서 DB 용 아키텍처로 유행함 (다이나모 스타일)

(이거 AWS dynamo DB 말하는거아님! dynamo DB 는 단일 리더 복제임)

  • 클라가 직접 복제 서버에게 쓰기 요청 날리거나, 중간에 코디네이터 노드를 둬서 클라는 코디한테만 보내고 클라가 모든 복제 서버에게 날리는 방법도 있다.

노드 다운됐을 때?

단순 무시한다. 3개 중 2개 서버 쓰기 완료하면 쓰기 성공이라고 판단하는 매커니즘이고 실패한 노드는 단순 무시하고 나중에 최신 데이터로 세팅한다.

리더 없는 복제 노드 다운

그래서 읽을 때도 읽기 요청을 모든 복제 서버에게 병렬로 요청하고 버저닝을 활용해 가장 최신 버전의 데이터를 받고 리플리카 3은 최신데이터로 값 다시 변경한다.

그럼 읽기 복구는 어떻게?

  • 읽기 시점에 복구: 위에처럼 읽기 요청 들어왔을 때 비교해서 오래된 값이면 최신 값으로 동기화
  • 안티 엔트로피 처리: 백그라운드 프로세스에서 데이터 버전 차이나는거 지속적으로 찾아 동기화

쓰기/읽기에서 몇 대 이상 응답해야 쓰기/읽기 성공했다고 판단할겨?

(일반화) n개의 복제 서버가 있을 때, 모든 쓰기는 w (write) 개 노드에서 성공해야 쓰기 확정되고, 모든 읽기는 최소한 r (read) 개 노드에 질의해야 한다.

r 과 w 를 따르는 읽기와 쓰기를 정족수(quorum) 읽기와 쓰기라 부른다.

(elsboo) 정족수 = fixed number. w, r 이 만족해야하는 최수 노드 수이고 이 수를 정족수라고 한다.

일반적인 기준

n 을 홀수로 두고 w = r = (n+1)/2 로 설정한다.

w + r > n 이면 다음과 같이 사용 불가능한 노드가 있어도 괜찮다고 판단한다(용인한다).

  • w < n 이면 노드 하나가 없어도 쓰기 처리 가능
  • r < n 이면 노드 하나가 없어도 읽기 처리 가능

정족수 반환 못 하면 에러 반환한다.

근데 w + r > n 만족해도 오래된 값 반환하는 에지 케이스가 있다.

(elsboo) 에지 케이스: 정상적인 조건이 아닐 떄 일어날 수 있는 시나리오

  • 느슨한 정족수면 r 개 노드와 w 개 노드가 겹치는 걸 보장하지 않는다. (elsboo) 그럼 쓰기는 다른 노드에서 하고 읽기는 다른 애꿎은 노드에서 해서 오래된 값 읽을 수도 있을 듯
  • 두개의 쓰기가 동시에 발생하면 어떤 쓰기가 먼저 일어났는지 분명하지 않다.
  • 쓰기와 읽기가 동시에 발생하면 둘 중 뭐가 먼저 처리될 지 확신할 수 없으므로 오래된 값을 읽을 수도 있다.
  • 쓰기 성공한 수가 정족수 w 보다 작아도 롤백하지 않는다. 쓰기 실패해도 이어지는 읽기에서 해당 쓰기 값이 반환될수도 아닐 수도 있다.
    • (elsboo) 쓰기 실패하면 에러난다며 왜 반환함?
  • 쓰기 처리한 노드가 고장나고 예전 데이터 가진 복제 서버가 복구되면 w 보다 낮아져 정족수 조건이 깨진다.

느슨한 정족수와 암시된 핸드오프

노드가 n 개 이상인 대규모 클러스터에서 n(정족수 구성에 들어가는 노드) 네트워크 장애 상황에 n에 구성되지 않는 노드에 연결될 가능성이 있다.

이 때 DB 설계자는 트레이드오프에 직면한다.

  • w나 r 노드 정족수를 만족하지 않는 모든 요청에 오류를 반환할까?
  • 일단 쓰기를 받아들이고 정족수 구성 노드 n 에 속하지는 않지만 연결할 수 있는 노드에 기록할까? => 이게 느슨한 정족수
    • 후에 n 노드가 장애에서 복구되면 임시로 쓰기 처리했던 노드는 n 노드에게 지금까지 처리한 데이터를 전송한다. = 암시된 핸드오프(hinted handoff, hinted= 넌지시 알림)

(elsboo) 용어가 암시된 핸드오프인 이유: 핸드오프 - 무선 단말이 접속되어 있는 무선 LAN의 AP 혹은 이동통신망의 기지국에서 다른 AP나 기지국으로 접속을 바꾸는 것을 의미한다. 사용자 모르게 장애 복구하면 넌지시 정족수 노드로 바꿔서 그런거같다

레퍼런스

데이터 중심 애플리케이션 설계