17. 읽기 복제본 지연과 read-your-writes 일관성 위반 (C#)
난이도 하내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 읽기 복제본 지연과 read-your-writes 일관성 위반 (C#)
난이도: 하
요약
쓰기는 프라이머리에, 읽기는 비동기 복제본에 보내는데, 방금 자기가 쓴 값을 곧바로 복제본에서 읽는다. 복제 지연(replication lag) 때문에 (D)의 조회는 (C)의 UPDATE 가 아직 전파되지 않은 옛 잔액을 돌려줄 수 있다. 사용자는 충전했는데 잔액이 그대로거나 덜 오른 화면을 본다(read-your-writes 일관성 위반).
문제점
1) [복제 지연] 방금 쓴 값을 복제본에서 읽음 (증상: stale 잔액)
- 증상: (C)
ChargeGold가 프라이머리에 UPDATE 를 커밋해도, 비동기 복제는 수십~수백 ms(부하 시 더) 지연된다. 그 사이 (D)GetBalanceForResponse가 복제본을 조회하면 충전 전 값을 읽는다. 클라이언트는 "충전이 안 됐다"고 오인해 재충전·문의를 유발한다. - 재현조건: 쓰기 직후(복제 지연 시간 이내)의 읽기, 특히 복제 지연이 큰 순간.
- 근본원인: "자기가 쓴 세션의 후속 읽기"를 복제본으로 보냈다. read-your-writes 보장이 필요한 읽기와, 일반 조회(약한 일관성 허용)를 구분하지 않았다.
2) [설계] 일관성 요구가 라우팅에 반영되지 않음 (증상: 산발적/재현난)
- 증상: 지연은 항상 나는 게 아니라 부하·네트워크에 따라 가변적이라 QA 에서 안 잡히고 프로덕션에서 간헐 발생한다. 어떤 읽기가 강한 일관성을 요구하는지가 코드에 표현되지 않았다.
수정안
방금 쓴 값을 즉시 보여줘야 하는 읽기는 프라이머리로 라우팅(또는 반환값 활용).
public async Task<long> ChargeAndReturnBalance(long userId, long amount)
{
// 방법 1: 쓰기와 같은 커넥션(프라이머리)에서 바로 최신값을 돌려받는다.
long newBalance = await _primary.QueryScalarAsync(
"UPDATE users SET gold = gold + @amt WHERE id = @id RETURNING gold", // PG: RETURNING
amount, userId);
return newBalance;
}
// 강한 일관성이 필요한 읽기는 명시적으로 프라이머리로
public Task<long> GetBalanceStrong(long userId) =>
_primary.QueryScalarAsync("SELECT gold FROM users WHERE id=@id", userId);
// 일반(약한 일관성 허용) 조회만 복제본
public Task<long> GetBalanceEventual(long userId) =>
_replica.QueryScalarAsync("SELECT gold FROM users WHERE id=@id", userId);
- 가장 견고:
UPDATE ... RETURNING(또는 트랜잭션 내 재조회)으로 쓰기 결과를 그 자리에서 받으면 별도 읽기 자체가 불필요. - 일관성 요구를 API 로 분리(
...Strongvs...Eventual)해 호출자가 라우팅 의도를 명시.
더 나은 설계 (트레이드오프)
- write-then-read 세션 고정(sticky): 쓰기 후 짧은 시간(복제 지연 상한) 동안 그 사용자의 읽기를 프라이머리로 고정. 트레이드오프: 프라이머리 부하 소폭 증가, 지연 상한 추정 필요.
- 복제 위치(LSN/GTID) 대기: 쓰기 시 얻은 복제 위치를 읽기 시 복제본이 그 지점까지 따라잡았는지 확인 후 조회(예: PostgreSQL
pg_wait_for_replay_lsn류, MySQL GTID). 강한 read-your-writes 를 복제본에서도 보장. 트레이드오프: 지연·구현 복잡도. - 읽기 후 자기 보정: 응답에 클라이언트가 계산한 예상 잔액을 쓰고, 복제 반영 후 정정. 트레이드오프: UX 복잡.
면접 포인트
- 비동기 복제의 read-your-writes/monotonic-read 일관성 문제와 복제 지연의 가변성.
- 어떤 읽기가 강한 일관성을 요구하고 어떤 읽기가 eventual 을 허용하는지 구분해 라우팅하는 설계.
UPDATE ... RETURNING, 세션 고정, 복제 위치(LSN/GTID) 대기 같은 해법과 각 트레이드오프.
해설 · C++
해설 — 읽기 복제본 지연과 read-your-writes 일관성 위반 (C++)
난이도: 하
요약
쓰기는 프라이머리에, 읽기는 비동기 복제본에 보내는데 방금 자기가 쓴 값을 곧바로 복제본에서 읽는다. 복제 지연 때문에 (D)의 조회가 (C)의 UPDATE 를 아직 못 본 옛 잔액을 돌려줄 수 있다. 사용자는 충전 후에도 갱신 전 잔액을 본다(read-your-writes 위반). 언어와 무관한 아키텍처 결함으로, C# 판과 동일한 논리다.
문제점
1) [복제 지연] 방금 쓴 값을 복제본에서 읽음 (증상: stale 잔액)
- 증상: (C)
ChargeGold가 프라이머리에 커밋해도 복제본 반영은 수십~수백 ms 지연된다. (D)GetBalanceForResponse가 그 사이 복제본을 조회하면 충전 전 값을 읽는다. - 재현조건: 쓰기 직후(복제 지연 시간 이내) 읽기.
- 근본원인: read-your-writes 를 요구하는 읽기를 복제본으로 라우팅. 강한 일관성 읽기와 eventual 읽기를 구분하지 않음.
2) [설계] 일관성 요구가 라우팅에 표현되지 않음 (증상: 간헐·재현난)
- 복제 지연은 부하·네트워크에 따라 가변적이라 QA 에서 놓치기 쉽고 프로덕션에서 산발 발생.
수정안
방금 쓴 값을 즉시 보여줄 읽기는 프라이머리로(또는 쓰기 결과를 그 자리에서 회수).
struct IDb {
virtual int64_t ExecuteReturning(const std::string& sql, int64_t a, int64_t b) = 0;
virtual int64_t QueryScalar(const std::string& sql, int64_t id) = 0;
virtual ~IDb() = default;
};
class GoldService {
IDb& primary_; IDb& replica_;
public:
GoldService(IDb& p, IDb& r): primary_(p), replica_(r) {}
// 쓰기와 같은 프라이머리 커넥션에서 최신값을 바로 회수 → 별도 읽기 불필요
int64_t ChargeAndReturnBalance(int64_t userId, int64_t amount) {
return primary_.ExecuteReturning(
"UPDATE users SET gold = gold + ? WHERE id = ? RETURNING gold",
amount, userId);
}
// 강한 일관성 조회: 프라이머리
int64_t GetBalanceStrong(int64_t id) {
return primary_.QueryScalar("SELECT gold FROM users WHERE id=?", id);
}
// eventual 허용 조회: 복제본
int64_t GetBalanceEventual(int64_t id) {
return replica_.QueryScalar("SELECT gold FROM users WHERE id=?", id);
}
};
UPDATE ... RETURNING(또는 트랜잭션 내 재조회)으로 쓰기 결과를 그 자리에서 받아 stale 읽기 자체를 없앤다.- 일관성 요구를 API 로 분리(
...Strong/...Eventual)해 라우팅 의도를 명시.
더 나은 설계 (트레이드오프)
- write-then-read 세션 고정(sticky): 쓰기 후 복제 지연 상한 동안 그 사용자 읽기를 프라이머리로 고정. 트레이드오프: 프라이머리 부하 증가.
- 복제 위치(LSN/GTID) 대기: 쓰기 시 얻은 복제 위치까지 복제본이 따라잡았는지 확인 후 복제본 조회. 복제본에서도 강한 read-your-writes. 트레이드오프: 지연·복잡도.
- 캐시 계층 주의: 로컬 read-through 캐시도 같은 함정 — 쓰기 시 무효화/갱신을 프라이머리 커밋과 함께 처리.
면접 포인트
- 비동기 복제의 read-your-writes/monotonic-read 문제와 복제 지연의 가변성.
- 강한 일관성 읽기 vs eventual 읽기를 구분해 라우팅하는 설계.
UPDATE ... RETURNING, sticky 라우팅, LSN/GTID 대기의 트레이드오프.