9. 스캐터-개더 부분 응답을 완전 응답으로 오인 + 무경계 팬아웃·집계 경합 (C#)
난이도 상내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 스캐터-개더 부분 응답을 완전 응답으로 오인 + 무경계 팬아웃·집계 경합 (C#)
난이도: 상
요약
전 샤드에 팬아웃해 재화 총량을 합산하는데, (1) catch { } 로 실패 샤드를 조용히 건너뛰고 총합을 "완전한 값"처럼 반환하며, (2) 각 호출에 타임아웃이 없어 느린 샤드 하나가 Task.WhenAll 을 무한정 매달고, (3) total += part 를 여러 Task.Run 이 동기화 없이 수행해 경합으로 갱신 유실이 난다.
문제점
- [부분 결과를 완전으로 오인] (A)
return total;+catch { }- 증상: 샤드가 예외를 던지면
catch {}가 삼켜 그 기여가 0인 채 합산 → 언더카운트된 총량을 글로벌 총량으로 신뢰. 인플레 감시·환수 정책이 잘못 판단된다. - 재현: 샤드 3개 중 1개 다운 → 그만큼 적은 총량이 정상값처럼 반환.
- 근본원인: 응답/전체 샤드 수와 완전성 표시 부재. 결과 타입이 부분 실패를 표현하지 못함.
- 증상: 샤드가 예외를 던지면
- [무경계 대기 / 타임아웃 부재]
await Task.WhenAll(tasks)- 증상: 한 샤드 호출이 hang →
WhenAll이 영원히 완료되지 않아 요청·스레드풀 자원 점유. - 근본원인: 퍼-샤드
CancellationToken/데드라인 부재.
- 증상: 한 샤드 호출이 hang →
- [집계 경합] (B)
total += part;- 증상: 여러
Task.Run이 같은total을 비원자 증가 → 읽고-더하고-쓰는 사이 갱신 유실.Interlocked/락 없음. - 근본원인: 공유 누산기 동기화 부재.
- 증상: 여러
수정안
퍼-샤드 타임아웃 + 성공/실패를 값으로 수집 + 완전성 플래그 명시. 누산은 결과 배열로 모은 뒤 합산.
public readonly record struct AggResult(ulong Total, int Responded, int TotalShards)
{
public bool Complete => Responded == TotalShards;
}
public async Task<AggResult> GlobalCurrencyTotalAsync(TimeSpan perShardTimeout)
{
async Task<(ulong v, bool ok)> QueryOne(IShardClient sc)
{
using var cts = new CancellationTokenSource(perShardTimeout);
try
{
// 클라이언트가 토큰을 받는다면 전달; 아니면 Task.WhenAny 로 타임아웃
var call = sc.QueryCurrencyTotalAsync();
var done = await Task.WhenAny(call, Task.Delay(perShardTimeout, cts.Token));
if (done != call) return (0, false); // 타임아웃 → 미응답
cts.Cancel();
return (await call, true);
}
catch { return (0, false); } // 실패를 값으로 표현
}
var results = await Task.WhenAll(_shards.Select(QueryOne)); // 예외 없이 전부 완료
ulong total = 0; int responded = 0;
foreach (var (v, ok) in results) // 단일 스레드 합산(경합 없음)
if (ok) { total += v; responded++; }
return new AggResult(total, responded, _shards.Count);
}
// 호출부: if (!r.Complete) { /* 권위 결정 보류 또는 '부분/저하'로 표시 */ }
- 샤드가 매우 많으면
SemaphoreSlim으로 유계 동시성 팬아웃. - 공유 누산이 꼭 필요하면
Interlocked.Add로, 단 결과 배열 합산이 더 단순·안전.
더 나은 설계
- 결과 타입에 완전성을 담아라:
(Total, Responded, TotalShards, AsOf). 호출자가 "권위 결정"인지 "대시보드 추정"인지에 따라 다르게 쓰게. 부분값을 완전값처럼 흘리는 게 핵심 결함. - 권위 불변식은 단일 출처: 전역 총량을 정책에 쓰려면 분산 합산보다 중앙 원장/이벤트 집계가 안전. 스캐터-개더는 부분 실패에 취약.
- 타임아웃·서킷브레이커·격벽: 느린 샤드 격리(
distributed_systems/problem7) — 단 여기 강조는 "결과 신뢰성". catch {}금지: 실패를 삼키지 말고 값(ok=false)으로 표현해 집계가 알게 하라.
면접 포인트
- 부분 실패를 0으로 삼키는 위험과, 결과에 완전성/응답 비율을 담는 설계.
Task.WhenAll의 무경계 대기 문제와 퍼-샤드 타임아웃(WhenAny+Delay/토큰) 패턴.- 전역 합산 불변식의 분산 집계 한계와 중앙 원장/이벤트 집계 대안,
Interlockedvs 결과 배열 합산.
해설 · C++
해설 — 스캐터-개더 부분 응답을 완전 응답으로 오인 + 무경계 팬아웃·집계 경합 (C++)
난이도: 상
요약
전 샤드에 팬아웃해 재화 총량을 합산하는데, (1) 일부 샤드가 죽거나 예외를 던지면 그 기여가 조용히 빠진 채 총합을 "완전한 값"으로 반환한다(권위 결정에 쓰면 오판), (2) 각 샤드 호출에 타임아웃이 없어 느린 샤드 하나가 전체를 무한정 매단다, (3) total += part 를 여러 비동기 스레드가 동기화 없이 수행해 데이터 레이스(UB)·집계 손실이 난다.
문제점
- [부분 결과를 완전으로 오인] (A)
return total;- 증상: 한 샤드가
queryCurrencyTotal()에서 예외를 던지면 그 future 에 예외만 저장되고t.wait()는 되던지지 않는다 → 그 샤드 기여가 0인 채 합산. 호출자는 언더카운트된 총량을 "글로벌 총량"으로 믿고 환수/지급 정책을 잘못 내린다. - 재현: 샤드 3개 중 1개 다운 → 총량이 그 샤드만큼 적게 나오는데 정상값처럼 반환.
- 근본원인: "응답한 샤드 수 / 전체 샤드 수"와 완전성(completeness) 표시가 없음. 부분 실패를 결과 타입이 표현하지 못한다.
- 증상: 한 샤드가
- [무경계 대기 / 타임아웃 부재] (A) 부근
t.wait()- 증상: 샤드 하나가 hang 하면
queryCurrencyTotal()가 반환하지 않아t.wait()가 영원히 블록 → 코디네이터 스레드·요청 자원 고갈. 호출이 잦으면 연쇄 지연. - 근본원인: 퍼-샤드 타임아웃·전체 데드라인 부재. (이건
distributed_systems/problem7의 자원 고갈과 닿지만, 여기 핵심은 "그래서 결과가 틀린다"는 정확성.)
- 증상: 샤드 하나가 hang 하면
- [집계 데이터 레이스] (B)
total += part;- 증상: 여러
std::async스레드가 같은total을 비원자로 증가 → 갱신 유실/찢김. C++ 에서 데이터 레이스 = UB. - 근본원인: 공유 누산기에 동기화(원자/락) 없음.
- 증상: 여러
수정안
퍼-샤드 타임아웃 + 결과별 성공/실패 수집 + 완전성 플래그를 명시한다. 누산은 결과 배열로 모은 뒤 합산(또는 원자).
#include <cstdint>
#include <vector>
#include <future>
#include <chrono>
struct AggResult {
uint64_t total = 0;
int responded = 0;
int totalShards = 0;
bool complete() const { return responded == totalShards; }
};
AggResult globalCurrencyTotal(std::vector<ShardClient*>& shards,
std::chrono::milliseconds perShardTimeout) {
struct Item { uint64_t v; bool ok; };
std::vector<std::future<Item>> futs;
for (ShardClient* sc : shards) {
futs.push_back(std::async(std::launch::async, [sc]() -> Item {
try { return { sc->queryCurrencyTotal(), true }; }
catch (...) { return { 0, false }; } // 실패를 값으로 표현
}));
}
AggResult r; r.totalShards = (int)shards.size();
for (auto& f : futs) {
if (f.wait_for(perShardTimeout) != std::future_status::ready)
continue; // 타임아웃 → 미응답으로 집계
Item it = f.get();
if (it.ok) { r.total += it.v; r.responded++; } // 단일 스레드에서 합산(레이스 없음)
}
return r; // (A) 완전성은 r.complete()로 판단
}
// 호출부: if (!r.complete()) { /* 권위 결정 보류 또는 '부분/저하' 표시 */ }
- 타임아웃은
wait_for로 퍼-future. 전체 데드라인이 필요하면 남은 예산을 깎아가며 적용. - 샤드 수가 매우 많으면 무제한
std::async대신 유계 동시성(스레드풀/세마포어)으로 팬아웃.
더 나은 설계
- 결과 타입에 완전성을 담아라:
total만 주지 말고(total, responded, totalShards, asOf)를 주고, 호출자가 "권위 결정"인지 "대시보드 추정"인지에 따라 다르게 쓰게 한다. 부분값을 완전값처럼 흘리는 게 핵심 결함. - 권위 불변식은 단일 출처: "전 서버 재화 총량" 같은 전역 불변식을 정책 결정에 쓰려면 분산 합산보다 중앙 원장/이벤트 집계(샤드가 변경을 중앙에 보고)가 안전. 스캐터-개더는 본질적으로 부분 실패에 취약.
- 타임아웃·서킷브레이커·격벽: 느린 샤드를 격리(
distributed_systems/problem7). 단 여기 강조점은 "격리"보다 "그 결과를 신뢰할 수 있는가". - stale-while-degraded: 일부 샤드 미응답 시 마지막으로 성공한 그 샤드 값으로 보강하고
asOf를 노출 — 단 권위 결정엔 부적합함을 표시.
면접 포인트
- 부분 실패를 "0 기여"로 삼키는 게 왜 권위 결정에서 위험한가, 결과에 완전성/응답 비율을 담는 설계.
- 퍼-샤드 타임아웃·전체 데드라인·유계 팬아웃이 없는 스캐터-개더의 실패 양상.
- 전역 합산 불변식을 분산 집계로 구할 때의 한계와 중앙 원장/이벤트 집계 대안.