////
Search

9장 - 일관성과 합의

Date
2023/01/29 11:02
Tags

1. 일관성 보장

약한보장
복제 데이터 베이스는 대부분 최소한 최종적 일관성을 제공한다.
모든 복제본이 결국 같은 값으로 수렴되기를 기대함으로 최종적 일관성보다 수렴이 더 나은 이름일 수도 있다.
다만 이는 매우 약한 보장이며, 언제 복제본이 수렴될지에 대해서는 이야기가 없다.
강한보장
데이터 시스템이 선택적으로 제공
올바르게 사용하기 쉬우나 시스템 성능이 나쁘거나 내결함성이 약할수도 있다.

2. 선형성

최종적 일관성을 지닌 데이터베이스에서 두 개의 다른 복제본의 같은 질문을 동시에 하면 두 가지 다른 응답을 받을 수도 있기에 혼란스럽다.
위 문제에서 데이터베이스가 복제본이 하나만 있다면 더 단순해지지 않을까라는 생각을 할 수 있으며, 이는 선형성을 뒷받침해주는 아이디어다.
원자적 일관성(atomic consistency)
강한 일관성(strong consistency)
즉각 일관성(immediate consistency)
외부 일관성(external consistency)
기본적인 아이디어는 시스템에 데이터 복사본이 하나만 있고 그 데이터를 대상으로 수행하는 모든 연산은 원자적인 것처럼 보이게 만드는 것
선형성 시스템에서는 클라이언트가 쓰기를 성공적으로 완료하자마자 그 데이터베이스를 읽는 모든 클라이언트는 방금 쓰여진 값을 볼 수 있어야 함
즉, 선형성은 최신성 보장(recency guarantee) 를 의미함

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

선형성을 받침하는 기본 아이디어는 시스템에 데이터 복사본이 하나뿐인 것처럼 보이게 만드는 것이다.
선형성 데이터베이스에서 동시에 같은 키 x를 읽고 쓰는 세 클라이언트를 보여준다.
분산 시스템에 분야에서는 x를 레지스터(register) 라고 부른다.
이 예제에서는 레지스터는 두 가지 종류의 연산이 있다.
read(x) => v 는 클라이언트가 레지스터 x의 읽기를 요청하고 데이터베이스가 값 v를 반환했다는 것을 의미
write(x, v) => r 은 클라이언트가 레지스터 x의 값을 v로 설정하라고 요청했고 데이터베이스가 응답 r을 반환했다는 것을 의미
클라이언트 C가 쓰기를 실행하는 동안 A와 B는 지속적으로 값을 폴딩하는데 어떤 응답을 받을 수 있는가
A가 첫 번째 실행한 요청은 0을 반환하는게 명백하다.
A의 마지막 응답은 명백히 1을 반환받게 된다.
쓰기가 실행 중 읽기 연산은 동시에 실행되므로 0을 받을지 1을 받을지 명확하지 않다.
선형적 읽기를 위해 최근 읽은적 있는 값은 후속 읽기에도 같은 값을 반환한다.

2.2. 선형성에 기대기

시스템이 올바르게 동작하도록 만들기 위한 선형성이 중요한 몇 가지 영역

2.2.1. 잠금과 리더 선출

단일 리더 복제를 사용하는 시스템은 리더가 여러개(스플릿 브레인)가 아니라 진짜 하나만 보장하도록 해야한다.
분산 잠금과 리더 선출을 구현하기 위해 아파치 주키퍼(Apache ZooKeepr)나 etcd 같은 코디네이션 서비스가 종종 사용된다.
이들은 합의 알고리즘을 사용해 선형성 연산을 내결함성이 있느 방식으로 구현한다.
분산 잠금은 오라클 리얼 애플리케이션 클러스터 같은 분산 데이터베이스에서 훨씬 세분화된 수준으로 사용되기도 한다.

2.2.2. 제약 조건과 유일성 보장

유일성 제약 조건은 데이터베이스에서 흔하다.
이러한 조건을 강제하고 싶다면 선형성이 필요하다.
마찬가지로, 은행 계좌 잔고가 음수가 되지 않거나 예약의 경우 모든 노드가 동의하는 하나의 최신 값이 있기를 요구한다.
실제 애플리케이션에서는 종종 이런 제약 조건을 느슨하게 다르기도 하지만, 관계형 데이터베이스에서 전형적으로 볼 수 있는 엄격한 유일성 제약 조건은 선현성이 필요하다.

2.2.3. 채널 간 타이밍 의존성

웹 서버와 이미지 크기 변경 모듈은 파일 저장소와 메시지 큐를 모두 써 통신하므로 경쟁 조건 발생 가능
파일 저장 서비스가 선형적이면 잘 돌아가지만, 아니라면 경쟁 조건의 위험이 언제나 있다.
선형성이 경쟁 조건을 회피하는 유일한 방법은 아니지만 이해하기에 가장 단순하다.

2.3. 선형성 시스템 구현하기

선형성은 근본적으로 데이터 복사본이 하나만 있는 것처럼 동작하고 그 데이터에 실행되는 모든 연산은 원자적 이라는 것을 의미한다.
But 이 방법으로 결함을 견뎌낼 수 없다.
시스템이 내결함성을 지니는 흔한 방법은 복제를 이용하는 것으로 앞서 다웠던 내용들을 정리하고 선형적으로 만들 수 있을지 정리한다.
단일 리더 복제 : 선형적 가능성 존재
읽기에 리더를 사용하기 위해서는 누가 리더인지를 정확하게 안다고 가정해야 한다.
합의 알고리즘 : 선형적
주키퍼 등의 코디네이트를 통해서 가능하다.
다중 리더 복제 : 비선형적
여러 노드에서 동시 쓰기를 처리하고 쓰여진 내용을 비동기적으로 처리하기 때문에 선형적이지 않다.
리더 없는 복제 : 아마도 비선형적
정족수 및 엄격한 일관성에 따라 결정된다.

2.3.1. 선형성과 정족수

엄격한 정족수를 사용한 읽기 쓰기는 선형적인 것처럼 보인다.
But. 네트워크 지연의 변동이 심하면 경쟁 조건이 생길 수 있다.
정족수 조건이 만족됨에도 실행은 선형적이지 않을 수 있다.
성능이 떨어지는 비용을 지불하고 다이타모 스타일 정족수를 선형적으로 만드는 것도 가능하다.
다만 이경우는 선형성 읽기와 쓰기 연산만 수행할 수 있다.
요약하면 다이나모 스타일 복제를 하는 리더 없는 시스템은 선형성을 제공하지 않는다고 보는 게 안전하다.

2.4. 선형성의 비용

네트워크가 끊기면 선형성과 가용성 사이에서 선택해야 한다.

2.4.1. CAP 정리

어떤 선형성 데이터베이라도 해당 문제를 가지고 있고 발생하는 트레이드오프를 정리해보자.
애플리케이션에서 선형성을 요구하고 네트워크 문제 때문에 일부 복제 서버가 다른 복제 서버와 연결이 끊기면 일부 복제 서버는 연결이 끊긴 동안은 요청을 처리할 수 없다.
네트워크 문제가 고쳐질 때까지 기다리거나 오류를 반환해야 한다.
즉, 가용성이 없다.
애플리케이션에서 선형성을 요구하지 않는다면 각 복제 서버가 다른 복제 서버와 연결이 끊기더라도 독립적으로 요청을 처리하는 방식으로 쓰기를 처리할 수 있다.
이 경우 애플리케이션은 네트워크 문제에 직면해도 가용한 상태를 유지하지만 그 동작은 선형적이지 않다.
따라서 선형성이 필요 없는 애플리케이션은 네트워크 문제에 더 강인하다.
이는 CAP 정리로 널리 알려졌다.
CAP는 데이터베이스에서 트레이드오프에 대한 논의를 시작하려는 목적으로 정확한 정의 없이 경험 법칙으로 제안됐다.
공식적으로 정의된 CAP 정리는 매우 범위가 좁으며, 오직 하나의 일관성 모델과 한 종류의 결함만 고려한다.
역사적인 영향력은 있으나 시스템 설계시 실용적인 가치는 없습니다.

2.4.2. 선형성과 네트워크 지연

선형성은 유용한 보장이지만 현실에서는 실제로 선형적인 시스템은 놀랄만큼 드물다.
성능을 위해 선형성이 손실되는 트레이드 오프가 발생하는 경우가 자주 발생한다.
분산 데이터베이스도 마찬가지의 이유다.
즉, 선형성을 원하면 읽기와 쓰기 요청의 응답 시간이 적어도 네트워크 지연의 불확싱성에 비례한다.
지연시간에 민감한 시스템에서는 이러한 트레이드오프가 중요하다.

3. 순서화 보장

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

3.1. 순서화와 인과성

순서화는 인과성을 보존하는데 도움을 준다.

3.1.1. 인과적 순서가 전체 순서는 아니다

전체 순서(total order)은 어떤 두 요소를 비교할 수 있게 하므로 두 요소가 있으면 항상 어떤 것이 더 크고 어떤 것이 더 작은지 말할 수 있다.
But. 수학적 집합은 이와 달리 비교불가(incomparable)하고, 부분적으로 순서가 정해진다(partially ordered).
전체 순서와 부분 순서의 차이점은 다른 데이터베이스 일관성 모델에 반영된다.
선형성 : 선형성 시스템에서는 연산의 전체 순서를 정할 수 있다.
인과성 : 두 연산 중 다른 것보다 먼저 실행되지 않았다면 두 연산이 동시적이라고 말했고, 인과성이 전체 순서가 아닌 부분 순서를 정의한다.
이 정의를 통해 선형성 데이터스토어는 동시적 연산이 없습니다. 하나의 타임라인이 있고 모든 연산은 그 타임라인을 따라서 전체 순서가 정해져야 한다.
동시성은 타임라인이 갈라졌다 다시 합쳐지는 것을 의미한다.
깃의 버전 히스토리는 인과적 의존성 그래프와 매우 유사하다.

3.1.2. 선형성은 인과적 일관성보다 강하다.

선형성은 인과성을 내포한다.
선형성은 인과성을 보장해준다는 사실은 이해하기도 쉽고 매력적으로 보이게 만들어 준다.
But. 네트워크 지연에 따라 성능과 가용성에 해가 될 수 있다.
절충선으로 선형적으로 만들지 않고도 인과적 일관성을 만족시킬 수 있다.
대부분의 시스템에서 실제로 필요한 것은 선형성이 아닌 인과성입니다.

3.2.3. 인과적 의존성 담기

인과성을 유지하기 위해서는 어떤 연산이 어떤 다른 연산보다 먼저 실행됐는지 알아야 한다.
인과적 의존성을 결정하려면 시스템에 있는 노드에 관한 지식을 기술할 방법이 필요하다.
리더 없는 데이더 저장소는 갱신 손실 방지를 위해 같은 키에 대한 동시 쓰기를 검출해야한다.
인과적 의존성은 여기서 더 나아가 버전 벡터(version vector)를 일반화할 수 있다.

3.3. 일련번호 순서화

인과성은 중요한 이론적 개념이지만 모든 인과적 의존성을 추적하는 것은 실용성이 떨어진다.
더 좋은 방법으로는 일련번호나 타임스탬프를 써서 이벤트의 순서를 정할 수 있다.
타임스템프는 물리적 시계 대신 논리적 시계에서 얻어도 된다.
이러한 일련번호나 타임스탬프는 크기가 작고 전체 순서를 제공한다.
특히 인과성에 일관적인 전체 순서대로 일련번호를 생성할 수 있다.
단일 리더 복제를 쓰는 데이터베이스에서는 복제 로그가 인과성에 일관적인 쓰기 연산의 전체 순서를 정의한다.

3.3.1. 비인과적 일련번호 생성기

단일 리더가 없다면 연산에 사용할 일련번호를 생성하는 방법이 명확해 보이지 않습니다. 현실에서는 다양한 방법이 사용된다.
각 노드가 자신만의 독립적인 일련번호 집합을 생성할 수 있다.
각 노드는 초당 연산수가 다를 수 있다.
각 연산에 일 기준 시계에서 얻은 타임스탬프를 붙일 수 있다.
물리적 시계에서 얻은 타임스탬프는 시계 스큐에 종속적이다.
일련번호 블록을 미리 할당할 수 있다.
블록 할당자의 경우, 한 연산이 1001과 2000사이의 구간에서 일련번호를 받고 인과적으로 나중에 실행되는 연산이 1과 1000사이의 구간에서 일련번호를 받을 수 있다.
위의 세가지 선택지는 카운터를 증가하는 단일 리더에 모든 연산보다는 확장성이 좋으나, 생성된 일련번호가 인과성에 일관적이지 않다.

3.3.2. 램포트 타임스탬프

램포트 타임스탬프는 인과성에 일관적인 전체 순서화를 제공한다.
이러한 방법 대신 인과성에 일관적인 일련번호를 생성하는 간단한 방법은 램포트 타임스탬프(Lamport timestamp)다.
램포트 타임스탬프는 카운터, 노드ID의 쌍이다.
두 노드는 때때로 카운터 값이 같을 수 있지만 노드ID가 포함되어 타임스탬프는 유일하다.

3.3.3. 타임스탬프 순서화로는 충분하지 않다

램포트 타임스탬프가 인과성에 일관적인 연산의 전체 순서를 정의하지만 분산 시스템의 공통 문제를 해결하는 데 아주 충분하지는 않다.
램포트 타임스탬프는 사후에 성공하는 쪽을 결정하는 데는 효과적이다.
시스템에서 사용자명 생성 연산을 모두 모으면 그들의 타임스태프를 비교할 수 있다.
But. 노드가 사용자로부터 사용자명 생성 요청을 막받고 그 요청이 성공해야 하는지 실패해야 하는지 당장 결정해야 할 때는 이 방법으로 부족하다.
다른 어떤 노드도 동시에 더 낮은 타임스탬프를 가지고 동일한 사용자명으로 계정 생성을 처리하는 중이 아니라고 확신하려면 다른 노드가 무엇을 하고 있는지 확인해야한다.
다른 노드 중 하나에 장애가 생기거나 네트워크 문제 때문에 연결할 수 없다면 시스템이 멈추게 된다.
문제는 연산의 전체 순서는 모든 연산은 모은 후에야 드러난다.
결론적으로 사용자명에 대해 유일성 제약 조건 같은 것을 구현하려면 연산의 전체 순서가 있는 것으로는 충분하지 않다.

3.4. 전체 순서 브로드캐스트

단일 리더 복제는 한 노드를 리더로 선택하고 리더의 단일 CPU 코어에서 모든 연산을 차례대로 배열함으로써 연산의 전체 순서를 정한다.
분산 시스템 분야에서 이 문제는 전체 순서 브로드캐스트(total order broadcast)나 원자적 브로드 캐스트(atomic broadcast)로 알려져 있다.
전체 순서 브로드캐스트는 보통 노드 사이에 메시지를 교환하는 프로토콜로 기술된다.
비공식적으로는 두 가지 안전성 속성을 항상 만족해야 한다.
신뢰성 있는 전달(reliable delivery)
어떤 메시지도 손실되지 않습니다. 메시지가 한 노드에 전달되면 모든 노드에도 전달된다.
전체 순서가 정해진 전달(Totally ordered delivery)
메시지는 모든 노드에 같은 순서로 전달된다.

3.4.1. 전체순서 브로드캐스트 사용하기

주키퍼나 etcd 같은 합의 서비스는 전체 순서 브로드캐스트를 실제로 구현한다.
모든 메시지가 데이터베이스의 쓰기를 나타내고 모든 복제 서버가 같은 쓰기 연산을 같은 순서로 처리하면 복제 서버들은 서로 일관성 있는상태를 유지한다.
이 원리를 상태 기계 복제(state machine replication) 이라고 한다.
전체 순서 브로드캐스트는 직렬성 트랜잭션을 구현하는데도 쓸 수 있다.
모든 노드가 메시지들을 같은 순서로 처리한다면 데이터베이스의 파티션과 복제본은 서로 일관적인 상태를 유지한다.
전체 순서 브로드캐스트의 중요한 특면은 메시지가 전달되는 시점에 그 순서가 고정된다는 것이다.
이 때문에 전체 순서 브로드캐스트가 타임스탬프 순서화보다 강하다.
전체 순서 브로드캐스트를 보는 또 다른 관점은 로그를 만드는 방법 중 하나라는 것이다.

4. 분산 트랜잭션과 합의

합의는 분산 컴퓨팅에서 가장 중요하고 근본적인 문제 중 하나이며 목적은 여러 노드들이 뭔가에 동의하게 만드는 것이다.
노드가 동의하는 것이 중요한 상황
리더 선출 = 단일 리더 복제를 사용하는 데이터베이스에서는 모든 노드는 어떤 노드가 리더인지를 동의해야 한다.
원자적 커밋 = 트랜잭션 원자성을 유지하고 싶다면 모든 노드가 트랜잭션의 결과에 동의하게 만들어야 한다.

4.1. 원자적 커밋과 2단계 커밋(2PC)

트랜잭션 원자성의 목적은 여러 쓰기를 실행하는 도중 뭔가 잘못되는 경우에 간단한 시맨틱을 제공하기 위함이다.
트랜잭션의 결과는 커밋 성공이나 어보트입니다.
원자성은 실패한 트랜잭션이 절반만 완료된 결과나 절반만 갱신된 상태로 데이터베이스를 어지럽히는 것을 막아준다.

4.1.1. 단일 노드에서 분산 원자적 커밋으로

단일 데이터베이스 노드에서 실행되는 트랜잭션에게 원자성은 흔히 저장소 엔진에서 구현된다.
단일 노드에서 트랜잭션 커밋은 데이터가 디스크에 지속성 있게 쓰여지는 순서에 결정적으로 의존한다.
커밋을 원자적으로 만들어주는 것은 단일 장치다.
트랜잭션에 여러 노드가 관여된다면 어떤 노드에서는 커밋이 성공하고 다른 노드에서는 실패해서 원자성 보장을 위반하기 쉽다.
어떤 노드들은 제약 조건 위반이나 충도을 가짐해서 어브가 필요하게 되지만 다른 노드들은 성공적으로 커밋될 수 있다.
어떤 커밋 요청은 네트워크에서 손실되어 타임아웃 때문에 결국 어보트되지만 다른 커밋 요청은 전달될 수 있다.
어떤 노드는 커밋 레코드가 완전히 쓰여지기 전에 죽어서 복구할 때 롤백되지만 다른 노드는 성공적으로 커밋될 수 있다.
어떤 노드가 트랜잭션을 커밋하지만 다른 노드는 어보트한다면 노드들이 서로 일관성이 없어진다.
트랜잭션 커밋은 되돌릴 수 없어야하며 이 원칙은 "커밋 후 읽기"에서 설명한 커밋 후 읽기 격리의 기반을 형성한다.
커밋된 트랜잭션의 효과를 나중에 다른 보상 트랜잭션(compensating transaction) 이 취소하는 것은 가능하다.

4.1.2. 2단계 커밋 소개

2단계 커밋은 여러 노드에 걸친 원자적 트랜잭션 커밋을 달성하는, 즉 모든 노드가 커밋되거나 모든 노드가 어보트되도록 보장되는 알고리즘이다.
일부 데이터베이스는 2PCV가 내부적으로 사용되고 XA 트랜잭션의 형태나 SOAP 웹 서비스용 WS-AtomicTransaction을 통해 애플리케이션에서 사용할 수 있다.
2단계 커밋의 성공적인 실행
2PC는 단일 노드 트랜잭션에서는 보통 존재하지 않느 새로운 컴포넌트인 코디네이터를 사용한다.
2PC 트랜잭션에서 데이터베이스 노드를 트랜잭션의 참여자(participant) 라고 부른다.
애플리케이션이 커밋할 준비가 되면 코디네이터가 1단계를 시작하고, 각 노드에 준비 요청을 보내서 커밋할 수 있는지 물어본 뒤 그 후 코디네이터는 참여자들의 응답을 추적한다.
모든 참여자가 커밋할 준비가 됐다는 뜻으로 "네"라고 응답하면 코디네이터는 2단계에서 커밋 요청을 보내고 커밋이 실제로 일어난다.
함여자 중 누구라도 "아니오"로 응답하면 코디네이터는 2단계에서 모든 노드에 어보트 요청을 보낸다.

4.1.3. 약속에 관한 시스템

준비 요청과 커밋 요청은 2단계에서 경우에서 그냥 쉽게 손실될 수 있다.
애플리케이션은 분산 트랜잭션을 시작할 때 코디네이터에게 트랜잭션 ID를 요청한다.
이 트랜잭션 ID는 전역적으로 유일하다.
애플리케이션은 발급받은 트랜잭션 ID를 가지고 각 참여자에서 단일 노드 트랜잭션을 시작한다.
읽기와 쓰기는 이런 단일 노드 트랜잭션 중 하나에서 실행된다.
이 단계에서 노드가 죽거나 타임아웃 등의 문제가 발생하면 코디네이터나 참여자 중 누군가 어보트할 수 있다.
참여자가 준비 요청을 받으면 트랜잭션 커밋할 수 있는지 확인한다.
가능하다고 판단이 되면 코디네이터에게 ‘네’라고 응답을 한다.
코디네이터가 준비 요청에 대한 응답을 받은 후, 트랜잭션을 커밋할지 어보트 할지 최종적으로 결정한다.
모든 참여자가 ‘네’에 투표했을 때만 커밋한다.
코디네이터가 추후 죽을 수도 있으므로 결정을 디스크의 트랜잭션 로그에 기록하는데 이를 커밋 포인트라 한다.
코디네이터가 최종 결정을 완료하였으니, 모든 참여자에게 커밋 또는 어보트 요청을 전송한다.
이 요청이 실패하거나 타임아웃이 발생하면 성공할 때 까지 영원히 재시도해야 한다.
코디네이터가 한 번 결정하면 그 결정은 변경할 수 없다.
이러한 약속으로 2PC의 원자성을 보장한다.

4.1.4. 코디네이터 장애

코디네이터가 준비 요청을 보내기 전에 장애가 나면 참여자가 안전하게 트랜잭션을 어보트할 수 있다.
코디네이터로부터 트랜잭션이 커밋됐는지 회신을 받는 와중, 코디네이터가 죽거나 이 시점에 네트워크 장애가 나면 참여자는 기다릴 수 밖에 없으며 이 트랜잭션을 의심스럽다(in doubt) 혹은 불확실하다(uncertain) 라고 한다.
2PC가 완료할 수 있는 유일한 방법은 코디네이터가 복구되기를 기다리는 것 뿐이다.
이때문에 코디네이터가 참여자들에게 커밋이나 어보트 요청을 보내기 전에 디스크에 있는 트랜잭션 로그에 자신의 커밋이나 어보트 결정을 써야하는 이유이다.

4.1.5. 3단계 커밋

2단계 커밋은 2PC가 코디네이터가 복구하기를 기다리느라 멈출 수 있다는 사실 때문에 블로킹 원자적 커밋 프로토콜이라고 부른다.
2PC의 대안으로 3단계 커밋(3PC)이라는 알고리즘이 제안되었다.
논블로킹 원자적 커밋은 완벽한 장애 감지기(perfect failure detector) ,노드가 죽었는지 아닌지 구별할 수 있는 신뢰성 있는 메커니즘이 필요하다.
타임아웃은 신뢰성 잇는 장애 감지기가 아니다.
이러한 이유로 2PC가 계속 사용된다.