← 문제로

6. 인스턴스 서버 failover 의 split-brain 과 보상 중복

난이도 상
내 리뷰 · C#
해설 · C#

해설 — 인스턴스 서버 failover 의 split-brain 과 보상 중복

난이도: 상

요약

헬스체크 한 번 실패만으로(C) 인스턴스를 백업 노드에서 다시 띄우고 소유권을 넘긴다(A). 그러나 "무응답"이 곧 "죽음"은 아니다. 원 노드가 긴 GC 멈춤·네트워크 순단으로 잠깐 응답을 못 했을 뿐 살아 있으면, 백업이 같은 인스턴스를 동시에 돌려 split-brain(한 인스턴스·두 노드) 이 된다. 두 노드가 보스 막타·보상을 각자 처리해 이중 지급된다. 또 마지막 체크포인트 이후 진행은 유실되고(B), 체크포인트에 안 들어간 보상이 백업에서 재지급된다(멱등성 부재).

문제점

(1) [split-brain] 펜싱 없는 소유권 이전 — (A)

  • 증상: 원 노드와 백업이 같은 인스턴스를 동시에 활성. 보상·아이템·랭킹이 두 곳에서 DB 에 써져 충돌·이중 지급.
  • 재현: 원 노드가 2초 GC 멈춤 → 헬스체크 실패 → 백업 기동 → 원 노드가 깨어나 자기가 여전히 주인인 줄 알고 계속 처리.
  • 근본원인: 새 주인을 세울 때 옛 주인을 무력화(fencing) 하지 않음. 소유권이 단순 맵 갱신일 뿐, 옛 노드의 쓰기를 막을 토큰/리스가 없다.

(2) [감지 오판] 단발 타임아웃으로 죽음 단정 — (C)

  • 증상: 일시적 지연을 장애로 오인해 불필요한 failover 가 빈발(플래핑).
  • 근본원인: 연속 실패 횟수·리스 만료가 아니라 한 번의 무응답으로 판정.

(3) [멱등성/체크포인트] 보상 재지급·진행 유실 — (B)

  • 증상: 백업이 마지막 체크포인트로 시작 → 그 이후 진행 손실. 체크포인트 전에 지급된 보상이 "아직 안 준 것"으로 보여 다시 지급.
  • 근본원인: 보상에 멱등 키가 없고, 체크포인트와 외부 효과(보상 지급)가 원자적으로 묶이지 않음.

수정안 — 리스 + 펜싱 토큰 + 멱등 보상

1) 단조 증가 펜싱 토큰을 동반한 리스(lease)

// 소유권 = (nodeId, epoch). 인계할 때마다 epoch 를 +1 (단조 증가).
public bool TryFailover(long instanceId, string deadNode)
{
    // 리스 저장소(예: etcd/Redis)에서 원자적으로 소유권 획득 + epoch 증가
    if (!_lease.TryAcquire(instanceId, expectedOwner: deadNode,
                           out string backup, out long newEpoch))
        return false;   // 원 노드가 아직 리스를 들고 있으면 인계 거부

    var state = LoadLastCheckpoint(instanceId);
    _nodes[backup].StartInstance(instanceId, state, newEpoch);  // 새 토큰 부여
    return true;
}

2) 모든 영속 쓰기에 펜싱 토큰 검사 — 옛 주인 무력화

// DB/저장소는 instance 의 현재 epoch 보다 낮은 토큰의 쓰기를 거부한다.
// UPDATE ... WHERE instance_id=@id AND epoch <= @token   -- 토큰이 오래됐으면 0행
void PersistReward(long instanceId, long token, Reward r)
{
    int rows = _db.Execute(
        "UPDATE instance_state SET ... WHERE instance_id=@id AND current_epoch=@token",
        instanceId, token);
    if (rows == 0) throw new FencedException();  // 나는 더 이상 주인이 아님 → 자진 종료
}
  • 원 노드가 깨어나도 토큰이 옛 epoch 라 모든 쓰기가 거부(fenced) → split-brain 무해화. fenced 를 감지하면 인스턴스를 스스로 내린다.

3) 감지는 연속 실패 + 리스 만료로

  • N 회 연속 하트비트 실패 그리고 리스 TTL 만료일 때만 인계. 원 노드는 리스를 갱신 못 하면 스스로 인스턴스를 정지(자기 펜싱)한다.

4) 보상 멱등성

  • 보상마다 reward_id(결정적 생성), 지급은 INSERT ... ON CONFLICT DO NOTHING. 체크포인트 이후 재지급해도 중복 없음. 가능하면 체크포인트와 보상 지급을 같은 트랜잭션/아웃박스로 묶는다.

더 나은 설계 (트레이드오프)

  • 합의(consensus) 기반 리더십: etcd/ZooKeeper/Raft 로 인스턴스 소유권 리스를 관리하면 펜싱 토큰이 공짜로 나온다. 운영 복잡도 ↑.
  • CAP 관점: split-brain 을 막으려면 분단 시 가용성을 희생(CP)해야 한다. "두 노드가 도는 것보다 잠깐 멈추는 게 낫다"는 트레이드오프를 받아들이는 것.

면접 포인트

  • "무응답 ≠ 죽음": 타임아웃은 장애를 확정하지 못한다(비동기 네트워크의 근본 한계). 그래서 새 주인을 세울 땐 반드시 옛 주인을 펜싱한다.
  • 펜싱 토큰(단조 증가 epoch)을 모든 쓰기 경로에서 검사 → 늦게 깨어난 옛 리더의 쓰기를 저장소가 거부.
  • failover 의 외부 효과(보상)는 멱등하게: 재기동·재인계가 정상 경로가 되도록.