3. 긴급 점검 종료의 드레이닝 부재 (순서·대기·플러시 누락)
난이도 최상내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 긴급 점검 종료의 드레이닝 부재 (순서·대기·플러시 누락)
난이도: 최상
요약
이 Shutdown은 "플래그 내리고(A) 리스너 닫고(B) 세션 전부 끊고(C) 즉시 프로세스 종료(D)"만 한다. 진행 중인 멀티스텝 요청을 기다리지 않고, 영속화 큐를 비우지(flush) 않으며, 클러스터에서 등록 해제하지 않는다. 결과적으로 분산 작업이 반쯤 적용된 채 끊기고(존 이전 원본 제거+대상 미추가), 큐에 쌓인 더티 상태가 통째로 유실되고, 다른 서버는 죽은 서버로 계속 라우팅한다.
문제점
1. (드레이닝) 진행 중 요청을 기다리지 않음 — (C)(D)
- 증상: 멀티스텝 요청이 중간에 잘린다. 예: 존 이전에서 step2(대상 서버 RPC) 응답 전에 (C)로 세션이 끊기고 (D)로 프로세스가 죽으면, 원본에서 아이템/캐릭터는 빠졌는데 대상에는 안 들어간 상태로 영구 유실.
- 재현조건:
HandleRequest가 step2 RPC 대기 중일 때Shutdown호출. - 근본원인: in-flight 요청 수를 추적하거나 그것이 끝나길 기다리는 단계가 없다.
_running=false(A)는 새 요청만 막을 뿐, 이미 시작된 멀티스텝을 안전 지점까지 진행시키지 못한다.
2. (데이터 유실) 영속화 큐 미플러시 — (D)
- 증상:
_persist에 쌓인 더티 상태가 DB에 못 가고 사라진다(플레이어 진행도 롤백처럼 보임). - 근본원인:
Environment.Exit(0)은 백그라운드 saver를 기다리지 않고 즉시 죽인다. 종료 전에 큐를 비우는(flush+await) 단계가 없다.
3. (분산 라우팅) 클러스터 등록 해제 누락 — _cluster
- 증상: 종료 중/직후에도 다른 서버·게이트웨이가 이 서버로 새 요청/세션을 보낸다 → 연결 거부·타임아웃·또 다른 반쪽 작업.
- 근본원인: 종료의 첫 단계여야 할
Deregister()(트래픽 차단)가 아예 없다.
4. (순서·데드라인) 강제 종료 폴백 없음
- 순서가 거꾸로다(세션부터 끊고 종료). 또 무한정 기다릴 수도 없으므로 유예 데드라인 + 데드라인 초과 시 강제 종료가 필요한데 둘 다 없다(지금은 데드라인=0인 강제 종료).
수정안
올바른 순서: 트래픽 차단 → 신규 거절 → in-flight 드레이닝(데드라인) → 영속화 플러시 → 세션 종료 → 종료.
private int _inFlight; // Interlocked 로 증감
public async Task ShutdownAsync(TimeSpan grace)
{
// 1) 새 트래픽부터 끊는다: 클러스터 등록 해제 + 리스너 닫기
_cluster.Deregister();
_listener.Close();
_running = false; // 새 요청 거절 시작
// 2) 진행 중 요청이 안전 지점까지 끝나길 데드라인까지 대기
var deadline = DateTime.UtcNow + grace;
while (Volatile.Read(ref _inFlight) > 0 && DateTime.UtcNow < deadline)
await Task.Delay(50);
// 3) 영속화 큐를 끝까지 비우고(flush) 확인
await _persist.DrainAndFlushAsync(deadline);
// 4) 그래도 남은 세션 정리 후 종료
foreach (var s in _sessions) s.CloseGracefully();
// 5) 데드라인 초과분만 강제 종료(데이터는 이미 플러시됨)
Environment.Exit(0);
}
public async Task HandleRequest(Session s, Request req)
{
if (!_running) { s.Reject("draining"); return; }
Interlocked.Increment(ref _inFlight);
try { /* step1..3: 멀티스텝은 중간 취소 대신 안전 지점까지 진행 */ }
finally { Interlocked.Decrement(ref _inFlight); }
}
- 멀티스텝 분산 작업(존 이전)은 사가/보상 트랜잭션으로 설계해, 드레이닝 중 끊겨도 재기동 후 "원본 제거 보상" 또는 "대상 추가 완료"로 수렴하게 한다(드레이닝만으론 부족, 아래 설계 참조).
더 나은 설계
- 드레이닝 상태 머신:
Running → Draining → Stopped. Draining에서 신규 거절(503/redirect), in-flight만 완료. 오케스트레이터(K8s preStop/lameduck)와 연동해 데드라인 부여. - 분산 작업의 원자성은 드레이닝이 아니라 프로토콜로: 존 이전 등은 2단계 커밋/사가 + 멱등 재개로 설계해야 "반쪽" 상태가 재기동 후 수렴한다. 드레이닝은 "정상 종료 시 손실 최소화"일 뿐, 갑작스런 크래시(전원 차단)에도 안전하려면 영속 로그/사가가 필수.
- 영속화는 동기 체크포인트 경로 보유: 종료 시 큐를 flush하고, 더해 주기적 스냅샷으로 유실 창을 좁힌다.
- 트레이드오프: 긴 grace는 안전하지만 점검 지연. 보통 수십 초 데드라인 + 강제 폴백. in-flight 추적 비용은 작다.
면접 포인트
- graceful shutdown의 순서: (등록 해제·리스너 닫기) → (신규 거절) → (in-flight 드레이닝, 데드라인) → (영속화 flush) → (세션 종료) → exit. 순서가 틀리면 트래픽이 새거나 데이터가 샌다.
- 드레이닝은 정상 종료를 매끄럽게 할 뿐, 갑작스런 크래시 안전성은 사가/2PC + 멱등 재개로 별도 보장해야 한다.
Environment.Exit/즉시 종료는 백그라운드 영속화를 죽인다 — 종료 전에 큐 flush + 데드라인 초과 시에만 강제.
해설 · C++
해설 — 긴급 점검 종료의 드레이닝 부재 + std::exit 의 스택 미해제 · C++
난이도: 최상
요약
shutdown은 "플래그 내리고(A) 리스너 닫고(B) 세션 즉시 끊고(C) std::exit(0)(D)"만 한다. in-flight 멀티스텝을 기다리지 않고, 영속화 큐를 flush하지 않으며, 클러스터 등록 해제도 없다. C++에서는 한 가지가 더 치명적이다: std::exit는 자동(스택) 객체의 소멸자를 호출하지 않는다. 즉 main에 있는 GameServer/PersistQueue의 소멸자(flush+join)가 실행되지 않고, 백그라운드 saver 스레드는 join되지 않은 채 죽어 더티 상태가 통째로 유실되며 UB 위험까지 생긴다.
문제점
1. (C++/종료) std::exit 가 스택 객체 소멸자를 건너뜀 — (D)
- 증상:
PersistQueue/GameServer가main의 지역 변수면 그 소멸자(큐 flush, saver 스레드 join)가 호출되지 않는다. 큐의 미저장분이 사라지고, join 안 된 백그라운드 스레드가 파괴 중인 전역/정적을 만지면 UB·크래시. - 근본원인:
std::exit는 정적 저장기간 객체의 소멸자와atexit핸들러만 부른다. 자동 변수의 스택은 풀지 않는다. RAII 기반 flush/join이 전부 무력화된다(std::abort/_Exit는 그조차 안 함).
2. (드레이닝) in-flight 멀티스텝 미대기 — (C)(D)
- 존 이전 RPC(step2) 대기 중 세션을 끊고 프로세스를 죽이면 원본에서 제거됐는데 대상에 추가 안 된 반쪽 상태가 영구 유실. in-flight 카운트 추적·대기 단계가 없다.
3. (데이터 유실) 영속화 큐 미플러시 — _persist
- 백그라운드 saver를 기다리지 않고 종료 → 큐 잔량 손실. 종료 전 flush+join 단계 부재(1번과 결합되어 더 확실히 유실).
4. (분산 라우팅) 클러스터 등록 해제 누락 — _cluster
- 종료의 첫 단계여야 할
deregister()가 없어 다른 서버가 죽는 서버로 계속 라우팅 → 타임아웃·또 다른 반쪽 작업.
5. (순서·데드라인) 강제 종료 폴백/순서 오류
- 세션부터 끊고 즉시 종료. 올바른 순서·유예 데드라인·데드라인 초과 시에만 강제, 가 모두 없다.
수정안
void shutdown(std::chrono::seconds grace) {
// 1) 새 트래픽부터 차단: 등록 해제 + 리스너 닫기
_cluster.deregister();
_listener.close();
_running.store(false); // 새 요청 거절 시작
// 2) in-flight 가 안전 지점까지 끝나길 데드라인까지 대기
auto deadline = std::chrono::steady_clock::now() + grace;
{
std::unique_lock<std::mutex> lk(_m);
_idle.wait_until(lk, deadline, [&]{ return _inFlight == 0; });
}
// 3) 영속화 큐 flush + saver 스레드 join
_persist.drainAndFlush(deadline);
// 4) 세션 정리
for (Session* s : _sessions) s->closeGracefully();
// 5) main 으로 정상 반환(또는 종료 신호) — std::exit 로 스택을 건너뛰지 않는다.
_stopRequested.store(true); // run-loop 가 return → 스택 풀림 → 소멸자 실행
}
void handleRequest(Session* s, Request& req) {
if (!_running.load()) { /* reject: draining */ return; }
{ std::lock_guard<std::mutex> lk(_m); ++_inFlight; }
// step1..3 (안전 지점까지 진행)
{ std::lock_guard<std::mutex> lk(_m); if (--_inFlight == 0) _idle.notify_all(); }
}
std::exit대신 run-loop를return시켜 스택을 정상으로 풀어 RAII(flush/join)가 실행되게 한다.- 백그라운드 스레드는
std::jthread/명시적 join으로 반드시 합류시킨다(detached 금지 — 종료 시 통제 불가). - 멀티스텝 분산 작업은 드레이닝만으로 부족 → 아래 설계.
더 나은 설계
- 종료 = run-loop 반환 + RAII:
main이 정상 반환해 스택 객체 소멸자가 flush/join을 보장.std::exit/abort는 데이터 안전 종료 경로로 부적합. - 드레이닝 상태 머신(Running→Draining→Stopped) + 오케스트레이터 preStop/lameduck 연동, 데드라인+강제 폴백.
- 분산 작업 원자성은 프로토콜로: 존 이전 등은 사가/2PC + 멱등 재개로 설계해 크래시(전원 차단)에도 재기동 후 수렴. 드레이닝은 정상 종료 손실 최소화일 뿐.
- 트레이드오프: jthread/join·in-flight 추적 비용은 작다. 긴 grace는 안전하나 점검 지연 → 수십 초 데드라인 + 강제 폴백.
면접 포인트
std::exit는 스택을 풀지 않는다 — 자동 객체 소멸자(RAII flush/join) 미실행. 데이터 안전 종료는 run-loop 반환으로 스택을 정상 해제해야 한다.- graceful drain 순서: 등록 해제·리스너 닫기 → 신규 거절 → in-flight 드레이닝(데드라인) → 영속화 flush → 세션 종료 → 반환.
- 백그라운드 스레드는 detach가 아니라 join/jthread로 통제 — 종료 시 미합류 스레드는 UB·유실의 원인.