책 정리와 리뷰

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

maruoov 2023. 10. 3. 20:33

일관성과 합의

분산시스템은 많은 것들이 잘못될 수 있어서, 내부 구성 요소 중 뭔가에 결함이 있더라도 서비스는 올바르게 동작하게 할 방법을 찾아야 한다
내결함성을 지닌 시스템을 구축하는 가장 좋은 방법은 범용 추상화를 찾아 구현하고 이 보장에 의존하게 하는 것이다. ex) 합의
분산 시스템에서 제공될수 있는 보장과 추상화의 범위를 알아볼 필요가 있다. 어떤 것을 할수 있고 없는지에 대한 범위를 이해해야 한다.

일관성 보장

복제 데이터베이스는 대부분 최소한 최종적 일관성을 제공한다. (불일치는 일시적이며 결국 스스로 해소)
그러나 언제 복제본이 수렴될지에 대해서는 아무것도 보장하지 않는 약한 보장이다.
수렴될 때까지 읽기는 뭔가를 반환할 수도, 아무것도 반환하지 않을 수도 있다.

선형성

복제본이 하나만 있다고 가정하면 훨씬 단순하지 않을까? 이것이 선형성을 뒷받침 하는 아이디어다.
기본 아이디어는 시스템에 데이터 복제본이 하나만 있고 그 데이터를 대상으로 수행하는 모든 연산은 원자적인 것처럼 보이게 만드는 것이다.
선형성 시스템에서는 클라이언트가 쓰기를 성공하자마자 데이터를 읽는 모든 클라이언트는 방금 쓰여진 값을 볼 수 있어야 한다.

시스템에 선형성을 부여하는 것은 무엇인가?

선형성을 잘 이해할 수 있도록 몇가지 예시를 살펴보자

위 그림에서 x 값은 처음에 0이고, 클라이언트 C가 1로 설정하는 쓰기를 실행한다.
쓰기 요청이 실행되는 동안 A 와 B 는 반복적으로 폴링한다. 이때 A와 B는 어떤 응답을 받을까?

  • A의 첫 읽기는 쓰기가 시작하기 전이므로 0을 반환하는게 명백하다
  • A 의 마지막 읽기는 쓰기가 완료된 후 시작되므로 선형적인 시스템이라면 새로운 값을 반환해야 한다
  • 쓰기 연산과 시간이 겹치는 읽기 연산은 0 혹은 1을 반환한다

쓰기와 도잇에 실행되는 읽기가 이전 / 새로운 값을 반환한다면 이는 데이터 단일 복사본 시스템에 기대하는 바가 아니다.
시스템을 선형적으로 만들려면 또 다른 제약 조건을 추가해야 한다.

선형성 시스템에선 x 값이 원자적으로 바뀌는 어떤 시점이 있다고 가정하고, 이 시점 이후에 모든 읽기 또한 새로운 값을 반환해야 한다.
클라이언트 A가 새로운 값을 읽고 나면 그 이후의 모든 다른 클라이언트의 읽기는 쓰기가 진행중이더라도 새로운 값인 1을 반환해야 한다.

선형성 대 직렬성

직렬성 : 트랜잭션들의 격리 속성, 직렬성은 각 트랜잭션들이 어떤 순서에 따라 실행되는 것처럼 동작하도록 보장해준다.

선형성 : 개별 객체에 실행되는 읽기와 쓰기에 대한 최신성 보장

선형성이 유용한 경우

잠금과 리더 선출

단일 리더 복제 시스템은 리더가 하나만 존재하도록 보장해야 한다.
이를 구현하는 한가지 방법은 잠금을 사용하는 것이고, 이는 어떻게 구현하든지 선형적이어야 한다.

제약 조건과 유일성 보장

유일성 보장 조건은 데이터베이스에서 흔하다. 이 제약 조건을 강제하고 싶다면 선형성이 필요하다.

채널 간 타이밍 의존성

시스템간 여러 통신 채널이 있을때, (ex. 파일저장소 + 메시지큐) 선형성 보장이 없으면 두 채널 사이에 경쟁 조건이 발생할 수 있다.

선형성 시스템 구현하기

가장 간단한 해답은 데이터 복사본을 하나만 사용하는 것이다.
하지만 이 방법으로는 결함을 견뎌낼 수 없다.
이전에 다뤘던 복제 방법을 살펴보며 선형적으로 만들수 있을지 비교해본다

  • 단일 리더 복제
    • 리더는 쓰기에 사용되는 데이터의 주 복사본을 갖고 있고 팔로워는 데이터의 백업 복사본을 보관한다. 리더나 동기식으로 갱신된 팔로워에서 실행한 읽니는 선형적이 될 가능성이 있다. 그러나 모든 단일 리더 데이터베이스가 선형적인 것은 아니다
  • 합의 알고리즘
    • 단일 리더 복제를 닮았고, 스플릿 브레인과 복제본이 뒤처지는 문제를 막을 수단이 포함된다. 이런 세부 사항 덕에 선형성 저장소를 구현할 수 있다.
  • 다중 리더 복제
    • 일반적으로 선형적이지 않다.
  • 리더 없는 복제
    • 정족수 읽기와 쓰기로 일관성을 달성할수 있다고 주장하지만, 정족수의 설정에 따라, 엄격한 일관성을 어떻게 정의하느냐에 따라 다를 수 있다.

순서화 보장

선형성 레지스터는 데이터 복사본이 하나만 있는 것처럼 동작하고, 모든 연산이 어느 시점에 원자적으로 효과가 나타나는 것처럼 보인다고 했다.
이는 연산들이 잘 정의된 순서대로 실행 된다는 것을 암시한다.

순서화, 선형성, 합의 사이에는 깊은 연결 관계가 있다.

순서화와 인과성

순서화는 인과성을 보존하는 데 도움을 준다.
인과성은 이벤트에 순서를 부여한다. 이는 무엇이 무엇보다 먼저 일어났는가를 정의한다.
시스템이 인과성에 의해 부과된 순서를 지키면 그 시스템은 인과적으로 일관적 이다

선형성 vs 인과성

선형성 : 연산의 전체 순서를 정할 수 있다.
인과성 : 두 이벤트에 인과적인 관계가 있으면 순서가 있지만, 동시에 실행되면 비교할 수 없다. 전체 순서가 아닌 부분 순서를 정의한다. 어떤 연산들은 서로에 대해 순서를 정할 수 있지만 어떤 연산들은 비교할 수 없다

선형성은 인과성을 포함한다.
어떤 시스템이던지 선형적이라면 인과성도 올바르게 유지한다.
많은 경우에 선형성이 필요한 것처럼 보여도 사실 진짜 필요한 것은 인과적 일관성이다.

인과적 의존성 담기

인과성을 유지하기 위해 어떤 연산이 다른 연산보다 먼저 실행됐는지 알아야 한다.
전체 데이터베이스에 걸친 인과정 의존성을 추적해야 하고, 이를 위해 버전 벡터를 일반화해 사용한다

단일리더 시스템이라면, 일련번호타임스탬프 를 써서 이벤트의 순서를 정할 수 있다.
이런 일련번호나 타임스탬프는 크기가 작고 전체 순서를 제공한다.
특히 인과성에 일관적인 전체 순서대로 일련번호를 생성할 수 있다.

단일리더가 아니라면, 위와 같은 방법이 가능해보이지 않아 다른 여러 방법을 사용한다.

  • 각 노드가 자신만의 독립적인 일련번호 집합을 생성한다
  • 각 연산에 일 기준 시계 타임스탬프를 붙인다 (해상도?)
  • 일련번호 블록을 미리 할당한다

이런 방법들은 잘 동작하지만 생성한 일련번호가 인과성에 일관적이지 않다.

  • 각 노드는 초당 연산수가 다를수 있어 어떤 것이 먼저 실행됐는지 정확히 알수 없다
  • 물리적 시계 타임스탬프는 시계 스큐에 종속적이어서 일관적이지 않게 될수 있다
  • 블록 할당의 경우 인과적으로 나중에 실행되는 연산이 앞의 번호를 받을 수 있다

램포트 타임스탬프

위의 문제점들을 해결하기 위한 간단한 방법이 있다.

각 노드는 고유 식별자를 갖고 처리한 연산 개수를 카운터로 유지한다.
램포트 타임스탬프는 (카운터, 노드ID) 의 쌍이다.
두 노드는 때때로 카운터 값이 같을 수 있지만 노드ID 를 포함시켜 각 타임스탬프는 유일하게 된다.

램포트 타임스탬프를 일관적으로 만들어주는 아이디어는,
모든 노드와 모든 클라이언트가 지금까지 본 카운터 값 중 최대값 을 추적하고 모든 요청에 그 최대값을 포함시킨다.
노드가 자신의 카운터 값보다 큰 최대 카운터를 가진 요청이나 응답을 받으면 자신의 카운터를 그 최대값으로 증가시킨다.

인과성에 일관적인 연산의 전체 순서를 정의하지만, 분산 시스템의 여러 문제를 해결하는데 충분하지는 않다.
문제는 연산의 전체 순서는 모든 연산을 모은 후에야 드러난다는 것이다.
다른 노드가 어떤 연산을 생성했지만 무엇인지 아직 알수 없다면 최종 순서를 만들어낼 수 없다.

유일성 제약 조건 같은 것을 구현하려면 순서가 있는 것으로는 충분치 않고 언제 그 순서가 확정되는지도 알아야 한다.
이를 해결하기 위한 아이디어로 전체 순서 브로드캐스트를 다룬다.

전체 순서 브로드캐스트

단일 리더 복제는 한 노드를 리더로 선택하고 단일 리더 CPU 코어에서 모든 연산을 차례대로 배열함으로써 연산의 전체 순서를 정한다.
처리량이 단일 리더가 처리할수 있는 수준을 넘어설 때 어떻게 확장할 것인가/리더에 장애가 발생했을 때의 장애 복구 처리가 문제다.

전체 순서 브로드캐스트는 두 가지 안전성 속성을 항상 만족해야 한다

  • 신뢰성 있는 전달 : 어떤 메시지도 손실되지 않는다. 메시지가 한 노드에 전달되면 모든 노드에도 전달된다
  • 전체 순서가 정해진 전달 : 메시지는 모든 노드에 같은 순서로 전달된다

노드나 네트워크에 결함이 있더라도 신뢰성과 순서화 속성이 항상 만족되도록 보장해야 한다.
전체 순서 브로드캐스트의 중요한 측면은 메시지가 전달되는 시점에 그 순서가 고정된다는 것이다.
이 사실 때문에 전체 순서 브로드캐스트가 타임스탬프 순서화보다 강하다.

분산 트랜잭션과 합의

합의는 분산 컴퓨팅에서 가장 중요하고 근본적인 문제중 하나다.
합의의 목적은 여러 노드들이 뭔가에 동의하게 만드는 것 이고, 겉으로 보기에 간단해 보인다.
노드가 합의하는 것이 중요한 상황은 여러가지가 있다

  • 리더 선출
  • 원자적 커밋

먼저 원자적 커밋 문제를 살펴본다

원자적 커밋과 2단계 커밋

2단계 커밋은 여러 노드에 걸친 원자적 트랜잭션 커밋을 달성하는 알고리즘이다.
2PC 의 커밋/어보트 과정은 두 단계로 나뉜다

애플리케이션이 커밋할 준비가 되면 코디네이터가 1단계를 시작한다

  • 각 노드에 준비 요청을 보낸다
  • 모든 참여자가 커밋할 준비가 됐다는 뜻으로 'yes' 를 응답하면 코디네이터는 2단계에서 커밋 요청을 보내고 커밋이 실제로 일어난다
  • 참여자 중 누구라도 'no' 로 응답하면 코디네이터는 2단계에서 모든 노드에 어보트 요청을 보낸다.

이 정도로는 왜 여러 노드에서 원자성을 보장하는지 알수 없다. 그 과정을 더 자세히 알아보자

  1. 애플리케이션은 분산 트랜잭션을 시작하기를 원할 때 코디네이터에게 트랜잭션ID를 요청한다. 이 트랜잭션ID는 전역적으로 유일하다
  2. 각 참여자에서 단일 노드 트랜잭션을 시작하고 단일 노드 트랜잭션에 전역적으로 유일한 트랜잭션 ID를 붙인다. 모든 읽기와 쓰기는 이런 단일 노드 트랜잭션 중 하나에서 실행된다.
  3. 커밋할 준비가 되면 코디네이터는 모든 참여자에게 전역 트랜잭션Id로 태깅된 준비 요청을 보낸다. 이런 요청 중 실패하거나 타임아웃된 것이 있으면 코디네이터는 모든 참여자에게 트랜잭션id로 어보트 요청을 보낸다
  4. 참여자가 준비 요청을 받으면 모든 상황에서 분명히 트랜잭션을 커밋할 수 있는지 확인한다. 여기엔 디스크에 쓰는것, 제약 조건 위반, 충돌 확인 등이 포함된다. 코디네이터에게 '네'라고 응답하면 트랜잭션을 오류 없이 커밋할 것으로 약속한다
  5. 코디네이터가 모든 준비 요청에 대해 응답을 받으면 커밋/어보트 결정을 한다. 추후 죽는 경우에 결정을 알수 있도록 디스크에 있는 트랜잭션 로그에 기록한다 (커밋 포인트)
  6. 결정이 디스크에 쓰여지면 모든 참여자에게 커밋/어보트 요청이 전송된다. 이 요청이 실패하거나 타임아웃이 되면 코디네이터는 성공할 때까지 영원히 재시도 해야한다.