24. 쿼럼 미충족(R+W≤N)과 버전 무시로 인한 stale 읽기
난이도 최상내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 쿼럼 미충족(R+W≤N)과 버전 무시로 인한 stale 읽기
난이도: 최상
요약
W 개 ack 로 쓰기를 성공 처리하고 (A), R 개 응답 중 첫 응답을 그대로 반환 (B) 한다. 게다가
N=3, W=1, R=1 이면 읽기 쿼럼과 쓰기 쿼럼이 겹치지 않을 수 있어(R+W≤N), 방금 쓴 값을 못 본 복제본을
읽어 예전 값이 나온다. 버전 비교도 없어 최신 값을 골라내지도 못한다.
문제점
- [쿼럼 교집합 부재] R+W ≤ N: 강한 읽기 일관성은 읽기 쿼럼과 쓰기 쿼럼이 최소 한 복제본에서 겹쳐야
성립한다 → 조건
R + W > N.N=3, W=1, R=1이면1+1 ≤ 3이라 교집합이 보장되지 않는다. 쓰기가 복제본 A 에만 반영되고, 읽기가 B 를 고르면 옛 값을 본다.- 재현: 인벤토리에 아이템 추가(version=5)가 A 에만 ack 되어 성공 반환 → 곧이어 읽기가 B(version=4)를 선택 → 방금 넣은 아이템이 사라진 것처럼 보인다(원복). 다음 사용자 행동이 stale 상태 위에서 진행돼 손실·이중 지급으로 번진다.
- [버전 무시] 첫 응답 반환:
(B)는responses[0]을 반환한다. 여러 응답을 받아도 버전이 가장 높은 값을 고르지 않으므로, 우연히 오래된 복제본이 먼저 응답하면 그 값이 나간다. - [수렴 부재] 읽기 수리(read-repair) 없음: stale 복제본을 최신으로 되돌리는 수리가 없어 불일치가 오래 지속된다.
- [write 방식] 순차 대기:
(A)는 태스크를 순서대로await하며 첫 W 개를 세지만, 이는 "가장 먼저 끝난 W 개" 가 아니라 "리스트 앞쪽 W 개" 라 느린 복제본에 걸리면 지연이 커진다(부차적). - 재현 조건: 복제 지연이 있는 상시 상황. W/R 을 낮춰 지연·가용성을 얻으려 할 때 흔히 실수한다.
수정안
R + W > N 을 만족시키고(예: N=3 이면 W=2, R=2), 읽기는 R 응답 중 최고 버전을 골라 반환하며,
뒤처진 복제본에 최신 값을 되써서 수렴시킨다(read-repair).
public QuorumStore(IReplica[] replicas)
{
_replicas = replicas;
_w = replicas.Length / 2 + 1; // 과반
_r = replicas.Length / 2 + 1; // R + W = 2*(⌊N/2⌋+1) > N 보장
}
public async Task<Versioned> ReadAsync(string key)
{
var tasks = _replicas.Select(rep => rep.GetAsync(key)).ToList();
var responses = new List<Versioned>();
while (responses.Count < _r && tasks.Count > 0)
{
var done = await Task.WhenAny(tasks); // 가장 먼저 온 응답부터
tasks.Remove(done);
responses.Add(await done);
}
var newest = responses.OrderByDescending(v => v.Version).First(); // 최고 버전 선택
// read-repair: 뒤처진 복제본에 최신 값 되쓰기(비동기 best-effort)
_ = RepairAsync(key, newest);
return newest;
}
더 나은 설계
- 일관성 요구에 따라 W/R 선택: 강한 읽기는
R+W>N. 쓰기 지연을 줄이려면W를 낮추고R을 높이거나, 반대로 읽기 지연을 줄이려면R=1, W=N. 트레이드오프를 명시적으로 고른다. - 동시 쓰기 충돌 처리: 단조 버전만으로는 동시 쓰기(같은 버전)를 구분 못 한다. 버전 벡터/타임스탬프+ 노드ID 타이브레이커(LWW) 또는 CRDT/충돌 병합으로 다룬다.
- 읽기 수리 + 안티엔트로피: read-repair 로 핫 키를, 백그라운드 Merkle 트리 동기화로 콜드 키를 수렴시킨다.
- 선형화가 꼭 필요하면: 쿼럼 저장소 대신 합의(Raft/Paxos) 기반 저장소나 리더 기반 강한 일관성을 쓴다 (인벤토리/재화처럼 손실이 치명적인 데이터).
- 트레이드오프:
R+W>N은 일관성을 주지만 가용성(한 복제본 장애 시 쿼럼 미달)과 지연이 나빠진다 — CAP/PACELC 상의 선택.
면접 포인트
- 쿼럼 일관성 조건
R+W>N의 의미(읽기·쓰기 쿼럼의 교집합 보장)와 왜 버전 최댓값 선택이 필요한가. - read-repair·안티엔트로피로 수렴시키는 방법, 동시 쓰기의 LWW/버전 벡터 처리.
- 최종 일관성 쿼럼 vs 합의 기반 강한 일관성의 트레이드오프(가용성·지연·데이터 민감도).
해설 · C++
해설 — 버전 최댓값을 골라도 R+W≤N 이면 stale: 쿼럼 교집합 부재
난이도: 최상
요약
읽기가 R 응답 중 버전 최댓값을 고르므로 (B) 자체의 선택 로직은 옳다. 그럼에도 (A) 의 W 와
읽기의 R 이 R + W ≤ N 이면 읽기 쿼럼이 방금 쓴 복제본과 겹치지 않을 수 있어, 최신 값을 가진
복제본이 애초에 R 응답 집합에 들어오지 않아 예전 값이 최댓값으로 뽑힌다. 근본 원인은 선택 로직이
아니라 쿼럼 교집합 조건이다.
문제점
- [쿼럼 교집합 부재] R+W ≤ N 이면 최댓값 선택도 무력: 강한 읽기 일관성은
R + W > N일 때만 읽기·쓰기 쿼럼의 교집합이 보장된다.N=3, W=1, R=1(또는W=1, R=2)이면 교집합이 없을 수 있다.- 재현(N=3, W=1, R=2): 쓰기(version=5)가 복제본 C 에만 ack 되고 성공 반환. 읽기가 먼저 응답한
A(v=4), B(v=4) 두 개로 R=2 를 채우면, C 는 응답 집합에 없다.
max_element는max(4,4)=4를 고른다 → 방금 쓴 version=5 를 못 본다. 버전 비교가 정확해도 최신 복제본이 표본에 없으면 소용없다. - 즉
(B)를 아무리 잘 만들어도(A)의W와R이 교집합을 보장하지 않으면 stale 이 난다.
- 재현(N=3, W=1, R=2): 쓰기(version=5)가 복제본 C 에만 ack 되고 성공 반환. 읽기가 먼저 응답한
A(v=4), B(v=4) 두 개로 R=2 를 채우면, C 는 응답 집합에 없다.
- [가용성 착시] 낮은 W 의 위험:
W=1은 한 복제본만 받으면 성공이라 빨라 보이지만, 그 복제본이 이후 장애 나면 확인된 쓰기가 유실될 수도 있다(내구성 저하). - [수렴 부재] 읽기 수리 없음: stale 복제본을 되돌리는 수리가 없어 불일치가 지속된다.
- [동시 쓰기] 버전 동률: 서로 다른 클라가 같은 버전으로 동시에 쓰면
version <비교가 둘을 구분 못 해 병합 규칙이 필요하다. - 재현 조건: 복제 지연이 있는 상시 상황. 지연/가용성을 위해 W·R 을 낮출 때 흔한 실수.
수정안
R + W > N 을 만족시키고(과반 쿼럼), 최댓값 선택은 유지하되 응답 표본이 반드시 최신 쓰기와 겹치도록
한다. 아울러 read-repair 로 수렴시킨다.
// 과반 쿼럼: N=3 이면 W=2, R=2 → R+W=4 > 3
QuorumStore(std::vector<IReplica*> reps)
: replicas_(std::move(reps)),
w_((int)replicas_.size()/2 + 1),
r_((int)replicas_.size()/2 + 1) {}
Versioned read(const std::string& key) {
std::vector<Versioned> responses;
for (auto* rep : replicas_) { // 실제로는 병렬 요청 후 먼저 온 R 개
responses.push_back(rep->get(key));
if ((int)responses.size() >= r_) break;
}
auto newest = *std::max_element(responses.begin(), responses.end(),
[](const Versioned& a, const Versioned& b){ return a.version < b.version; });
// read-repair: 뒤처진 복제본에 newest 를 best-effort 로 되쓴다.
return newest;
}
과반(W=R=⌊N/2⌋+1)이면 어떤 읽기 쿼럼도 어떤 쓰기 쿼럼과 최소 한 복제본에서 겹치므로, 최신 값을 가진
복제본이 반드시 표본에 포함되고 max_element 가 그것을 고른다.
더 나은 설계
- 일관성 요구에 맞춘 W/R: 강한 읽기는
R+W>N. 읽기 지연 우선이면R=1,W=N, 쓰기 지연 우선이면W=1,R=N— 단, 각각의 가용성/지연 비용을 이해하고 고른다. - 버전 벡터 / LWW 타이브레이커: 동시 쓰기 충돌을 노드ID·물리시계로 정렬하거나 CRDT 로 병합한다.
- read-repair + 안티엔트로피(Merkle): 핫 키는 읽기 시, 콜드 키는 백그라운드로 수렴.
- 선형화가 필요하면: 합의(Raft/Paxos) 기반 저장소나 리더 기반 강한 일관성으로 — 인벤토리·재화처럼 손실이 치명적인 데이터에 적합.
- 트레이드오프:
R+W>N은 일관성을 주지만 한 복제본 장애 시 쿼럼 미달 위험과 지연 증가(CAP/PACELC).
면접 포인트
- "버전 최댓값을 고르는 정확한 읽기" 조차
R+W≤N이면 왜 stale 인가 — 쿼럼 교집합이 진짜 조건임. - 과반 쿼럼이 교집합을 보장하는 원리와 read-repair/안티엔트로피.
- 최종 일관성 쿼럼 vs 합의 기반 강한 일관성의 트레이드오프.