본문 바로가기

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

[9] 일관성과 합의

일관성과 합의: 분산 시스템에서 결함이 나더라도 올바르게 동작하게 할 방법들

내결함성을 지닌 시스템을 구축하는 좋은 방법: 문제를 추상화해라!

  1. 트랜잭션 추상화: 트랜잭션이라는 개념으로 인해 원자성, 격리성, 지속성을 보장한다.
  2. 합의: 모든 노드가 어떤 것에 동의하게 만들어, 리더가 두 개가 되는 스플릿 브레인 같은 문제를 해결할 수 있다.

(후반부에서 합의 알고리즘을 알려줄게)

일관성 보장

최종적 일관성

  • 동시에 DB 노드 두 대를 본다면 두 노드에서 서로 다른 데이터를 볼 가능성이 있다.
  • 근데 시간이 지나면 대부분 최신데이터가 반영이 돼 스스로 같은 값을 반환하게 해소된다.
  • "기다리면 최종적으로 값이 일치하는 일관성을 보장한다" 라는 의미

→ 근데 이건 언제까지 기다려야할지 아무도 모르기 때문에 약한 보장이다

더욱 강한 일관성 모델

  • 선형성
  • 이벤트 순서화 문제
  • 분산 트랜잭션과 합의

선형성(linearizability)

= 원자성 = 강한 = 즉각 = 외부 일관성

복제본이 하나만 있다는 환상 만들어주기. 쓰기 완료하자마자 모든 클라들은 방금 쓰여진 값을 볼 수 있어야 한다.

그러므로 선형성은 최신성 보장(recency guarantee)이다

선형성을 부여하면 어떻게 되는 것인가?

AS-IS

읽기와 쓰기 동시에 일어날 때 읽기는 0 or 1을 왔다갔다하면서 반환할 수 있음

TO-BE

A 가 최신값 1을 반환했으면 그 이후에는 쭉 최신값을 반환해야함. 클라 B 가 복제가 완료되지 않더라도.

CAS 포함 버전

CAS(Compare And Set) 연산으로 동시에 값을 바꾸진 않았는지 확인하고 바꿈.

CAS

(elsboo) 근데 마지막 클라 B는 왜 2를 읽는거지? 클라A는 4 최신값 잘만 읽는데 - 선형성 깨지는 이유 설명하기 위해 억지로 넣은 듯

※ 선형성 vs. 직렬성

모두 순차적인 순서로 배열될 수 있는 의미. 차이는

  • 선형성: 최신성을 의미. 한 번 최신값을 읽으면 그 이후에 어떤 클라가 읽기 요청 처리하든 이전값을 반환하면 안 된다.
  • 직렬성: 트랜잭션이 어떤 순서에 따라 실행되는 것처럼 동작하도록 보장해줌. 실제는 안 그러더라도.
  • 직렬성 스냅숏 격리는 선형적이지 않다. 이전에 쓰인 스냅숏을 읽으므로 최신성을 보장하지 않기 때문이다.

선형성 보장이 유용한 경우

잠금으로 리더 선출할 때

단일 리더 복제 시스템에선 리더가 반드시 하나이도록 보장해야 한다.

잠금을 획득하는 노드를 리더로 선출하는 방법이 있는데 이 때 잠금 획득 로직은 선형성이 보장돼야한다.

ex. 주키퍼같은 코디네이터

제약 조건이나 유일성 보장해야할 때

동시에 유니크 키가 생성되려고 한다거나 동시에 출금해서 은행 계좌가 음수가 되면 안 될 때

선형성을 구현가능한 시스템

단일 리더 복제

  • 선형적 가능
  • 동기식 복제는 가능, 비동기식 복제는 위반할 수 도 있음

합의 알고리즘

  • 선형적
  • ex. 주키퍼

다중 리더 복제

  • 비선형적
  • 여러 노드에서 동시에 쓰고 비동기로 복제하기 때문

리더 없는 복제

  • 아마도 비선형적
  • 정족수 설정을 잘 해도 비선형적(최신성) 깨질 수 있음

CAP 정리

일관성(Consistency), 가용성(Availability), 분단 내성(Partition tolerance) 세 개 중 두개는 고르라는 것으로 표현되나,

네트워크 분단(P) 는 언제든 일어날 수 있으므로 선택할 수 있는게 아니기 때문에

네트워크 분단이 생겼을 때 가용성과 일관성 중 하나를 선택하라는 의미로 보는게 좋다.

근데 CAP 가 많은 오해와 혼란이 있어 피하는 게 좋다.

 

선형성을 요구하면 연결 끊긴 동안은 처리할 수 없으므로 가용성이 없다.

선형성을 요구 안 하면 독립적으로 쓰기 처리 할 수 있으므로 가용성은 있지만 선형성은 없다.

(elsboo) 책에선 일관성(Consistency)과 선형성을 같은 의미로 쓴건가?  ㅇㅇ

 

※ 또 하나의 선형성을 버리는 이유, 성능

선형성을 보장하려면 느리므로 CPU 코어나 분산DB는 성능 향상을 위해 선형성을 보장하지 않는다.

순서화

순서화와 선형성에는 깊은 관련이 있다.

  • 선형성 레지스터는 데이터 복사본이 하나만 있는것처럼 동작하고 연산들이 순서대로 실행된다 것을 뜻한다.

순서화와 인과성에는 깊은 관련이 있다.

  • 질문과 답 사이에는 인과적 의존성(causal dependency) 이 있다고 말한다.
  • insert 가 오기 전에 update 가 오면 안되므로 인과성이 존재한다.

인과성은 이벤트에 순서를 부여한다. 시스템이 인과성에 의한 순서를 지키면 그 시스템은 인과적으로 일관적이라고 한다.(causally consistent)

그러면 선형성과 인과성의 차이?

① 선형성은 전체 순서, 인과성은 일부 순서

: 선형성 시스템에선 모든 연산의 순서를 정할 수 있지만,

인과성은 두 이벤트만의 순서만 정할 수 있고 다른 경우의 순서는 알 수 없다.

② 선형성 > 인과성

선형성은 인과성을 내포한다.

선형성이라면 인과성도 올바르게 유지한다.

인과적 일관성을 유지하는 법

: 어떤 이벤트가 먼저 실행됐는지 알아내는 법

① 동시 쓰기 감지 (p.186)

② 직렬성 스냅숏 격리 (p.260)

  • 트랜잭션이 커밋하려할 때 읽은 데이터 버전이 여전히 최신인지 확인

③ 일련번호나 타임스탬프 제공

  • 전체 순서 제공

④ (단일 리더 복제) 복제 로그로 일관성에 일관적이도록 전체 순서 정의

⑤ (다중 리더나 리더 없는 DB) 에서의 일련번호 생성기

  • 각 노드가 자신만의 독립적인 일련번호 생성. → 초당 연산 수가 노드마다 다르면 노드까리 순서가 뒤바뀔 수 있다.
  • 각 연산에 일 기준 시계에서 얻은 타임스탬프를 붙인다. → 물리적인 시계가 서로 미세하게 다르면(시계 스큐) 인과성이 깨질 수 있다.
  • 노드 당 일련번호 블록(ex.1~1000) 을 미리 할당한다. → 1001~2000 블록 할당 받았는데 그 후 나중에 실행되는 애가 1~1000 을 할당받는 문제가 생길 수 있다

→ 해결: 램포트 타임스탬프

램포트 타임스탬프

  • (카운터, 노드ID)
  • 카운터가 큰 것이 타임스탬프가 크다. 카운터 값이 같으면 노드ID 가 큰 것이 타임스탬프가 크다.
  • 일관적인 연산의 전체 순서를 정의할 수 있다.
  • 카운터는 모든 노드가 같이 쓴다. 각 노드당 각 카운트 가지고 있는게 아님
  • 노드들 간의 카운터 값은 동기화된다. 클라 A 가 노드2로부터 카운터 값 5 받으면 노드1한테 카운트 값 5 보낸다.
  • 두 연산이 동시적인지 인과적으로 의존선이 있는지는 알 수 없다.

→ 이런 타임스탬프 순서화로는 충분하지 않은 경우도 있다. 해결: 브로드캐스트

브로드캐스트

(=전체 순서 브로드캐스트(total order broadcast) , 원자적 브로드케스트 (atomic broadcast)

동시에 같은 ID 로 계정 생성을 했는데 누가 먼저 했는지는 타임스탬프로 판별 가능하다.

근데 이 경우는 이벤트가 일어난 후에 순서를 결정하는 거고 계정 생성 전에 당장 순서를 결정해야할때는 판별이 어렵다.

그래서 브로드캐스트로 메시지를 전달하면 메시지가 전달되는 시점에 그 순서가 고정돼서 순서를 당장 결정할 수 있다.

분산 트랜잭션 과 합의

합의(모든 노드가 동의하는 거)가 중요한 경우

1. 리더 선출

특히 장애 상황일 때 어떤 노드가 리더인지 합의를 통해 선출한다. 스플릿 브레인을 막을 수 있다.

2. 원자적 커밋 (트랜잭션 ACID 중 A)

모든 노드가 동의해야 트랜잭션을 커밋하거나 롤백할 수 있다. 어떤 노드는 성공하고 어떤 노드는 실패하면 안 된다.

2단계 커밋 (2PC, 2 Phase Commit)

2PC 등장 배경

DB 트랜잭션에서 커밋이 되는 시점은 디스크가 커밋 레코드 쓰기를 마치는 시점이다.

이 트랜잭션에 여러 노드가 관여하면 각 노드에서 독립적으로 커밋하는건 충분하지 않다! 일관성이 없어지니깐.

그래서 이런 분산 트랜잭션 환경에서도 원자성을 유지할 수 있도록 2PC 가 나왔다.

하는 방법

2PC 성공

1단계 - 확인) 코디네이터가 각 노드(참여자)들한테 커밋할 수 있는지 확인 요청을 보낸다.

  • ->1단계 끝나면 각 트랜잭션은 코디네이터 요청 기다리는 의심스러운(in doubt) 상태가 된다. 이 때 트랜잭션이 잠금을 유지하기 때문에 코디네이터가 죽으면 잠금 해제 위해 수동으로 커밋/어보트 해줘야 한다.

2단계 - commit or abort) 모든 참여자들한테 "예" 응답을 받으면 코디네이터는 실제 커밋하라는 요청을 보내고 각 노드는 커밋을 진행한다.

하나라도 "아뇨" 응답을 받으면 어보트 요청을 보낸다.

근데 코디네이터가 죽으면 문제임

코디네이터 죽음

코디네이터가 보내는 커밋이나 어보트 요청이 실패하면 코디네이터가 무한히 재시도하지만,

코디네이터가 죽어버리면 참여자들은 꼼짝 없이 복구될 때까지 기다려야 한다.

※ 3PC 커밋

네트워크와 응답 시간에 제한이 있는 3PC 커밋도 있지만 타임아웃으론 신뢰성을 보장하지 못한다. 노드가 살아있어도 네트워크 지연으로 타임아웃 날 수도 있는거니깐

현실에서의 분산 트랜잭션

분산 트랜잭션은 매우 느리다. 2PC 커밋에서 디스크 강제 쓰기(장애복구 위한)랑 네트워크 왕복 시간 때문이다.

그래서 여러 클라우드 서비스들은 분산 트랜잭션을 안 쓰지만,

DB 내부 트랜잭션은 호환될 필요가 없어 잘 작동하기 때문에 볼트DB 나 MySQL 클러스터 NDB저장소 엔진에서는 쓰이기도 한다.

혹은 이종 분산 트랜잭션 을 하기도 한다.

메시지 전달 브로커가 메시지 전달하면 DB 에 쓰기 까지 한 트랜잭션으로 처리해서 DB 쓰기까지 성공하면 커밋, 안되면 메시지 브로커에 다시 쌓이는 어보트할 수 있다.

→ 정확히 한 번(exatctly once) 처리되도록 보장 가능

 

레퍼런스

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

글 읽어주셔서 언제나 감사합니다. 좋은 피드백, 개선 피드백 너무나도 환영합니다