26. 읽기 복제본 분산과 세션 일관성(비단조 읽기) (C#)
난이도 상내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 읽기 복제본 분산과 세션 일관성(비단조 읽기) (C#)
난이도: 상
요약
읽기를 복제본에 무상태 라운드로빈으로 흩뿌리기 때문에, 한 세션의 연속 조회가 복제 지연이 다른 복제본들을 오간다. 그 결과 방금 본(또는 방금 쓴) 값보다 과거 값이 다시 보이는 비단조 읽기(monotonic reads 위반) 가 발생한다. 라우터가 세션이 지금까지 관측한 버전을 추적하지 않고(B), 그 버전 이상을 반영한 복제본을 고르지도 않는다(A).
문제점
- (A) 세션 무관 라운드로빈 라우팅:
_next커서로 요청마다 다른 복제본을 고른다. 세션/플레이어와 무관하므로 같은 세션의 두 연속 조회가 최신 복제본 → 뒤처진 복제본 순서로 갈 수 있다.- 재현: 점수 100→150 갱신 직후, 1차 조회가 최신 복제본(150)에, 2차 조회가 지연 복제본(아직 100)에 도달 → 사용자에게 점수가 150→100 으로 거꾸로 보인다.
- 근본 원인: 읽기 라우팅이 세션의 일관성 요구를 고려하지 않음.
- (B) 관측 버전 추적/하한 부재:
복제본이
version을 돌려주는데 라우터가 이를 버리고, "이 세션이 지금까지 본 최대 버전"을 저장하지도, "그 버전 이상인 복제본"을 고르지도 않는다. 그래서 stale 복제본을 걸러낼 수단이 없다.
수정안
세션이 관측한 최대 버전을 워터마크로 들고, 그 이상 따라잡은 복제본만 고른다(없으면 주로 폴백). 또는 세션을 특정 복제본에 스티키로 고정한다.
public sealed class ScoreReadRouter
{
private readonly List<IReplica> _replicas;
private readonly IReplica _primary;
// 세션별 마지막으로 관측한 버전(단조 워터마크).
private readonly Dictionary<long, long> _seen = new();
public long GetScore(long sessionId, long playerId)
{
long watermark = _seen.TryGetValue(sessionId, out var v) ? v : 0;
foreach (var r in PickCandidates(sessionId)) // 세션 스티키 순서 등
{
var (score, version) = r.Read(playerId);
if (version >= watermark) // 관측 버전 이상만 채택
{
_seen[sessionId] = version; // 워터마크 전진(단조)
return score;
}
}
// 어떤 복제본도 못 따라잡았으면 주(primary)에서 읽어 단조성 보장.
var (ps, pv) = _primary.Read(playerId);
_seen[sessionId] = pv;
return ps;
}
}
대안: read-your-writes 가 필요한 창(쓰기 직후 수 초)에는 그 세션 읽기를 주로 보내거나
스티키 복제본으로 고정. (_seen 은 동시 접근 시 적절히 보호/샤딩.)
더 나은 설계
- 세션 일관성 보장 명시: monotonic reads / read-your-writes 를 명시적 워터마크(버전 토큰)로 구현. 클라이언트가 마지막 버전 토큰을 들고 다니면 서버 상태 없이도 가능.
- 스티키 라우팅으로 세션을 한 복제본에 고정(복제본 장애 시 재고정 + 워터마크 유지).
- 일관성/성능 트레이드오프 계층화: 랭킹판 브라우징 같은 건 약한 일관성 허용, 내 점수 확인 같은 건 세션 일관성 요구 — 요청별 일관성 등급 부여.
면접 포인트
- 비동기 복제 + 무상태 라운드로빈 = 비단조 읽기. 세션 일관성은 공짜가 아니다.
- monotonic reads / read-your-writes 를 버전 워터마크 또는 스티키 라우팅으로 구현.
- (문제 24의 쿼럼 R+W stale 과 구별: 여기서는 정상 운영 중 복제본 선택으로 인한 세션 일관성 문제이지, 쿼럼 교차 실패가 아니다.)
해설 · C++
해설 — 읽기 복제본 분산과 세션 일관성(비단조 읽기) (C++)
난이도: 상
요약
읽기를 복제본에 무상태 라운드로빈으로 분산해, 한 세션의 연속 조회가 복제 지연이 다른 복제본을 오간다. 방금 본(또는 쓴) 값보다 과거 값이 다시 보이는 비단조 읽기(monotonic reads 위반) 가 발생한다. 라우터가 세션의 관측 버전을 추적하지 않고(B), 그 버전 이상을 반영한 복제본을 고르지도 않는다(A).
문제점
- (A) 세션 무관 라운드로빈:
next_커서로 요청마다 다른 복제본을 골라 같은 세션의 두 조회가 최신 → 뒤처진 복제본 순서가 될 수 있다.- 재현: 점수 100→150 갱신 직후, 1차 조회 최신(150), 2차 조회 지연 복제본(100) → 사용자에게 150→100 으로 거꾸로 보인다.
- 근본 원인: 읽기 라우팅이 세션 일관성 요구를 무시.
- (B) 관측 버전 하한 부재: 복제본이
version을 주는데 이를 버리고, "세션이 본 최대 버전 이상인 복제본" 선택 로직이 없어 stale 복제본을 걸러낼 수 없다.
수정안
세션이 관측한 최대 버전을 워터마크로 들고 그 이상 따라잡은 복제본만 고른다(없으면 주로 폴백).
#include <unordered_map>
struct IReplica { virtual ~IReplica()=default; virtual ReadResult read(int64_t)=0; };
class ScoreReadRouter {
public:
ScoreReadRouter(std::vector<IReplica*> replicas, IReplica* primary)
: replicas_(std::move(replicas)), primary_(primary) {}
int64_t getScore(int64_t sessionId, int64_t playerId) {
int64_t watermark = 0;
auto it = seen_.find(sessionId);
if (it != seen_.end()) watermark = it->second;
for (IReplica* r : candidatesFor(sessionId)) { // 스티키 순서 등
ReadResult res = r->read(playerId);
if (res.version >= watermark) { // 관측 버전 이상만 채택
seen_[sessionId] = res.version; // 워터마크 전진(단조)
return res.score;
}
}
ReadResult p = primary_->read(playerId); // 못 따라잡으면 주에서 읽음
seen_[sessionId] = p.version;
return p.score;
}
private:
std::vector<IReplica*> candidatesFor(int64_t) { return replicas_; }
std::vector<IReplica*> replicas_;
IReplica* primary_;
std::unordered_map<int64_t,int64_t> seen_; // 세션별 관측 버전(적절히 보호)
};
대안: read-your-writes 창(쓰기 직후 수 초)에는 해당 세션 읽기를 주로 보내거나 스티키 복제본에 고정.
더 나은 설계
- 세션 일관성 명시: monotonic reads / read-your-writes 를 버전 워터마크(토큰)로 구현. 클라가 마지막 버전 토큰을 들고 다니면 서버 상태 최소화.
- 스티키 라우팅 + 복제본 장애 시 재고정·워터마크 유지.
- 요청별 일관성 등급: 랭킹판 브라우징은 약한 일관성, 내 점수 확인은 세션 일관성.
면접 포인트
- 비동기 복제 + 무상태 라운드로빈 = 비단조 읽기.
- monotonic reads/read-your-writes 를 워터마크·스티키로 구현하는 방법.
- (문제 24 쿼럼 R+W stale 과 구별: 정상 운영 중 복제본 선택의 세션 일관성 문제.)