9장. 일관성과 합의
- 분산 시스템에서 결함을 견뎌낼(tolerating), 즉 내부 구성 요소에 결함이 있더라도 서비스는 올바르게 동작하게 할 방법을 찾아야 한다
- 내결함성을 지닌 시스템을 구축하는 가장 좋은 방법은 유용한 보장을 해주는(애플리케이션이 분산 시스템에 있는 문제를 무시할 수 있게 만들어주는) 범용 추상화를 찾아 이를 구현하고 애플리케이션에서 이 보장에 의존하게 하는 것이다
- 분산 시스템에서 가장 중요한 추상화는 합의, 즉 모든 노드가 어떤 것에 동의하게 만드는 것
- 이번 장에서 분산 시스템에서 제공될 수 있는 보장과 추상화의 범위를 알아보고, 합의와 합의 관련 문제를 해결하는 알고리즘을 살펴본다
일관성 보장
- 복제 데이터베이스는 대부분 최소한 최종적 일관성을 제공한다
- 데이터베이스에 쓰기를 멈추고 불특정 시간 동안 기다리면 결국 모든 읽기 요청이 같은 값을 반환
- 그러나 이것은 매우 약한 보장
- 언제 복제본이 (같은 값으로 )수렴될지에 대해서는 알 수 없다
- 이번 장에서 데이터 시스템이 선택적으로 제공할 수 있는 더욱 강한 일관성 모델을 살펴본다
- 강한 보장을 제공하는 시스템은 성능이 나쁘거나 약한 보장을 제공하는 시스템보다 내결함성이 약할지도 모른다
- 그럼에도 강한 보장은 올바르게 사용하기 쉬우므로 매력적
- 분산 일관성 모델과 트랜잭션 격리 수준 계층에는 비슷한 점이 있다
- 이들은 대개 독립적인 관심사
- 트랜잭션 격리는 주로 동시에 실행되는 트랜잭션 때문에 발생하는 경쟁 조건을 회피하기 위한 것
- 분산 일관성은 지연과 결함이 있더라도 복제본의 상태를 코디네이션하는 것에 관한 것
- 그러나 사실 이 영역들은 깊게 연결돼 있다
- 공통적으로 사용되는 가장 강한 일관성 모델 중 하나인 선형성(linearizability)
- 분산 시스템에서의 이벤트 순서화 문제, 특히 인과성과 전체 순서화와 관련된 문제
- 분산 트랜잭션을 원자적으로 커밋하는 방법
선형성
- 선형성의 기본 아이디어
- 시스템에 데이터 복사본이 하나만 있고 그 데이터를 대상으로 수행하는 모든 연산은 원자적인 것처럼 보이게 만드는 것
- 이런 보장이 있다면 현실에는 여러 복제본이 있더라도 애플리케이션이 거기에 신경 쓸 필요가 없다
- 선형성은 최신성 보장(recency guarantee)
- 읽힌 값이 최근에 갱신된 값이며 뒤처진 캐시나 복제본에서 나온 값이 아니라고 보장해준다

- 선형성 위반 사례
- 밥은 앨리스가 최종 점수를 외친 것을 들은 후에 새로고침 버튼을 눌렀음(질의를 실행했음)을 알고 따라서 그는 질의의 결과가 적어도 앨리스의 질의 결과만큼은 새로워야 한다고 기대한다
시스템에 선형성을 부여하는 것은 무엇인가?

- 쓰기는 쓰기 연산의 시작과 끝 사이의 어느 시점에선가 처리되어야 하고 읽기는 읽기 연산의 시작과 끝 사이의 어느 시점에선가 처리되어야 한다
- 쓰기 연산과 시간이 겹치는 읽기 연산은 0을 반환할 수도 1을 반환할 수도 있다
- 쓰기와 동시에 실행되는 읽기가 오래된 값과 새로운 값 사이에서 여러 번 왔다 갔다 하는 것은 “데이터의 단일 복사본”을 모방하는 시스템에 기대하는 바가 아니다

- 새로운 제약 조건을 추가해 한 클라이언트가 새로운 값 1을 반환하면 이후의 모든 읽기 또한 새로운 값을 반환해야 한다(쓰기 연산이 아직 완료되지 않았더라도)
- A의 읽기가 반환된 후 B의 읽기는 확실히 A의 읽기 뒤에 실행되므로 C가 실행한 쓰기가 아직 진행 중이라도 역시 1을 반환해야 한다

- 선형성의 요구사항은 연산 표시를 모은 선들이 항상 시간순으로 진행돼야 하고 결코 뒤로 가서는 안된다는 것이다
- 데이터베이스가 D의 쓰기를 먼저 처리한 후 A의 쓰기를 처리하고 마지막으로 B의 읽기를 처리했다
- 요청을 보낸 순서와는 다르지만 세 요청이 동시적이기 때문에 이 순서는 허용된다
- 클라이언트 B의 읽기는 클라이언트 A가 데이터베이스로부터 값을 1로 쓰는 데 성공했다는 응답을 받기 전에 1을 반환했다
- 값이 쓰여지기 전에 읽혔다는 의미가 아니라 데이터베이스에서 클라이언트 A로 가는 ok 응답이 네트워크에서 지연됐다는 의미
- 이 모델은 어떤 트랜잭션 격리도 가정하지 않는다
- 다른 클라이언트가 동시에 값을 바꾸지 않았는지 확인하기 위해 원자적 compare-and-set(cas) 연산을 사용할 수 있다
- 클라이언트 B의 마지막 읽기는 선형적이지 않다
- 이 연산은 C의 cas 쓰기와 동시적이다
- 클라이언트 A는 B의 읽기가 시작하기 전에 이미 새로운 값 4을 읽었다
- 따라서 B는 A가 읽은 것보다 과거의 값을 읽는 것이 허용되지 않는다
- 선형성과 직렬성
- 직렬성
- 모든 트랜잭션이 여러 객체를 읽고 쓸 수 있는 상황에서의 트랜잭션의 격리 속성
- 트랜잭션들이 어떤 순서에 따라 실행되는 것처럼 동작하도록 보장해준다
- 그 순서가 트랜잭션들이 실제 실행되는 순서와 달라도 상관 없다
- 선형성
- 레지스터(개별 객체)에 실행되는 읽기와 쓰기에 대한 최신성 보장
- 연산을 트랜잭션으로 묶지 않아서 충돌 구체화 같은 부가적인 수단을 사용하지 않으면 쓰기 스큐같은 문제를 막지 못한다
- 데이터베이스는 직렬성과 선형성을 모두 제공할 수 있으며, 이런 조합을 엄격한 직렬성(strict serializability) 또는 강한 단일 복사본 직렬성(strong one-copy serializability, strong-1SR)이라고 한다
- 2단계 잠금(2PL)이나 실제적인 직렬 실행을 기반으로 한 직렬성 구현은 보통 선형적
- 직렬성 스냅숏 격리(SSI)는 선형적이지 않다
- 직렬성 스냅숏 격리는 설계상 읽는 쪽과 쓰는 쪽 사이의 잠금 경쟁을 피하기 위해 일관된 스냅숏에서 읽는다
- 일관된 스냅숏의 요점은 스냅숏보다 나중에 실행된 쓰기를 포함하지 않는다는 것이고, 따라서 스냅숏에서 읽으면 선형적이지 않다
선형성에 기대기
- 어떤 환경에서 선형성이 유용할까?
- 잠금과 리더 선출
- 단일 리더 복제 시스템은 리더가 진짜로 하나만 존재하도록 보장해야 한다
- 리더를 선출하는 방법은 잠금을 사용하는 것이고, 이 잠금은 어떻게 구현하든지 선형적이어야 한다
- 분산 잠금과 리더 선출을 구현하기 위해 아파치 주키퍼나 etcd 같은 코디네이션 서비스가 사용
- 합의 알고리즘을 사용해 선형성 연산을 내결함성이 있는 방식으로 구현
- 아파치 큐레이터 같은 라이브러리가 주키퍼 위에 고수준 레시피를 제공해서 도움을 준다
- 분산 잠금은 오라클 리얼 애플리케이션 클러스터(RAC) 같은 분산 데이터베이스에서 훨씬 세분화된 수준으로 사용되기도 한다
- RAC는 여러 노드가 동일한 디스크 저장 시스템을 공유해서 접근 → 디스크 페이지마다 잠금을 사용
- 제약 조건과 유일성 보장
- 사용자명이나 이메일 주소는 사용자 한 명을 유일하게 식별할 수 있어야 하며 파일 저장 서비스에서는 경로와 파일 이름이 동일한 파일이 두 개 있을 수 없다
- 잠금과 비슷 → 사용자가 선택한 사용자명에 “잠금”을 획득하는 것으로 생각
- 연산은 원자적 compare-and-set과도 매우 비슷
- 은행 계좌 잔고가 결코 음수가 되지 않게 하거나 창고에 있는 재고보다 더 팔지 않게 하거나 두 사람이 동시에 같은 비행기나 영화 좌석을 예매하지 못하게 보장하고 싶을 때 → 모드 노드가 동의하는 하나의 최신 값이 있기를 요구한다
-
채널 간 타이밍 의존성

- 사용자가 사진을 올리고 백그라운드 프로세스가 사진을 빨리 내려받을 수 있도록 저해상도로 바꾸는(썸네일) 웹사이트
- 파일 저장 서비스가 선형적이지 않다면 경쟁 조건의 위험이 있다
- 메시지 큐가 저장 서비스 내부의 복제보다 빠를 수도 있다
- 이런 경우 크기 변경 모듈이 이미지를 읽으면 그 이미지의 과거 버전을 보거나 아무것도 보지 못할 수도 있다
- 웹 서버와 크기 변경 모듈 사이에 두 가지 다른 통신 채널, 파일 저장소와 메시지 큐가 있기 때문에 발생한다
- 선형성의 최신성 보장이 없으면 두 채널 사이에 경쟁 조건이 발생할 수 있다
선형성 시스템 구현하기
- 복제를 통해 시스템이 내결함성을 지니도록 하면서 이를 선형적으로 만들 수 있는가?
- 단일 리더 복제(선형적이 될 가능성 있음)
- 리더나 동기식으로 갱신된 팔로워에서 실행한 읽기는 선형적이 될 가능성이 있다
- 리더가 아닌 노드가 자신이 리더라고 생각해서 계속해서 요청을 처리하면 선형성을 위반하기 쉽다
- 비동기 복제를 사용하면 장애 복구를 할 때 커밋된 쓰기가 손실될 수 있는데, 이는 지속성과 선형성을 모두 위반하는 것
- 합의 알고리즘(선형적)
- 단일 리더 복제를 닮았지만, 스플릿 브레인과 복제본이 뒤처지는 문제를 막을 수단이 포함된다
- 주키퍼와 etcd가 이렇게 동작한다
- 다중 리더 복제(비선형적)
- 여러 노드에서 동시에 쓰기를 처리하고 그 내용을 비동기로 다른 노드에 복제하므로 일반적으로 비선형적이다
- 다중 리더 복제 시스템은 충돌 해소가 필요한 충돌 쓰기를 만들 수 있다
- 리더 없는 복제(아마도 비선형적)
- 정족수 읽기와 쓰기를 요구함으로써 “엄격한 일관성”을 달성할 수 있다고 주장하지만 완전한 진실은 아니다
- 일 기준 시계를 기반으로 한 “최종 쓰기 승리” 충돌 해소 방법은 거의 확실히 비선형적이다
- 시계 타임스탬프는 시계 스큐 때문에 이벤트의 실제 순서와의 일치를 보장할 수 없다
- 느슨한 정족수도 선형성의 가능성을 망친다
선형성과 정족수

- 정족수 조건이 만족(w + r > n)됨에도 이 실행은 선형적이지 않다
- 클라이언트 B는 두 노드로 구성된 다른 정족수로부터 읽어서 두 노드 모두에서 예전 값 0을 본다
- 성능이 떨어지는 비용을 지불하고 다이나모 스타일 정족수를 선형적으로 만드는 게 가능하다
- 읽기를 실행하는 클라이언트는 결과를 반환하기 전에 읽기 복구를 동기식으로 수행
- 쓰기를 실행하는 클라이언트는 쓰기 요청을 보내기 전에 노드들의 정족수로부터 최신 상태 읽기를 수행
- 그러나 리악은 성능상 불이익 때문에 동기식 읽기 복구를 수행 X
- 카산드라는 정족수 읽기를 할 때 읽기 복구가 완료되기를 기다리지만 최종 쓰기 승리 충돌 해소 방법을 쓰기 때문에 같은 키에 여러 쓰기를 동시에 실행하면 선형성을 잃게 된다
- 다이나모 스타일 복제를 하는 리더 없는 시스템은 선형성을 제공하지 않는다고 보는 게 가장 안전하다
선형성의 비용

- 각 데이터센터의 내부 네트워크는 동작하고 클라이언트는 데이터센터에 접근할 수 있지만 데이터센터끼리는 서로 연결할 수 없다고 가정한 사례
- 다중 리더 데이터베이스에서 각 데이터센터는 계속 정상 동작할 수 있다
- 한 데이터센터에 쓰여진 내용이 비동기로 다른 데이터센터로 복제되므로 쓰기는 그냥 큐에 쌓였다가 네트워크 연결이 복구되면 전달된다
- 단일 리더 설정에서 데이터센터 사이의 네트워크가 끊기면 팔로워 데이터센터로 접속한 클라이언트들은 리더로 연결할 수 없으므로 아무것도 쓸 수 없고 선형적 읽기도 할 수 없다
- 팔로워로부터 읽을 수는 있지만 비선형적(뒤처졌을 수 있다)
- 애플리케이션에서 선형성 읽기와 쓰기를 요구한다면 네트워크가 끊길 때 리더와 연결할 수 없는 데이터센터에서는 애플리케이션을 사용할 수 없다
- 클라이언트가 리더 데이터센터로 직접 접속할 수 없다면 해당 클라이언트는 네트워크 링크가 복구될 때까지 중단을 경험한다
CAP 정리
- 애플리케이션에서 선형성을 요구하고 네트워크 문제 때문에 일부 복제 서버가 다른 복제 서버와 연결이 끊기면 일부 복제 서버는 요청을 처리할 수 없다(가용성 X)
- 애플리케이션에서 선형성을 요구하지 않는다면 이 애플리케이션은 네트워크 문제에 직면해도 가용한 상태를 유지하지만 그 동작은 선형적이지 않다
- CAP 정리
- CAP는 각각 일관성(Consistency), 가용성(Availability), 분단 내성(Partition tolerance)를 의미하며, 네트워크 분단이 생겼을 때 일관성과 가용성 중 하나를 선택하라는 의미로 이해하는 게 좋다
- 선형성이 필요 없는 애플리케이션은 네트워크 문제에 더 강인하다
- 공식적으로 정의된 CAP 정리는 매우 범위가 좁고 네트워크 지연, 죽은 노드나 다른 트레이드오프에 대한 어떤 것도 고려하지 않기 때문에 역사적인 영향력은 있지만 시스템을 설계할 때는 실용적인 가치가 거의 없다
선형성과 네트워크 지연
- 선형성은 유용한 보장이지만 현실에서 실제로 선형적인 시스템은 놀랄 만큼 드물다, 왜?
- 선형성은 느리다
- 여러 분산 데이터베이스는 성능을 향상시키기 위해서 선형성 보장을 제공하기 않기를 택한다
- 심지어 최신 다중코어 CPU의 램(RAM)조차 선형적이지 않다
- 모든 CPU 코어가 저마다 메모리 캐시와 저장 버퍼를 갖기 때문
- 메모리 접근은 기본적으로 캐시로 먼저 가고(훨씬 빠르니까) 변경은 메인 메모리에 비동기로 기록
- 이런 특성은 성능에 필수적, 그러나 데이터 복사본이 여러 개 생기고 비동기로 갱신되므로 선형성은 손실
순서화 보장
- 선형성 레지스터는 연산들이 어떤 잘 정의된 순서대로 실행된다는 것을 암시한다
- 순서화는 계속해서 되풀이되는 주제이며, 중요한 근본적 아이디어일 수 있다는 것을 시사한다
- 단일 리더 복제에서 리더의 주 목적은 복제 로그에서 쓰기의 순서, 즉 팔로워가 쓰기를 적용하는 순서를 결정한다
- 직렬성은 트랜잭션들이 마치 어떤 일련의 순서에 따라 실행되는 것처럼 동작하도록 보장하는 것과 관련돼 있다
- 분산 시스템에서 타임스탬프와 시계 사용은 무질서한 세상에 질서(순서)를 부여하려는 시도다.
- 순서화, 선형성, 합의 사이에는 깊은 연결 관계가 있음이 드러난다
순서화와 인과성
- 순서화는 인과성을 보존하는 데 도움을 준다
- 일관된 순서로 읽기에서 질문에 대한 응답을 먼저 보고 나서 응답된 질문을 보게 되면 원인과 결과에 관한 직관을 위반하기 때문에 혼란스럽다
- 리더 사이의 복제에서 네트워크 지연 때문에 어떤 쓰기가 다른 쓰기를 “추월”할 수 있음을 알았다
- 인과성은 로우가 갱신되기 전에 먼저 생성돼야 함을 의미한다
- 이전 발생(happened before) 관계는 인과성을 표현하는 또 다른 방법이다
- A가 B보다 먼저 실행됐다면 B가 A에 대해 알거나 A를 기반으로 하거나 A에 의존할지도 모른다
- A와 B가 동시적이면 그들 사이에는 인과적 연결이 없다
- 트랜잭션용 스냅숏 격리의 맥락에서 트랜잭션은 일관된 스냅숏으로부터 읽는다
- 이 맥락에서 “일관적”이란 인과성에 일관적(consistent with causality)이라는 의미다
- 한 시점에서 데이터베이스를 보면 인과성 측면에서 그 시점 전에 실행된 모든 연산의 효과는 볼 수 있지만 나중에 실행된 연산의 효과는 볼 수 없다
- 트랜잭션들 사이의 쓰기 스큐 예제는 인과적 의존성을 보여준다
- 호출 대기에서 빠지는 동작은 현재 누가 호출 대기 중인지 관찰하는 것에 인과적으로 의존한다
- 직렬성 스냅숏 격리는 트랜잭션 사이의 인과적 의존성을 추적함으로써 쓰기 스큐를 검출한다
- 앨리스의 탄성은 점수 발표에 인과적으로 의존적이므로 밥 또한 앨리스의 외침을 들은 후에 그 점수를 볼 수 있어야 한다
- 인과성은 이벤트에 순서를 부과한다
- 인과적으로 의존하는 연산의 연쇄는 시스템에서 인과적 순서, 즉 무엇이 무엇보다 먼저 일어났는가를 정의한다
- 시스템이 인과성에 의해 부과된 순서를 지키면 그 시스템은 인과적으로 일관적(causally consistent)이라고 한다
인과적 순서가 전체 순서는 아니다
- 전체 순서(total order)는 어떤 두 요소를 비교할 수 있게 하므로 두 요소가 있으면 항상 어떤 것이 더 크고 어떤 것이 더 작은지 말할 수 있다
- 수학적 집합은 항상 전체 순서를 정할 수 있는 것이 아니다
- 어느 것도 다른 것의 부분 집합이 아니므로 이것들은 비교 불가(incomparable)
- 수학적 집합은 부분적으로 순서가 정해진다(partially ordered)
- 어떤 경우에는 한 집합이 다른 집합보다 크지만 다른 경우에는 비교 불가능하다
- 선형성
- 연산의 전체 순서를 정할 수 있다
- 어떤 두 연산에 대해 항상 둘 중 하나가 먼저 실행됐다고 말할 수 있다
- 인과성
- 두 이벤트에 인과적인 관계가 있으면 이들은 순서가 있지만 이들이 동시에 실행됐다면 비교할 수 없다
- 인과성이 전체 순서가 아닌 부분 순서를 정의한다는 뜻이다
- 정의에 따르면 선형적 데이터스토어에는 동시적 연산이 없다
- 하나의 타임라인이 있고, 모든 연산은 그 타임라인을 따라서 전체 순서가 정해져야 한다
- 동시성은 타임라인이 갈라졌다 다시 합쳐지는 것을 의미한다
- 깃의 버전 히스토리는 인과적 의존성 그래프와 매우 유사하다
선형성은 인과적 일관성보다 강하다
- 선형성은 인과성을 내포한다
- 어떤 시스템이든지 선형적이라면 인과성도 올바르게 유지한다
- 그러나 시스템을 선형적으로 만들면, 특히 네트워크 지연이 크면 성능과 가용성에 해가 될 수 있다
- 시스템은 선형적으로 만드는 성능 손해를 유발하지 않고도 인과적 일관성을 만족시킬 수 있다
- 인과적 일관성은 네트워크 지연 때문에 느려지지 않고 네트워크 장애가 발생해도 가용한 일관성 모델 중 가장 강한 것이다
- 많은 경우에 선형성이 필요한 것처럼 보이는 시스템에 사실 진짜로 필요한 것은 인과적 일관성이며 이는 더 효율적으로 구현될 수 있다
인과적 의존성 담기
- 인과성을 유지하기 위해 어떤 연산이 다른 연산보다 먼저 실행됐는지 알아야 한다
- 부분 순서 → 동시에 실행되는 연산은 어떤 순서로든 처리될 수 있지만 한 연산이 다른 연산보다 먼저 실행됐다면 모든 복제 서버에서 그 순서로 처리돼야 한다
- 어떤 연산이 다른 연산보다 먼저 실행됐는지 결정하는 기법
- 단일 키뿐만 아니라 전체 데이터베이스에 걸친 인과적 의존성을 추적해야 한다
- 이를 위해 버전 벡터(version vector)를 일반화할 수 있다
- 인과적 순서를 결정하기 위해 데이터베이스는 애플리케이션이 데이터의 어떤 버전을 읽었는지 알아야 한다
일련번호 순서화
인과성을 추적하기 위해 일련번호나 타임스탬프를 통해 이벤트의 순서를 정할 수 있다.
이 때 타임스탬프는 물리적 시계(신뢰성 없는 시계 문제가 있음)가 아니라 논리적 시계(일련번호 생성 알고리즘, 보통 카운터)에서 얻어도 된다.
이렇게 생성된 일련번호는 전체 순서를 보장하기 때문에 인과성에 일관적인 전체 순서대로 일련번호 생성이 가능하다. 단일리더 복제에서는 연산마다 카운터를 증가시켜 복제 로그 각 연산에 단조 증가하는 일련번호를 할당하기만 하면 됨.
비인과적 일련번호 생성기
다중 리더, 리더 없는 데이터베이스, 혹은 파티셔닝이 되어 있다면 3가지 방법이 고려될 수 있다.
- 각 노드마다 자신만의 일련번호 집합을 생성
- 예를 들어 한 노드는 짝수, 다른 노드는 홀수만 생성
- 각 연산에 일 기준 시계(물리적 시계)에서 얻은 타임스탬프를 붙인다
- 이런 타임스탬프는 순차적이지 않지만 해상도가 충분히 높다면 연산의 전체 순서를 정하는데 충분할 수 있음
- 일련번호 블럭을 미리 할당한다
- 노드 A는 1~1000까지, 노드 B는 1001~2000까지의 블럭을 차지할 수 있다.
이 방법들은 단일 리더에서 모든 연산을 처리하는 것보다 확장성이 좋다. 하지만 이러한 일련번호는 인과성에 일관적이지 못한 문제가 있다.
- 각 노드는 초당 연산 수가 다를 수 있다
- 물리적 시계에서 얻은 타임스탬프는 시계 스큐에 종속적이기 때문에 인과성에 일관적이지 않게 될 가능성이 있다
- 블럭 할당자의 경우 한 연산이 1001~2000 사이 구간의 일련번호를 받고, 인과적으로 나중에 실행되는 연산이 1~1000 사이 구간의 일련번호를 받게 될 가능성이 있다. (일련번호가 일관성을 잃어버림)
즉 위에 3가지 방법은 인과적이지 못하다.
램포트 타임스탬프
인과성에 일관적인 일련번호를 생성하는 방법.

타임스탬프 순서화로 충분하지 않다
램포트 타임스탬프가 인과성에 일관적 연산의 전체 순서를 정의하지만, 분산 시스템의 여러 공통 문제 해결에는 아주 충분하지는 않다.
램포트 타임스탬프는 사후에 성공하는 쪽을 결정하는데에 효과적이지만, 당장 결정해야 하는 경우에는 부족하기 떄문이다.
다른 어떤 노드도 동시에 더 낮은 타임스탬프를 갖고 동일한 사용자명으로 계정 생성을 처리하는 중이 아니라고 확신하기 위해서는 다른 모든 노드가 무슨 작업을 하고 있는지 확인해야 한다. 이는 노드 중 하나에 장애가 발생할 경우 시스템이 멈추게 되어 내결함성을 지키지 못하게 된다.
여기서 문제는 연산의 전체 순서는 모든 연산을 모은 후에 드러난 다는 것(= 연산이 수행되는 도중에는 순서를 알 수 없을 수 있음)
= 언제 전체 순서가 확정되는지 알아야 한다는 아이디어를 뒤에 다룸
전체 순서 브로드 캐스트
프로그램이 단일 CPU 코어에서만 실행되면 CPU에서 실행된 순서가 전체 순서이기 때문에 연산의 전체 순서를 정하기가 매우 쉽다.
하지만 내결함성을 위해서는 분산 시스템을 이용해야 하기 떄문에 전체 순서가 동일하도록 합의하기 까다롭다.
단일 리더 복제에서는 한 노드를 리더로 선정하고 리더의 단일 CPU 코어에서 모든 연산을 차례대로 배열함으로써 연산의 전체 순서를 정한다. 여기서 문제는 처리량이 단일 리더가 처리할 수준을 넘어설 때 시스템을 어떻게 확장할 것인가 and 리더에 장애가 발생했을 때 어떻게 장애 복구를 할 것인가이다.
= 분산 시스템 분야에서 이 문제는 전체 순서 브로드캐스트, 원자적 브로드캐스트로 알려져 있음.
전체 순서 브로드캐스트는 보통 노드 사이에 메시지 교환하는 프로토콜로 기술되며 비공식적으로 2가지 안전성 속성을 항상 만족해야 한다.
- 신뢰성 있는 전달
- 어떤 메시지도 손실되지 않고, 한 노드에 전달이 됐다면 모든 노드에 동일하게 전달되어야 한다.
- 전체 순서가 정해진 전달
전체 순서 브로드캐스트 사용하기
모든 메시지가 데이터베이스에 쓰기를 나타내고, 모든 복제 서버가 같은 쓰기 연산을 같은 순서로 처리하면 복제 서버들은 서로 일관성 있는 상태를 유지한다.(일시적 복제 지연 제외) 이 원리를 상태 기계 복제라고 한다.
여기서 중요한 포인트는 전체 순서 브로드캐스트는 메시지가 전달되는 시점에 순서가 고정된다는 것에 있다.
- 이미 메시지가 전달됐다면, 그 앞 순서에 새로운 메시지를 끼워넣을 수 없다.
전체 순서 브로드캐스트를 사용해 선형성 저장소 구현하기
선형성 시스템(연산에 전체 순서 있음)과 전체 순서 브로드캐스트는 동일하지 않지만 밀접한 관계가 있다.
전체 순서 브로드캐스트는 비동기식이기 때문에 언제 메시지가 전달될지 보장되지 않지만, 선형성은 최신성 보장이기 떄문에 읽기가 최근에 쓰여진 값을 보는 것이 보장된다.
하지만 전체 순서 브로드캐스트 구현을 기반으로 선형성 저장소를 만들 수 있다.
예를 들어 사용자 계정을 유일하게 식별하도록 보장할 수 있는데, 이를 전체 순서 브로드캐스트를 추가 전용 로그로 사용해 선형성 compare-and-set 연산을 구현할 수 있다.
- 메시지를 로그에 추가해 점유하기 원하는 사용자명을 가리킨다.
- 로그를 읽고, 추가한 메시지가 되돌아오길 기다린다.
- 원하는 사용자명을 점유하려는 메시지가 있는지 확인한다.
- 이 때 첫번째 메시지가 자신의 메시지라면 성공, 아니라면 어보트 시킨다.
로그는 모든 노드에게 같은 순서로 전달되므로 쓰기 충돌이 일어날 경우 첫번째 쓰기를 승자로 택하고 나머지를 어보트 시킨다. (다중 객체 트랜잭션에도 비슷한 방법을 쓸 수 있음)
이 절차는 선형성 쓰기는 보장하지만, 선형성 읽기는 보장되지 않는다. (로그는 비동기적으로 갱신되기 떄문에 오래된 값이 읽힐 가능성이 있다, =타임라인 일관성, 순차적 일관성)
읽기를 선형적으로 만들기 위해선 몇 가지 선택지가 존재한다.
- 로그를 통해 순차 읽기를 할 수 있다.
- 로그에 메시지를 추가하고, 이 메시지가 돌아왓을 때 읽기를 수행한다.
- 읽기를 위한 쓰기 메시지를 모든 노드에 전파하고, 여기까지 모든 노드가 동기화가 됐을 때 읽는다고 보면 됨. (전체 순서가 보장되기 떄문에 가능한 방법)
- 로그에서 최신 로그 메시지의 위치를 선형적 방법으로 얻을 수 있다면 그 위치를 질의하고, 그 위치까지 모든 항목이 전달되기를 기다린 후 읽기를 수행할 수 있다.
- 주키퍼 sync() 연산이 그렇다는 듯.
- 로그 위치를 선형적으로 알 수 있는 여러 알고리즘이 있나봄. (합의 알고리즘, 버전 벡터, 타임스탬프 등)
- 쓰기를 실행할 때 동기식으로 갱신돼서 최신이 보장되는 복제서버에서 읽을 수 있다.
선형성 저장소를 사용해 전체 순서 브로드캐스트 구현하기
선형성 저장소를 기반으로 전체 순서 브로드캐스트 구현도 가능하다.
가장 쉬운 방법은 정수를 저장하고 원자적 increment-and-get 연산이 지원되는 선형성 레지스터가 있다고 가정하는 것. 대신 원자적 compare-and-set 연산이 있어도 된다.
- 전체 순서 브로드캐스트를 통해 보내고 싶은 모든 메시지를 준비한다.
- 선형성 정수로 increment-and-get 연산을 수행하고 레지스터에서 얻은 값을 일련번호로 메시지에 붙인다.
- 메시지를 모든 노드에 보낸다.(손실되면 재전송)
- 수신하는 노드들은 일련번호 순서대로 메시지를 전달한다. (동기화, 쓰기)
램포트 타임스탬프와 달리 선형성 레지스터를 증가시켜 얻은 숫자들은 틈이 없는 순열을 형성한다.
- 어떤 노드가 메시지 4를 전달하고, 일련번호가 6인 메시지를 받았다면 메시지 6을 전달하기 전에 메시지 5를 기다려야 한다.
램포트는 그렇지 않다. 이것이 전체 순서 브로드캐스트와 타임스탬프 순서화의 핵심 차이다.
원자적 increment-and-get 연산이 지원되는 선형성 정수를 만드는 것은 단일 노드에 변수 하나로 처리하면 쉽다. 하지만 내결함성을 위해서는 이 방식을 사용할 수 없다.
= 합의 알고리즘을 사용할 수 있다.
분산 트랜잭션과 합의
여러 노드들이 뭔가에 동의하게 만드는 것.
- 리더 선출
- 어떤 노드가 리더가 될지 모든 노드가 동의해야 한다.
- 잘못된다면 스플릿 브레인을 유발할 수 있다.
- 원자적 커밋
- 여러 노드나 파티션에 걸친 트랜잭션을 지원하는 데이터베이스에서는 어떤 노드에서는 성공하고 어떤 노드에서는 실패할 수 있다. 트랜잭션의 원자성을 위해서는 모든 노드가 트랜잭션 결과에 동의하게 만들어야 한다.
- = 원자적 커밋 문제
원자적 커밋과 2단계 커밋(2PC)
원자성은 실패한 트랜잭션이 절반만 완료되거나 절반만 갱신된 상태로 데이터베이스를 어지럽히는 것을 막아준다.
단일 노드에서 분산 원자적 커밋으로
단일 데이터베이스 노드에서 실행되는 원자성은 흔히 저장소 엔진에서 구현된다. 커밋 레코드를 디스크 로그에 추가하기 때문에 만약 트랜잭션 도중에 데이터베이스가 죽더라도 재시작 시 로그를 통해 복구가 가능하다. (죽기 전에 레코드가 디스크에 쓰였다면 커밋으로 간주하고, 아니라면 롤백)
즉, 단일 노드에서 트랜잭션 커밋은 순서에 결정적으로 의존한다. 트랜잭션이 커밋되거나 어보트되는 시점은 데이터를 먼저 쓰고, 커밋 레코드 쓰기를 마치는 시점이다.
하지만 트랜잭션에 여러 노드가 관여한다면 더 복잡하다. 여러 노드에 커밋 요청을 보내고 각 노드에서 독립적으로 커밋하는 것으론 충분하지 않다.
- 어떤 노드들은 제약 조건 위반이나 충돌 감지를 통해 어보트가 필요하지만 다른 노드들은 성공적으로 커밋될 수 있다.
- 어떤 커밋 요청은 네트워크에서 손실되어 타임아웃 때문에 결국 어보트되지만 다른 커밋 요청은 전달될 수 있다.
- 어떤 노드는 커밋 레코드가 완전히 쓰여지기 전에 죽어서 복구할 때 롤백되지만 다른 노드는 성공적으로 커밋될 수 있다.
트랜잭션 커밋은 되돌릴 수 없어야 한다. 왜냐하면 데이터가 커밋되면 다른 트랜잭션에 데이터가 보이게 되고, 다른 클라이언트들은 그 데이터에 의존하기 시작할지 모르기 때문이다.
= 되돌릴 수 있어야 한다면 그 데이터를 의존하는 추가 트랜잭션 작업들도 모두 취소되어야 한다.
2단계 커밋 소개
모든 노드가 커밋되거나 어보트되도록 보장하는 알고리즘이다.

2PC를 구현하기 위해서 트랜잭션 관리를 하는 코디네이터가 존재하고, 데이터베이스 노드들은 참여자라고 부른다. 이 때 흐름은
- 애플리케이션이 커밋할 준비가 되면 코디네이터가 각 노드에 준비 요청을 보낸다.
- 모든 참여자가 커밋할 준비가 됐다는 뜻으로 “네”를 응답한다.
- 만약 참여자 중 하나라도 “아니오”를 응답한다면 코디네이터는 모든 노드에 어보트 요청을 보낸다.
- 코디네이터는 모든 참여자에게 커밋 요청을 보낸다.
- 모든 참여자는 실제 커밋을 수행한다.
약속에 관한 시스템
1단계와 마찬가지로 2단계 커밋도 쉽게 손실될 수 있다. 하지만 2PC는 원자성 보장을 해주는가에 대해서는 과정을 자세히 이해해야 한다.
- 트랜잭션 ID 요청: 애플리케이션은 분산 트랜잭션을 시작하기 위해 코디네이터로부터 전역적으로 유일한 트랜잭션 ID를 요청합니다.
- 단일 노드 트랜잭션 시작: 애플리케이션은 참여자(데이터베이스 노드)에게 단일 노드 트랜잭션을 시작하라고 요청하고, 이 트랜잭션에 전역적으로 유일한 트랜잭션 ID를 부여합니다. 이 단계에서 실패하면 트랜잭션이 중단될 수 있습니다.
- 커밋 준비 요청: 애플리케이션이 커밋할 준비가 되면, 코디네이터는 모든 참여자에게 준비 요청을 보냅니다. 만약 준비 요청 중 하나라도 실패하거나 타임아웃되면, 코디네이터는 트랜잭션을 중단(어보트)합니다.
- 참여자의 준비 확인: 참여자는 트랜잭션 데이터를 디스크에 안전하게 저장하고, 충돌이나 제약 조건 위반 없이 트랜잭션을 커밋할 준비가 되었는지 확인합니다. 준비가 완료되면, 참여자는 코디네이터에게 커밋 준비 완료를 알립니다.
- 최종 결정: 코디네이터는 모든 참여자로부터 준비 완료 응답을 받으면 트랜잭션을 커밋할지 어보트할지 최종 결정합니다. 이 결정은 트랜잭션 로그에 기록됩니다.
- 커밋 또는 어보트 실행: 코디네이터의 결정이 디스크에 기록되면, 모든 참여자에게 커밋 또는 어보트 요청을 전송합니다. 요청이 실패하거나 타임아웃되면 코디네이터는 요청을 반복합니다.
따라서 이 프로토콜에는 2개의 중대한 “돌아갈 수 없는 지점”이 있다. 참여자는 “네”에 투표할 때 나중에 분명히 커밋할 수 있을 거라고 약속한다. 그리고 코디네이터가 한 번 결정하면 그 결정은 번복할 수 없다. 이런 약속은 2PC의 원자성을 보장한다.
코디네이터 장애
모든 참여자가 “네”에 투표하고, 코디네이터가 모든 노드에게 커밋 요청을 보냈다면 코디네이터는 커밋됐는지 어보트됐는지 회신을 기다려야 한다.
하지만 이 때 코디네이터가 죽거나 네트워크 장애가 발생하면 모든 참여자는 기다릴 수 밖에 없다. 이 상태에 있는 참여자의 트랜잭션을 의심스럽다(in doubt) 또는 불확실하다(uncertain)고 한다.

여기서 데이터베이스2는 커밋 요청을 받았고, 데이터베이스1에는 커밋 요청을 보내기 전에 코디네이터가 죽었다. 이 때 데이터베이스1은 커밋할지 어보트할지 알지 못한다.
2PC가 완료할 수 있는 유일한 방법은 결국 코디네이터가 복구되기를 기다리는 것 뿐이다. (코디네이터가 참여자들에게 커밋이나 어보트 요청하기 전에 디스크에 있는 트랜잭션 로그에 기록하는 이유)
3단계 커밋
2PC가 코디네이터가 복구하기를 기다리느라 멈출 수 있다는 사실 때문에 블로킹 원자적 커밋 프로토콜이라고 불린다. 3PC는 이론상 노드에 장애가 나도 멈추지 않도록 논블로킹 원자적 커밋을 만들 수 있다.
하지만 노드가 죽었는지 안 죽었는지 구별할 수 있는 신뢰성 있는 완벽한 장애 감지기 메커니즘이 필요하기 때문에 2PC가 계속해서 쓰이고 있다. (구현이 어려움)
현실의 분산 트랜잭션
2PC로 구현된 분산 트랜잭션은 운영상 문제를 일으키고 성능을 떨어뜨리기 때문에 평판이 엇갈리며 여러 클라우드 서비스들은 분산 트랜잭션을 구현하지 않는 선택을 한다.
어떤 분산 트랜잭션 구현은 무거운 성능 손해를 수반하며, Mysql 분산 트랜잭션은 단일 노드 트랜잭션보다 10배 이상 느리다고 보고된다. 따라서 사람들이 분산 트랜잭션을 쓰지 말라고 하는 것도 놀랍지 않다.
그러나 다중 트랜잭션을 완전히 일축하기 보단 여기서 배울 교훈에 집중해야 한다. 우선 분산 트랜잭션이 무엇을 의미하는지 정확해야 한다.
- 데이터베이스 내부 분산 트랜잭션
- 이종 분산 트랜잭션
- 참여자들 간에 둘 이상의 다른 데이터베이스 시스템이 혼합
- 훨씬 어렵다.
정확히 한 번 메시지 처리
이종 분산 트랜잭션은 다양한 시스템들이 강력한 방법으로 통합될 수 있게 한다. 메시지 전달이나 데이터베이스 트랜잭션 중 하나가 실패하면 둘 다 어보트되고 메시지 브로커는 나중에 메시지를 안전하게 다시 전달할 수 있다.
그러므로 처리 과정의 부수 효과를 원자적으로 커밋함으로써 메시지가 결과적으로 정확히 한번 처리됨을 보장할 수 있다. 성공을 위해 여러번 재시도가 있을 수 있지만 어보트는 부분적으로 완료된 트랜잭션의 부수 효과를 폐기한다.
= 하지만 영향을 받는 모든 시스템이 원자적 커밋 프로토콜을 사용할 수 있을 때만 사용이 가능하다. 예를 들어 메시지를 처리하는 부수효과가 이메일 전송이라면 실패나 재시도되면 이메일이 두 번 이상 전송될 수 있다.
XA 트랜잭션
XA/Open XA는 이종 기술에 걸친 2단계 커밋을 구현하는 표준이다. Mysql, Postgresql, DB2, SQL 서버, 오라클, 액티브MQ, 호닛Q, MSQM, IBM MQ등 여러 플랫폼에서 지원이 된다.
XA API를 통해 준비, 커밋 요청 등을 보낼 수 있고, 코디네이터가 장애가 발생 후 복구되도 XA 롤백을 사용해 커밋 혹은 롤백 요청이 가능하다는 듯.
의심스러운 상태에 있는 동안 잠금을 유지하는 문제
트랜잭션이 의심스러운 상태에 빠지는 것을 주의하는 이유는 잠금에 있다. 트랜잭션은 보통 더티 쓰기 방지를 위해 로우 락을 획득하게 된다. 데이터베이스는 트랜잭션이 커밋 or 어보트되기 전까지 잠금을 해제할 수 없는데, 만약 이때 코디네이터가 죽는다면 코디네이터가 복구될 때까지 해당 잠금은 해제되지 못한다.
코디네이터 장애에서 복구하기
코디네이터가 죽은 후 재시작하면 로그로부터 의심스러운 트랜잭션들을 해소해야 하지만, 현실에서는 고아가 된 의심스러운 트랜잭션이 발생할 수 있다. 이런 트랜잭션은 자동으로 해소될 수 없어 잠금을 유지하고 다른 트랜잭션을 차단하면서 데이터베이스에 영원히 남는다. (데이터베이스 서버를 재시작해도 고칠 수 없음)
이 문제의 유일한 해결책은 관리자가 수동으로 트랜잭션을 커밋하거나 롤백할지 결정하는 것 뿐이다.
여러 XA 구현에서는 확정적 결정을 얻지 않고 의심스러운 트랜잭션 해소를 위해 경험적 결정이라 부르는 비상탈출구를 제공한다. 여기서 경험적 결정은 2PC의 약속 체계를 위반하기 때문에 원자성을 깰 수 있음을 완곡하게 표현한 것이다. (= 큰 장애 시에 사용되도록 의도)
분산 트랜잭션의 제약
XA 트랜잭션은 여러 문제를 해결해주지만, 운영상 문제도 가져온다. 핵심 구현은 트랜잭션 코디네이터 자체가 (트랜잭션 결과를 저장할 수 있는) 일종의 데이터베이스여야 한다는 점이고, 따라서 데이터베이스와 동일하게 신경 써서 접근해야 한다.
- 코디네이터가 복제되지 않고 단일 장비에서 실행된다면 단일 장애 지점이 된다. (내결함성 취약)
- 상태가 없다면 스케일아웃이 자유롭지만, 트랜잭션 결과를 저장하게 된다면 이제 스케일아웃이 어렵다.
- XA는 광범위한 데이터 시스템과 호환 가능해야 하므로 최소 공통 분모가 될 필요가 있다.
- 예를 들어 여러 시스템에 걸친 교착 상태를 감지할 수 없고, SSI(직렬성 스냅숏 격리)와 함께 동작하지 않는다. (= 이것들을 구현하려면 잠금 정보 교환, 충돌 식별 프로토콜 등이 필요하다)
- 분산 트랜잭션은 장애를 증폭시키는 경향이 있으며 내결함성을 위한 시스템의 목표에 위반한다.
내결함성을 지닌 합의
합의는 여러 노드가 어떤 것에 동의해야 한다는 뜻이다. 이러한 합의 알고리즘은 4가지 속성을 만족해야 한다.
- 균일한 동의
- 무결성
- 유효성
- 한 노드가 값 v를 결정한다면 v는 어떤 노드에서 제안된 것이다
- 종료
- 죽지 않은 모든 노드는 결국 어떤 값을 결정한다
내결함성이 상관 없다면 처음 3개 속성을 만족시키는건 쉽다. 그냥 한 노드를 독재자로 하드코딩하고 그 노드가 모든 결정을 내리게 하면 된다. (= 하지만 단일 장애 지점)
종료 속성은 내결함성 아이디어를 형식화한다. 그냥 그 상태에 머물러서 영원히 아무것도 안 할 수는 없다. 어떤 노드에 장애가 발생하더라도 다른 노드들은 여전히 결정을 내려야 한다.
합의 시스템 모델은 노드가 죽으면 그 노드가 갑자기 사라져서 영영 돌아오지 않는다고 가정한다.
그리고 합의 알고리즘에는 과반수(정족수)가 동의해야 하기 때문에 견딜 수 있는 장애의 수에는 제한이 있다.
대부분 합의 알고리즘은 비잔틴 결함이 없다고 가정한다. 즉 노드가 프로토콜을 올바르게 따르지 않으면 프로토콜 안정성 속성이 깨질 가능성이 존재한다. (하지만 3분의 1 미만의 노드만 비잔틴 결함이 있다면 비잔틴 결함에도 견고하게 만들 순 있다)
합의 알고리즘과 전체 순서 브로드캐스트
내결함성을 지닌 합의 알고리즘 중 가장 널리 알려진 것은 뷰 스탬프 복제, 팍소스, 라프트, 잽이다.
값의 순차열에 대해 결정해서 전체 순서 브로드캐스트 알고리즘을 만든다. 전체 순서 브로드캐스트는 모든 노드에게 메시지가 정확히 한번, 같은 순서로 전달돼야 함을 상기하자.
단일 리더 복제와 합의
단일 리더 복제에서 리더를 어떻게 선택하느냐에 따라 합의를 걱정할 필요가 생긴다.
독재자 방식(한 노드만 쓰기)의 합의 알고리즘을 사용해서 사람이 직접 개입해서 장애가 발생할 시 다른 리더를 새로운 리더로 선정한다. 하지만 사람의 개입이 필요하기 때문에 합의의 종료 속성을 만족하지 않는다.
어떤 데이터베이스는 기존 리더에 장애가 발생하면 팔로워 하나를 새 리더로 승격하여 자동 리더 선출과 장애 복구를 수행하는데, 이렇게 함으로써 내결함성을 지닌 전체 순서 브로드캐스트에 가까워지고 합의를 해결하는데 가까워진다.
에포크 번호 붙이기와 정족수
리더가 유일하다고 보장하지는 않지만, 더 약한 보장은 가능하다. 이 프로토콜은 에포크 번호, 뷰스탬프는 뷰 번호, 라프트에서는 텀번호를 정의하고 에포크 내에서 리더가 유일하다고 보장 받는다.
리더가 뭔가를 결정하도록 허용하기 전에 충돌되는 결정을 할지 모르는 에포크 번호가 더 높은 다른 리더가 없는지 먼저 확인한다. 리더는 자신이 다른 노드에 의해 쫓겨나지 않았음을 정족수로부터 투표를 받아 결정한다.
리더는 내리는 모든 결정에 제안된 값을 다른 노드에 보내서 정족수가 그 제안을 찬성한다고 응답하기를 기다려야 한다. (따라서 리더 선출 과정에 2번의 투표가 있다. 리더 선출과 리더의 제안에 투표)
합의의 제약
합의 알고리즘은 내결함성을 유지하며 안전성 속성과 원자적 연산 등 많은 이점이 있지만 모든 곳에 쓰이진 않는다.(트레이드 오프)
- 제안이 결정되기 전 투표하는 방식은 일종의 동기식 복제이며 장애 복구 시 커밋된 데이터가 손실될 수 있다.
- 엄격한 과반수가 동작하기를 요구하며, 대부분 합의 알고리즘은 투표에 참여하는 노드 집합이 고정돼있다고 가정한다. 즉 클러스터에 노드를 동적으로 추가하거나 제거할 수 없다는 뜻이 된다.
- 합의 알고리즘의 동적 멤버십 확장은 동적으로 추가나 제거됨을 보장하지만 이해하기 어렵다.
- 합의 시스템은 장애 노드 감지를 위해 일반적으로 타임아웃에 의존한다.
- 일시적 네트워크 지연 때문에 리더에 장애가 발생했음을 오인하여 성능 저하를 유발할 수 있다.
- 따라서 네트워크 문제에 민감하다
멤버십과 코디네이션 서비스
주키퍼나 etcd같은 프로젝트는 데이터베이스와 유사해보이는데, 왜 합의 알고리즘 구현에 노력을 다하는 것일까?
주키퍼와 etcd는 완전히 메모리 안에 들어올 수 있는 작은 양의 데이터를 보관하도록 설계됐다. 이 소량의 데이터는 내결함성을 지닌 전체 순서 브로드캐스트 알고리즘으로 사용해 모든 노드에 걸쳐 복제된다.
주키퍼는 구글의 처비 잠금 서비스를 모델로 다른 흥미로운 기능 집합도 구현한다.
- 선형성 원자적 연산
- 연산의 전체 순서화
- 장애 감지
- 변경 알림
이 기능 중 오직 선형성 원자적 연산만 실제로 합의가 필요하다.
작업을 노드에 할당하기
주키퍼나 처비 모델은 리더 선출, 작업 스케줄링, 파티션 재균형화와 같은 분산 시스템 관리 작업에 유용하다.
이들은 원자적 연산, 임시 노드, 알림 기능을 활용해 자동 결함 복구를 가능하게 하며, 주키퍼는 고정된 수의 노드에서 실행되며, 이를 통해 대규모 클라이언트 지원과 효율적인 합의 달성이 가능하다.
주키퍼는 느리게 변하는 데이터 관리에 적합하며, 빠르게 변하는 애플리케이션 상태 저장용이 아니며 상태 복제가 필요한 경우 다른 도구를 고려해야 한다.
서비스 찾기
가상 장비가 흔하게 사용되는 클라우드 환경에서는 IP 주소를 사전에 알지 못할 때가 자주 있다. 이 때 DNS를 통해 서비스 이름으로 IP 주소를 찾을 수 있다. 이 때 좋은 성능과 가용성 보장을 위해 다층 캐시를 사용한다.
서비스 찾기는 합의가 필요 없지만, 리더 선출은 필요하며, 합의 시스템이 누가 리더인지 이미 안다면 다른 서비스들이 리더가 누구인지 찾는데 그 정보를 사용하는 것은 타당하다. 이를 위해 어떤 합의 시스템은 읽기 전용 캐시 복제 서버를 지원한다.
이 복제 서버는 합의 알고리즘의 모든 결정에 능동적으로 참여하진 않지만, 선형적일 필요가 없는 읽기 요청을 서비스할 수 있다.
멤버십 서비스
주키퍼와 유사 프로젝트들은 오랜 멤버십 서비스 연구 역사의 일부로 볼 수 있다.
멤버십 서비스는 클러스터 내 노드가 현재 활성화된 살아 있는 멤버인지 결정한다. 여전히 실제로 노드가 살아있지만 합의에 의해 죽은 것으로 잘못 선언될 가능성은 존재하지만, 합의는 시스템에서 어떤 노드가 현재 멤버십을 구성하는지 동의하는 데 매우 유용하다.