1. 마스터-읽기복제본 복제 지연(read-your-writes 위반)
난이도 상해설 — 마스터-읽기복제본 복제 지연(read-your-writes 위반)
난이도: 상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
UpdateAndEcho 가 마스터에 쓴 직후 복제본에서 다시 읽는다(A). 복제는 비동기라 그 시점
복제본엔 아직 옛 값이 있어 방금 쓴 값을 못 읽는다(read-your-writes 위반) — 닉네임을 바꿨는데
직후 응답엔 옛 닉네임이 나온다. 더 나쁜 건 (B) 그 stale 값을 캐시에 저장해, 복제가 따라잡은
뒤에도 캐시 TTL 동안 옛 값이 계속 반환된다(stale 고착). (C) GetProfile 의 캐시미스 read-through
도 복제본을 읽어 같은 stale 을 캐시에 굳힐 수 있다. 정답 한 줄: 방금 쓴 직후의 읽기는 마스터로
보내거나(read-your-writes), 복제 위치(LSN/버전)를 확인해 따라잡힌 복제본에서만 읽고, 캐시는
쓴 값으로 직접 갱신(write-through)하거나 무효화한다.
변별: concurrency_memory/problem13(캐시-DB 이중 쓰기의 일관성 — 같은 노드 캐시/DB 동기 갱신 순서)과 달리, 본 문제는 비동기 복제 지연으로 인한 read-your-writes 위반과 그 stale 의 캐시 고착이 핵심이다(마스터/복제본 분리 토폴로지).
문제점
(A) 쓰기 직후 복제본 읽기 — read-your-writes 위반 (정합/일관성) ★간판
- 분류 태그: replication-lag / stale-read.
- 증상:
WriteMaster직후ReadReplica는 복제 지연(수십~수백 ms) 동안 옛 값을 돌려줄 수 있다. 사용자가 방금 바꾼 값을 자기 화면에서 못 보는 전형적 일관성 버그. 변경 성공 응답에 옛 값이 실려 "저장이 안 됐나?" 오해를 부른다. - 재현조건: write 직후, 복제가 따라잡기 전(고부하·지연 시 흔함)에 같은 키를 복제본에서 읽음.
- 근본 원인: 비동기 복제 토폴로지에서 "자기 쓰기" 직후 읽기를 복제본으로 보냄.
(B) stale 값 캐싱 — stale 고착 (정합/캐시) ★간판
- 분류 태그: cache-poisoning-with-stale.
- 증상: (A)로 읽은 옛 값을
_cache에 저장한다. 복제가 곧 따라잡아도 캐시는 갱신되지 않아 TTL/무효화 전까지 계속 옛 값을 반환한다. 짧은 불일치가 장기 불일치로 증폭된다. - 근본 원인: "쓴 값" 이 아니라 "복제본에서 읽은(신뢰 못 할) 값" 으로 캐시를 채운다.
(C) read-through 캐시미스 경합 — stale 재고착 (정합)
- 증상:
GetProfile캐시미스 시 복제본을 읽는데, 그 직전 다른 경로가 마스터에 새 값을 썼다면 복제 지연으로 옛 값을 읽어 다시 캐시에 굳힐 수 있다. - 근본 원인: 캐시 채움의 소스가 지연된 복제본이고, 일관성 토큰/버전 확인이 없다.
(참고) _cache 동시성
Dictionary캐시가 여러 요청 스레드에서 동시 접근되면 손상/예외. 동시성 컬렉션/락 필요.
수정안
핵심: ① 방금 쓴 직후 읽기는 마스터 또는 버전 확인된 복제본, ② 캐시는 쓴 값으로 직접 갱신(write-through) 또는 무효화, ③ read-your-writes 윈도우 관리.
public string UpdateAndEcho(long playerId, string key, string value)
{
_db.WriteMaster(playerId, key, value);
// (A) 방금 쓴 값은 마스터에서 읽거나(read-your-writes), 애초에 쓴 값 자체를 사용
// 네트워크 한 번 더 아끼려면 'value' 를 그대로 신뢰해도 됨(마스터가 받아들인 값).
string authoritative = value; // 또는 _db.ReadMaster(playerId, key);
// (B) 캐시는 '쓴 값' 으로 직접 갱신(write-through) — stale 을 채우지 않는다
var map = GetOrAddMap(playerId);
map[key] = authoritative;
return authoritative;
}
public string GetProfile(long playerId, string key)
{
if (TryCache(playerId, key, out var v)) return v;
// (C) 캐시미스 read-through: 일반 읽기는 복제본 OK. 단,
// '최근에 이 키를 쓴' 플레이어면 read-your-writes 윈도우 동안 마스터로.
string fromDb = RecentlyWrote(playerId, key)
? _db.ReadMaster(playerId, key)
: _db.ReadReplica(playerId, key);
SetCache(playerId, key, fromDb);
return fromDb;
}
포인트
- "방금 쓴 값" 은 마스터가 수락한
value자체 또는ReadMaster로 — 복제본의 옛 값을 믿지 않는다. - 캐시는 권위 있는 값(쓴 값) 으로 채운다(write-through) → stale 고착 차단.
- read-your-writes 윈도우(예: 쓰기 후 N초, 플레이어/키 단위)에는 읽기를 마스터로 라우팅하거나 복제본의 복제 위치를 확인.
_cache는ConcurrentDictionary/락으로 보호.
더 나은 설계 (+트레이드오프)
- 복제 위치(LSN/GTID) 기반 일관성 읽기: 쓰기가 반환한 LSN 을 들고, 그 LSN 이상 따라잡은 복제본에서만 읽는다(또는 따라잡을 때까지 대기). 트레이드오프: 클라/세션에 LSN 토큰 전파 필요.
- 세션 일관성(sticky to master) 윈도우: 쓰기 직후 일정 시간 그 세션의 읽기를 마스터로. 단순하지만 마스터 부하↑. 윈도우 크기 튜닝 필요.
- write-through / write-around 캐시 정책 명확화: 쓰기 시 캐시를 권위값으로 갱신하거나 무효화. stale 을 절대 캐시에 넣지 않음. 트레이드오프: 캐시 무효화 복잡도.
- 단조 읽기(monotonic reads): 한 사용자가 같은 데이터에 대해 더 옛 값을 보지 않도록 복제본 고정/버전 비교. 트레이드오프: 라우팅 상태 관리.
면접 포인트 (예상 질문)
- read-your-writes 일관성이란? 마스터-복제본 구조에서 왜 깨지고, 어떻게 보장하는가 (마스터 라우팅 / LSN 대기 / 쓴 값 사용)?
- 복제 지연으로 읽은 stale 값을 캐시에 넣으면 왜 "짧은 불일치"가 "긴 불일치"로 커지나? write-through 가 어떻게 막는가?
- 모든 읽기를 마스터로 보내면 정확하지만 왜 안 되나? 복제본 분산과 정확성을 어떻게 절충 하겠는가(윈도우/토큰)?
해설 — 마스터-읽기복제본 복제 지연(read-your-writes 위반) (C++)
난이도: 상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
updateAndEcho 가 마스터에 쓴 직후 복제본에서 다시 읽는다(A). 비동기 복제라 그 시점
복제본엔 옛 값이 있어 방금 쓴 값을 못 읽는다(read-your-writes 위반). 게다가 (B) 그 stale
값을 캐시에 저장해 복제가 따라잡은 뒤에도 한동안 옛 값을 반환한다(stale 고착). (C)
getProfile 의 캐시미스 read-through 도 복제본을 읽어 stale 을 굳힐 수 있다. 정답 한 줄:
방금 쓴 직후 읽기는 마스터로(또는 쓴 값 자체 사용), 캐시는 쓴 값으로 write-through, 복제
위치(LSN) 기반 일관성 읽기를 둔다.
변별: concurrency_memory/problem13(같은 노드 캐시-DB 이중 쓰기 순서)과 달리, 본 문제는 비동기 복제 지연의 read-your-writes 위반 + stale 캐시 고착(마스터/복제본 토폴로지)이 핵심.
문제점
(A) 쓰기 직후 복제본 읽기 — read-your-writes 위반 (정합) ★간판
- 분류 태그: replication-lag / stale-read.
- 증상:
writeMaster직후readReplica는 복제 지연 동안 옛 값을 반환. 방금 바꾼 값을 자기 응답에서 못 본다. - 근본 원인: 비동기 복제 구조에서 "자기 쓰기" 직후 읽기를 복제본으로 보냄.
(B) stale 값 캐싱 — stale 고착 (정합/캐시) ★간판
- 분류 태그: cache-poisoning-with-stale.
- 증상: (A)로 읽은 옛 값을
cache_에 저장 → 복제가 따라잡아도 무효화 전까지 옛 값 반환. 짧은 불일치가 장기 불일치로 증폭. - 근본 원인: "쓴 값" 이 아니라 "지연된 복제본 값" 으로 캐시를 채움.
(C) read-through 캐시미스 경합 — stale 재고착 (정합)
- 증상:
getProfile캐시미스 시 복제본을 읽어, 직전 마스터 쓰기와 경합하면 옛 값을 다시 캐시에 굳힌다. - 근본 원인: 캐시 채움 소스가 지연된 복제본, 버전/일관성 토큰 없음.
(참고) cache_ 동시성 / 복사 비용
unordered_map캐시가 여러 스레드에서 동시 접근되면 데이터 레이스(rehash/노드). 락 또는 동시성 자료구조 필요.std::string값 복사 비용도 고려(필요 시shared_ptr<const string>).
수정안
핵심: ① 방금 쓴 직후 읽기는 마스터(또는 쓴 값 사용), ② 캐시는 쓴 값으로 write-through, ③ read-your-writes 윈도우/LSN.
std::string updateAndEcho(int64_t playerId, const std::string& key, const std::string& value) {
db_.writeMaster(playerId, key, value);
// (A) 복제본의 옛 값을 믿지 않는다: 마스터가 수락한 value 를 권위값으로 사용
const std::string& authoritative = value; // 또는 db_.readMaster(playerId, key)
// (B) 캐시는 권위값으로 직접 갱신(write-through) — stale 을 넣지 않는다
{
std::lock_guard<std::mutex> g(mu_);
cache_[playerId][key] = authoritative;
}
return authoritative;
}
std::string getProfile(int64_t playerId, const std::string& key) {
{ std::lock_guard<std::mutex> g(mu_);
auto pit = cache_.find(playerId);
if (pit != cache_.end()) { auto kit = pit->second.find(key); if (kit != pit->second.end()) return kit->second; } }
// (C) 캐시미스: 일반 읽기는 복제본 OK. 단, 최근에 쓴 (player,key)면 윈도우 동안 마스터로.
std::string fromDb = recentlyWrote(playerId, key)
? db_.readMaster(playerId, key)
: db_.readReplica(playerId, key);
{ std::lock_guard<std::mutex> g(mu_); cache_[playerId][key] = fromDb; }
return fromDb;
}
(멤버에 std::mutex mu_;, recentlyWrote() 는 최근 쓰기 (player,key)→만료시각 맵으로 판정)
포인트
- 방금 쓴 값은 마스터 수락 value 또는
readMaster— 복제본 옛 값 불신. - 캐시는 권위값으로 write-through → stale 고착 차단.
- read-your-writes 윈도우 동안 마스터 라우팅,
cache_는 락 보호.
더 나은 설계 (+트레이드오프)
- 복제 위치(LSN/GTID) 기반 일관성 읽기: 쓰기가 반환한 LSN 이상 따라잡은 복제본에서만 읽기(또는 대기). 트레이드오프: LSN 토큰 전파.
- 세션 sticky-to-master 윈도우: 쓰기 직후 일정 시간 그 세션 읽기를 마스터로. 단순하나 마스터 부하↑.
- write-through/around 정책 명확화 + 무효화: stale 을 절대 캐시에 넣지 않음.
- 단조 읽기(monotonic reads): 같은 사용자가 더 옛 값을 보지 않게 복제본 고정/버전 비교.
면접 포인트 (예상 질문)
- read-your-writes 일관성이란? 마스터-복제본에서 왜 깨지고 어떻게 보장하나(마스터 라우팅/ LSN 대기/쓴 값 사용)?
- 복제 지연으로 읽은 stale 을 캐시에 넣으면 왜 불일치가 장기화되나? write-through 의 효과는?
- 모든 읽기를 마스터로 보내면 정확하지만 왜 지양하나? 복제본 분산과 정확성의 절충 (윈도우/LSN 토큰)은?