10. C# 서버 간 세션 핸드오프/마이그레이션 + Graceful Shutdown 드레이닝 (복합)
난이도 최상내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — C# 서버 간 세션 핸드오프/마이그레이션 + Graceful Shutdown 드레이닝 (복합)
난이도: 최상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
무중단 마이그레이션의 어려운 세 축(split-brain 방지 / 핸드오프 정합성 / 진짜 graceful 종료)이 모두 깨져 있다. 핵심 결함:
- split-brain: "스냅샷 전송 → 옛 노드에서 제거" 사이에 두 노드가 동시에 같은 세션을 활성으로 본다. 그 구간 메시지는 옛 노드에서 처리되어 새 노드에 반영 안 됨 (유실) 또는 양쪽 처리(중복).
- 즉시 Close 로 유실: 클라가 새 노드에 재접속을 끝내기도 전에 옛 노드가 소켓을 닫아(G) in-flight·미전달 메시지가 사라진다. 핸드오프가 "확정"되기 전에 버린다.
- 가짜 graceful:
Environment.Exit(0)(H)는 진행 중 async 작업·in-flight 요청을 기다리지 않고 프로세스를 즉사시킨다. 드레인 플래그만으로는 진행 중 작업을 못 지킴. - 동시성/멱등성 결함: 드레인 중 신규 접속·핸드오프·요청 처리의 경합, 핸드오프 실패 시 롤백·재시도 없음, 직렬 await 로 드레인이 느려 강제 종료 위험.
문제점
(E)(F)(G) split-brain — 두 노드 동시 활성 구간 (분류: 정합성, 치명)
- 증상: 순서가
스냅샷 전송(E) → 클라 리다이렉트(F) → 옛 노드 제거(G). 그런데 스냅샷을 보낸 그 순간부터 새 노드는 그 세션을 가질 수 있고, 옛 노드도 (G) 전까진 세션을 활성으로 들고HandleRequest(I)로 계속 처리한다. 이 구간에 도착한 클라 메시지는:- 옛 노드에서 처리되어 새 노드 스냅샷엔 없음 → 유실, 또는
- 클라가 이미 새 노드로 옮겨 새 노드에서도 처리 → 중복/순서 역전.
- 근본원인: 소유권 이전이 원자적 전환점(cutover) 없이 진행. "정확히 한 노드만 활성"을 강제하는 펜싱(fencing)/배리어가 없다.
(G) 핸드오프 확정 전 즉시 Close → 유실 (분류: 정합성/수명관리)
- 증상: 스냅샷 보내고 리다이렉트 지시하자마자
s.Close(). 클라가 새 노드 재접속에 성공했다는 확인(ack) 을 받기 전에 옛 소켓을 닫는다. 재접속이 실패/지연되면 클라는 양쪽 어디에도 없고, 옛 노드의 미전송 송신 큐(problem5)도 통째로 사라진다. - 근본원인: 핸드오프 완료를 "낙관적"으로 가정. commit/ack 기반 2단계 필요.
(H) Environment.Exit(0) — 가짜 graceful (분류: 견고성, 치명)
- 증상: in-flight async(스냅샷 전송, 진행 중 요청, 미완료 await)를 전혀 기다리지 않고 프로세스를 즉사. finally/Dispose/flush 도 안 돌 수 있다. "graceful" 의도와 정반대.
- 근본원인: 종료를 "드레인 완료의 신호"가 아니라 "강제 kill"로 구현. 활성 세션 0, in-flight 0 을 검증한 뒤 정상 반환·정상 종료해야 한다.
(C)(I) 드레인 상태와 처리의 비일관 (분류: 동시성/정확성)
- 증상:
- (C)
Draining체크와 (D) 스냅샷 사이/이후에 들어온 신규 접속이 테이블에 추가된 뒤 드레인 스냅샷에 안 잡혀 좀비로 남을 수 있다(체크-등록 비원자, TOCTOU). - (I)
HandleRequest가 드레이닝/핸드오프 진행 여부를 무시하고 처리 → 이미 새 노드로 넘어가는 세션의 상태를 옛 노드가 변경(스냅샷과 불일치).
- (C)
- 근본원인: 세션의 마이그레이션 상태머신이 없고, 처리 경로가 상태를 존중하지 않음.
(D)(E) 직렬 await 드레인 — 느림 + 부분 실패 미처리 (분류: 견고성/성능)
- 증상:
foreach안에서 세션마다await. 수천~수만 세션을 직렬 처리하면 드레인이 수 분 걸려 배포 타임아웃·강제 kill 위험. 또 중간에SendSnapshotAsync가 실패하면 롤백/재시도 없이 그 세션만 어정쩡하게 남거나 루프가 깨진다. - 근본원인: 배치/병렬·재시도·타임아웃·부분실패 복구 설계 부재.
(A)(B) 가시성/상태 모델 (분류: 동시성)
Active(A)는 단순 bool 로 마이그레이션 상태(Active→Migrating→Migrated)를 표현 못 함.volatile bool Draining(B)은 플래그 자체 가시성은 되지만 "체크+행동"의 원자성은 없음.
수정안
핵심: 마이그레이션 상태머신 + 펜싱(cutover) + ack 기반 commit + 진짜 드레인 대기
public enum SessState { Active, Migrating, Migrated, Closed }
public sealed class Session
{
public long Id;
private int _state = (int)SessState.Active;
public long Epoch; // 펜싱 토큰: 핸드오프마다 증가, 새 노드가 더 큰 epoch 로 무효화
public bool TryBeginMigrate()
=> Interlocked.CompareExchange(ref _state,
(int)SessState.Migrating, (int)SessState.Active) == (int)SessState.Active;
public SessState State => (SessState)Volatile.Read(ref _state);
public void MarkMigrated() => Volatile.Write(ref _state, (int)SessState.Migrated);
public byte[] Snapshot() { /* 큐 잔여 포함 직렬화 */ return Array.Empty<byte>(); }
}
public sealed class GatewayNode
{
private readonly ConcurrentDictionary<long, Session> _sessions = new();
public volatile bool Draining = false;
private long _inFlight = 0; // 진행 중 요청 수 (드레인 대기용)
public Session OnNewConnection(long id)
{
if (Draining) return null; // 신규 거절(or 다른 노드로 redirect)
var s = new Session { Id = id };
// 추가 직후 한 번 더 검사: 드레인이 막 켜졌으면 즉시 회수(TOCTOU 해소)
if (!_sessions.TryAdd(id, s)) return null;
if (Draining) { _sessions.TryRemove(id, out _); return null; }
return s;
}
public async Task DrainAndHandoffAsync(string target, IPeer peer, CancellationToken ct)
{
Draining = true; // 1) 신규 차단
// 2) 진행 중 요청이 빠질 시간을 준 뒤 핸드오프 (배치+병렬+재시도)
var sessions = _sessions.Values.ToArray();
using var gate = new SemaphoreSlim(64); // 동시 핸드오프 상한
var tasks = sessions.Select(s => HandoffOneAsync(s, target, peer, gate, ct));
await Task.WhenAll(tasks);
// 3) in-flight 요청이 0 이 될 때까지 대기 (진짜 graceful)
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(30);
while (Interlocked.Read(ref _inFlight) > 0 && DateTime.UtcNow < deadline)
await Task.Delay(50, ct);
// 4) 정상 반환 → 호출자가 호스트를 정상 종료(StopAsync). Exit() 금지.
}
private async Task HandoffOneAsync(Session s, string target, IPeer peer,
SemaphoreSlim gate, CancellationToken ct)
{
await gate.WaitAsync(ct);
try
{
if (!s.TryBeginMigrate()) return; // 이미 마이그레이션/종료 중이면 skip
// (펜싱) epoch 증가분을 스냅샷에 실어 새 노드가 권위를 갖게 함
long epoch = Interlocked.Increment(ref s.Epoch);
// a) 스냅샷 전송 (재시도/타임아웃) — 단, 아직 옛 노드가 권위자
await RetryAsync(() => peer.SendSnapshotAsync(target, s.Id, epoch, s.Snapshot()), ct);
// b) 클라 리다이렉트 + 클라/새 노드의 "재접속 완료 ack" 대기 (commit point)
s.RedirectTo(target);
bool committed = await peer.WaitMigrationCommitAsync(s.Id, epoch, ct); // 새 노드가 활성화 확인
if (!committed) { /* 롤백: state Active 복귀, 옛 노드 계속 서비스 */ RollbackMigrate(s); return; }
// c) commit 후에야 옛 노드 제거 + 소켓 정리(멱등). 이 시점부터 새 노드가 권위.
s.MarkMigrated();
_sessions.TryRemove(s.Id, out _);
DrainSessionSends(s); // 남은 송신 flush 후
s.Close();
}
finally { gate.Release(); }
}
public void HandleRequest(long id, byte[] req)
{
if (!_sessions.TryGetValue(id, out var s)) return;
if (s.State != SessState.Active) return; // 마이그레이션 중이면 옛 노드는 처리 안 함
// (클라는 곧 새 노드로 가서 거기서 처리)
Interlocked.Increment(ref _inFlight);
try { /* 처리 */ }
finally { Interlocked.Decrement(ref _inFlight); }
}
// RetryAsync / RollbackMigrate / DrainSessionSends ... (생략)
}
포인트
- 상태머신 + CAS(
TryBeginMigrate): 한 세션의 마이그레이션을 단 한 번만 시작. 처리 경로(HandleRequest)는Active일 때만 동작 → 핸드오프 중 옛 노드가 상태를 바꾸지 않음(split-brain 의 "양쪽 처리" 차단). - epoch 펜싱: 새 노드가 더 큰 epoch 로 권위를 갖고, 늦게 도착한 옛 노드의 행동을 무효화. 메시지가 두 곳에서 권위 있게 처리되는 것을 막는다.
- commit/ack 기반 cutover: 새 노드 활성화 + 클라 재접속이 확인된 뒤에야 옛 노드 제거·Close. 실패 시 롤백해 옛 노드가 계속 서비스(유실 0).
- 진짜 graceful:
Environment.Exit제거. in-flight 0 을 확인하고 정상 반환 → 호스트의StopAsync/finally 가 돌게 한다. 데드라인으로 무한 대기 방지. - 병렬+상한+재시도+타임아웃: 대규모 세션도 빠르게, 부분 실패에 견고하게 드레인.
더 나은 설계
1) 클라 주도 재접속 + 멱등 키 (at-least-once → exactly-once)
- 클라가 migrate ticket(서명·epoch 포함)으로 새 노드에 재접속하고, 옛 노드는 ack 전까지 버퍼 보관. 메시지에 시퀀스/멱등 키를 부여해 중복 도착을 새 노드가 dedup.
- 트레이드오프: 클라 프로토콜 복잡도 ↑. 대신 네트워크 흔들림에 견고.
2) 외부 권위(레지스트리/합의)로 split-brain 원천 차단
- "이 세션의 현재 노드"를 etcd/Redis(또는 Raft 그룹)에 두고 lease + fencing token 으로 단일 활성을 강제. 노드 간 직접 합의 대신 외부 진실 소스를 신뢰.
- 트레이드오프: 레지스트리 의존·지연. 하지만 정합성 보장이 명확.
3) 연결 드레이닝과 LB 협조
- Drain 시작 시 LB 헬스체크를 의도적으로 fail(또는 connection-draining 모드)로
돌려 신규 트래픽을 LB 단에서 끊고, 기존 연결만 마무리.
OnNewConnection거절은 최후 방어.
4) 스냅샷 대신 상태 스트리밍/공유 스토어
- 큰 세션 상태는 일괄 스냅샷보다 증분 복제 또는 공유 세션 스토어(상태를 노드 밖에 두고 노드는 무상태)로 두면 핸드오프가 "포인터 이전"에 가까워져 cutover 창이 짧다.
- 트레이드오프: 스토어 지연·비용, 핫스테이트 캐싱 필요.
면접 포인트
- "세션 마이그레이션에서 split-brain 을 어떻게 막나?" → 단일 활성 보장을 위해 상태머신 + CAS 로 처리 경로를 펜싱하고, epoch/fencing token + 외부 권위(lease)로 "정확히 한 노드만 권위"를 강제. cutover 전에 옛 노드가 상태를 못 바꾸게 한다.
- "
Environment.Exit(0)가 왜 graceful 이 아닌가? 진짜 graceful 종료는?" → in-flight async/요청/flush 를 기다리지 않고 즉사시킨다. 신규 차단 → 진행 중 작업 드레인(카운터 0 대기) → 정상 반환으로 호스트가 정리하게 하고, 데드라인을 둬 무한 대기를 막는다. - "핸드오프 중 메시지 유실/중복을 어떻게 0 으로 만드나?" → commit/ack 기반 2단계 cutover(새 노드 활성 확인 전엔 옛 노드 유지·버퍼 보관), epoch 펜싱, 멱등 키로 dedup, 실패 시 롤백. 즉시 Close 금지.
- "수만 세션을 직렬 await 로 드레인하면? 어떻게 개선?" → 배포 타임아웃·강제 kill 위험. 동시성 상한을 둔 병렬 처리 + 재시도/타임아웃 + LB 드레이닝 협조로 빠르고 견고하게.
해설 · C++
해설 — C++ 서버 간 세션 핸드오프/마이그레이션 + Graceful Shutdown 드레이닝 (복합)
난이도: 최상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
무중단 마이그레이션의 어려운 세 축(split-brain 방지 / 핸드오프 정합성 / 진짜 graceful 종료)이 모두 깨져 있다. 핵심 결함:
- split-brain: "스냅샷 전송 → 옛 노드에서 제거" 사이에 두 노드가 동시에 같은 세션을 활성으로 본다. 그 구간 메시지는 옛 노드에서 처리되어 새 노드에 반영 안 됨 (유실) 또는 양쪽 처리(중복).
- 즉시 Close+delete 로 유실 + UAF: 클라가 새 노드 재접속을 끝내기도 전에 옛 노드가
소켓을 닫고 객체를
delete해(G) in-flight·미전달 메시지가 사라지고, in-flightHandleRequest가 같은 포인터를 만지면 use-after-free. - 가짜 graceful:
std::exit(0)(H)는 진행 중 future·스레드·in-flight 요청을 기다리지 않고 프로세스를 즉사시킨다. detach된 스레드/소멸자가 안 돈다. - 블로킹 직렬 핸드오프 + 부분 실패 미처리:
future.get()직렬 처리로 느리고, 실패 시 롤백·재시도가 없다.
문제점
(E)(F)(G) split-brain — 두 노드 동시 활성 구간 (분류: 정합성, 치명)
- 증상: 순서가
스냅샷 전송(E) → 클라 리다이렉트(F) → 옛 노드 제거(G). 그런데 스냅샷을 보낸 그 순간부터 새 노드는 그 세션을 가질 수 있고, 옛 노드도 (G) 전까진 세션을 활성으로 들고HandleRequest(I)로 계속 처리한다. 이 구간에 도착한 클라 메시지는:- 옛 노드에서 처리되어 새 노드 스냅샷엔 없음 → 유실, 또는
- 클라가 이미 새 노드로 옮겨 새 노드에서도 처리 → 중복/순서 역전.
- 근본원인: 소유권 이전이 원자적 전환점(cutover) 없이 진행. "정확히 한 노드만 활성"을 강제하는 펜싱(fencing)/배리어가 없다.
(G) 핸드오프 확정 전 즉시 Close+delete → 유실 + UAF (분류: 정합성/수명관리, 치명)
- 증상: 스냅샷 보내고 리다이렉트 지시하자마자
s->Close(); delete s;. 클라가 새 노드 재접속에 성공했다는 확인(ack) 을 받기 전에 옛 소켓을 닫고 객체를 해제한다. 재접속이 실패/지연되면 클라는 양쪽 어디에도 없고, 옛 노드의 미전송 송신 큐도 통째로 사라진다. 게다가delete s직후 in-flightHandleRequest(다른 수신 스레드)가 같은 raw 포인터를 들고 있으면 use-after-free. - 근본원인: 핸드오프 완료를 "낙관적"으로 가정. commit/ack 기반 2단계 + 마지막 참조자 해제 규칙(shared_ptr)이 없음.
(H) std::exit(0) — 가짜 graceful (분류: 견고성, 치명)
- 증상: in-flight 비동기(스냅샷 전송 future, 진행 중 요청)를 전혀 기다리지 않고
프로세스를 즉사.
std::exit는 정적 객체 소멸자만 부르고 detach된 스레드·자동 객체 소멸자·flush 를 보장하지 않으며, 다른 스레드가 자원을 만지는 중이면 미정의 동작. "graceful" 의도와 정반대. - 근본원인: 종료를 "드레인 완료의 신호"가 아니라 "강제 kill"로 구현. 활성 세션 0, in-flight 0 을 검증한 뒤 정상 반환·정상 종료해야 한다.
(C)(I) 드레인 상태와 처리의 비일관 (분류: 동시성/정확성)
- 증상:
- (B)
draining은 비원자bool+ (C) 체크와 (D) 스냅샷 사이/이후에 들어온 신규 접속이 테이블에 추가된 뒤 드레인 스냅샷에 안 잡혀 좀비로 남을 수 있다(체크-등록 비원자, TOCTOU).draining가시성도 보장 안 됨. - (I)
HandleRequest가 드레이닝/핸드오프 진행 여부를 무시하고 처리 → 이미 새 노드로 넘어가는 세션의 상태를 옛 노드가 변경(스냅샷과 불일치).
- (B)
- 근본원인: 세션의 마이그레이션 상태머신이 없고, 처리 경로가 상태를 존중하지 않음.
(D)(E) 블로킹 직렬 핸드오프 — 느림 + 부분 실패 미처리 (분류: 견고성/성능)
- 증상:
for안에서 세션마다future.get()으로 블로킹. 수천~수만 세션을 직렬 처리하면 드레인이 수 분 걸려 배포 타임아웃·강제 kill 위험. 또 중간에SendSnapshotAsync가 throw 하면.get()이 예외를 던져 롤백/재시도 없이 루프가 깨지고 일부 세션만 어정쩡하게 남는다. - 근본원인: 배치/병렬·재시도·타임아웃·부분실패 복구 설계 부재.
수정안
핵심: 마이그레이션 상태머신 + 펜싱(cutover) + ack 기반 commit + shared_ptr 수명 + 진짜 드레인 대기
#include <unordered_map>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <future>
#include <string>
#include <chrono>
#include <cstdint>
enum class SessState { Active, Migrating, Migrated, Closed };
class Session {
public:
int64_t id;
std::atomic<int> state{(int)SessState::Active};
std::atomic<int64_t> epoch{0}; // 펜싱 토큰: 핸드오프마다 증가
bool TryBeginMigrate() {
int expected = (int)SessState::Active;
return state.compare_exchange_strong(expected, (int)SessState::Migrating);
}
SessState State() const { return (SessState)state.load(); }
void MarkMigrated() { state.store((int)SessState::Migrated); }
void RollbackToActive() { state.store((int)SessState::Active); }
std::vector<char> Snapshot() { /* 큐 잔여 포함 직렬화 */ return {}; }
void RedirectTo(const std::string&) {}
void Close() { /* 소켓 종료(멱등) */ }
};
class IPeer {
public:
virtual ~IPeer() = default;
virtual std::future<bool> SendSnapshotAsync(const std::string&, int64_t, int64_t epoch,
const std::vector<char>&) = 0;
virtual std::future<bool> WaitMigrationCommitAsync(int64_t id, int64_t epoch) = 0;
};
class GatewayNode {
std::unordered_map<int64_t, std::shared_ptr<Session>> sessions_;
std::mutex mtx_;
std::atomic<bool> draining_{false};
std::atomic<int64_t> inFlight_{0};
public:
std::shared_ptr<Session> OnNewConnection(int64_t id) {
if (draining_.load()) return nullptr; // 신규 거절(or redirect)
auto s = std::make_shared<Session>();
s->id = id;
std::lock_guard<std::mutex> lk(mtx_);
if (draining_.load()) return nullptr; // 등록 직전 재검사(TOCTOU 해소)
sessions_[id] = s;
return s;
}
// 드레인: 신규 차단 → 병렬+상한 핸드오프 → in-flight 0 대기 → 정상 반환
void DrainAndHandoff(const std::string& target, IPeer& peer) {
draining_.store(true); // 1) 신규 차단
std::vector<std::shared_ptr<Session>> all;
{ std::lock_guard<std::mutex> lk(mtx_);
for (auto& kv : sessions_) all.push_back(kv.second); }
// 2) 병렬 핸드오프(동시성 상한은 세마포어/스레드풀로; 여기선 async 예시)
std::vector<std::future<void>> tasks;
for (auto& s : all)
tasks.push_back(std::async(std::launch::async,
[this, s, &target, &peer] { HandoffOne(s, target, peer); }));
for (auto& t : tasks) t.get();
// 3) in-flight 요청이 0 이 될 때까지 대기 (진짜 graceful, 데드라인 포함)
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
while (inFlight_.load() > 0 && std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// 4) 정상 반환 → 호출자가 정상 종료(소멸자/flush 가 돈다). std::exit 금지.
}
void HandoffOne(const std::shared_ptr<Session>& s, const std::string& target, IPeer& peer) {
if (!s->TryBeginMigrate()) return; // 이미 마이그레이션/종료 중이면 skip
int64_t epoch = s->epoch.fetch_add(1) + 1; // 펜싱 epoch 증가
// a) 스냅샷 전송(재시도/타임아웃은 생략) — 아직 옛 노드가 권위
if (!peer.SendSnapshotAsync(target, s->id, epoch, s->Snapshot()).get()) {
s->RollbackToActive(); return; // 실패 → 옛 노드 계속 서비스
}
// b) 클라 리다이렉트 + 새 노드/클라의 "재접속 완료 ack" 대기 (commit point)
s->RedirectTo(target);
if (!peer.WaitMigrationCommitAsync(s->id, epoch).get()) {
s->RollbackToActive(); return; // 롤백: 옛 노드 유지(유실 0)
}
// c) commit 후에야 옛 노드 제거 + 소켓 정리. 이 시점부터 새 노드가 권위.
s->MarkMigrated();
{ std::lock_guard<std::mutex> lk(mtx_); sessions_.erase(s->id); }
s->Close(); // delete 없음: 마지막 shared_ptr 가 사라질 때 자동 소멸(UAF 차단)
}
void HandleRequest(int64_t id, const std::vector<char>& req) {
std::shared_ptr<Session> s;
{ std::lock_guard<std::mutex> lk(mtx_);
auto it = sessions_.find(id);
if (it != sessions_.end()) s = it->second; } // shared_ptr 로 수명 고정
if (!s) return;
if (s->State() != SessState::Active) return; // 마이그레이션 중이면 처리 안 함
inFlight_.fetch_add(1);
// ... 처리 ...
inFlight_.fetch_sub(1);
}
};
포인트
- 상태머신 + CAS(
TryBeginMigrate): 한 세션의 마이그레이션을 단 한 번만 시작. 처리 경로(HandleRequest)는Active일 때만 동작 → 핸드오프 중 옛 노드가 상태를 바꾸지 않음(split-brain 의 "양쪽 처리" 차단). - epoch 펜싱: 새 노드가 더 큰 epoch 로 권위를 갖고, 늦게 도착한 옛 노드의 행동을 무효화. 메시지가 두 곳에서 권위 있게 처리되는 것을 막는다.
- commit/ack 기반 cutover: 새 노드 활성화 + 클라 재접속이 확인된 뒤에야 옛 노드 제거·Close. 실패 시 롤백해 옛 노드가 계속 서비스(유실 0).
shared_ptr수명: rawdelete제거.HandleRequest가 들고 있으면 erase/Close 해도 마지막 ref 가 사라질 때 해제 → use-after-free 차단.- 진짜 graceful:
std::exit제거. in-flight 0 을 확인하고 정상 반환 → 호출자가 소멸자/flush 를 돌게 한다. 데드라인으로 무한 대기 방지. - 병렬+상한+재시도: 대규모 세션도 빠르게, 부분 실패에 견고하게 드레인.
더 나은 설계
1) 클라 주도 재접속 + 멱등 키 (at-least-once → exactly-once)
- 클라가 migrate ticket(서명·epoch 포함)으로 새 노드에 재접속하고, 옛 노드는 ack 전까지 버퍼 보관. 메시지에 시퀀스/멱등 키를 부여해 중복 도착을 새 노드가 dedup.
- 트레이드오프: 클라 프로토콜 복잡도 ↑. 대신 네트워크 흔들림에 견고.
2) 외부 권위(레지스트리/합의)로 split-brain 원천 차단
- "이 세션의 현재 노드"를 etcd/Redis(또는 Raft 그룹)에 두고 lease + fencing token 으로 단일 활성을 강제. 노드 간 직접 합의 대신 외부 진실 소스를 신뢰.
- 트레이드오프: 레지스트리 의존·지연. 하지만 정합성 보장이 명확.
3) 연결 드레이닝과 LB 협조
- Drain 시작 시 LB 헬스체크를 의도적으로 fail(또는 connection-draining 모드)로 돌려
신규 트래픽을 LB 단에서 끊고, 기존 연결만 마무리.
OnNewConnection거절은 최후 방어.
4) 스냅샷 대신 상태 스트리밍/공유 스토어
- 큰 세션 상태는 일괄 스냅샷보다 증분 복제 또는 공유 세션 스토어(노드는 무상태)로 두면 핸드오프가 "포인터 이전"에 가까워져 cutover 창이 짧다.
- 트레이드오프: 스토어 지연·비용, 핫스테이트 캐싱 필요.
면접 포인트
- "세션 마이그레이션에서 split-brain 을 어떻게 막나?" → 단일 활성 보장을 위해 상태머신 + CAS 로 처리 경로를 펜싱하고, epoch/fencing token + 외부 권위(lease)로 "정확히 한 노드만 권위"를 강제. cutover 전에 옛 노드가 상태를 못 바꾸게 한다.
- "
std::exit(0)가 왜 graceful 이 아닌가? 진짜 graceful 종료는?" → in-flight 비동기/요청/flush·소멸자를 기다리지 않고 즉사시킨다(detach 스레드/자동 객체 정리 미보장). 신규 차단 → 진행 중 작업 드레인(카운터 0 대기) → 정상 반환으로 호출자가 정리하게 하고, 데드라인으로 무한 대기를 막는다. - "핸드오프 중 메시지 유실/중복을 어떻게 0 으로 만드나?" → commit/ack 기반 2단계 cutover(새 노드 활성 확인 전엔 옛 노드 유지·버퍼 보관), epoch 펜싱, 멱등 키로 dedup, 실패 시 롤백. 즉시 Close+delete 금지(shared_ptr 로 수명 관리).
- "수만 세션을
future.get()직렬 블로킹으로 드레인하면? 어떻게 개선?" → 배포 타임아웃·강제 kill 위험. 동시성 상한을 둔 병렬 처리 + 재시도/타임아웃 + LB 드레이닝 협조로 빠르고 견고하게.