분산 환경의 실패 처리

분산 장애 처리 1편: 요청은 어디까지 처리됐는가

프로세스 강제 종료와 응답 유실이 만드는 결과 불명확성을 분석하고 트랜잭션, 멱등성, Outbox, 재조정의 최소 안전선을 정의합니다.

검증일 근거 자료

핵심 요약

  • 분산 환경의 가장 어려운 문제는 실패 자체보다 요청이 성공했는지 실패했는지 알 수 없는 결과 불명확성
  • 프로세스는 DB 커밋, 외부 API 호출, 메시지 발행, 응답 전송 사이의 어느 지점에서든 종료될 수 있음
  • 응답 실패는 처리 실패의 증거가 아니므로 같은 명령을 안전하게 다시 확인하거나 실행할 수 있어야 함
  • 최소 안전선은 비즈니스 불변식, 로컬 DB 트랜잭션, 멱등성 키, Outbox, 제한된 재시도, 재조정 작업으로 구성
  • replica와 자동 재시작은 새 요청의 가용성을 높이지만 중단된 요청의 정합성을 복구하지 않음
  • Saga 같은 복잡한 패턴을 선택하기 전에 단일 저장소와 단순한 비동기 경계로 문제를 줄이는 것이 우선

한 요청에는 성공과 실패만 존재하지 않음

  • 클라이언트가 관측하는 결과와 서버가 영속화한 결과는 서로 다를 수 있음

    • 서버가 DB를 커밋하기 전에 종료되면 일반적인 로컬 트랜잭션은 rollback됨
    • 서버가 커밋한 뒤 응답 전에 종료되면 변경은 완료됐지만 클라이언트에는 timeout이나 5xx가 보임
    • 클라이언트가 새 요청으로 재시도하면 결제·주문·포인트가 중복될 수 있음
  • 네트워크 timeout은 실패가 아니라 결과를 모른다는 신호

    • 상대 시스템이 요청을 받지 못했을 수 있음
    • 상대 시스템이 성공했지만 응답만 유실됐을 수 있음
    • 상대 시스템이 아직 처리 중일 수 있음
    • 이 세 경우를 하나의 예외 처리로 합치면 안전한 재시도 정책을 만들 수 없음
종료 지점내구성 있는 상태클라이언트 관측안전한 다음 행동
DB 커밋 전변경 없음연결 종료 또는 5xx같은 키로 재시도
DB 커밋 후 응답 전변경 완료timeout 또는 5xx같은 키로 결과 조회·재시도
외부 API 호출 전내부 준비 상태만 존재처리 중 또는 실패저장된 명령 재전달
외부 성공 후 내부 결과 기록 전외부 효과만 존재할 수 있음unknown외부 참조 키로 조회 후 재조정
메시지 처리 후 acknowledge 전소비자 변경 완료같은 메시지 재전달멱등 consumer로 중복 제거

echo 서버와 트랜잭션 서비스는 실패의 의미가 다름

  • 무상태 echo 서버는 프로세스가 종료돼도 해당 요청 하나만 실패하는 경우가 대부분

    • 영구 데이터 변경이 없으므로 부분 커밋을 복구할 필요가 없음
    • 다른 replica가 새 요청을 받을 수 있지만 이미 끊긴 연결을 이어받지는 않음
    • resource 여유, probe, replica, timeout 검증이 주요 배포 조건임
  • 주문·결제 서비스는 실패한 응답 뒤에도 부작용이 남을 수 있음

    • 결제 승인은 완료됐지만 주문 저장이 실패할 수 있음
    • 재고 예약은 완료됐지만 event 발행이 실패할 수 있음
    • 환불은 완료됐지만 operation 상태가 계속 compensating에 머물 수 있음
  • 인프라 이벤트의 종류가 달라도 애플리케이션이 받는 결과는 비슷함

    • Kubernetes의 OOM kill·Pod 교체·node 장애가 프로세스를 끝낼 수 있음
    • OpenStack의 VM reboot·compute host 장애·network 단절이 연결을 끝낼 수 있음
    • 배포와 운영 자동화도 정상 요청 중간에 같은 종료 조건을 만들 수 있음

먼저 비즈니스 불변식을 저장소에 고정함

  • 한 번만 성립해야 하는 사실을 문장과 DB 제약으로 함께 정의해야 함

    • 하나의 order_id에는 성공한 결제가 최대 하나만 존재함
    • 하나의 payment_id에 대한 환불 총액은 결제 금액을 넘지 않음
    • 같은 reservation_id의 재고 예약은 한 번만 반영됨
    • 같은 멱등성 키에 다른 요청 본문이 오면 409 Conflict로 거절함
  • 애플리케이션의 사전 조회만으로 중복을 막을 수 없음

    • 두 요청이 동시에 존재 여부를 확인하면 둘 다 없음으로 판단할 수 있음
    • 최종 방어선은 DB PRIMARY KEY, UNIQUE, 조건부 UPDATE 같은 원자적 제약이어야 함
    • 제약 위반은 서버 오류가 아니라 기존 결과 조회로 연결할 수 있어야 함
  • 하나의 DB가 불변식을 보장할 수 있다면 변경을 하나의 로컬 트랜잭션으로 묶는 구성이 우선

    • 입력 검증과 권한 검사는 부작용 전에 수행함
    • 주문과 멱등성 결과를 같은 commit 단위에 기록함
    • commit 전 종료는 rollback되고 commit 후 재시도는 저장된 결과를 찾도록 구성함
idempotent-operation.sql
CREATE TABLE operations (
  scope text NOT NULL,
  idempotency_key text NOT NULL,
  request_hash text NOT NULL,
  status text NOT NULL,
  response_code integer,
  response_body jsonb,
  created_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (scope, idempotency_key)
);

DB 밖으로 나갈 때 내구성 있는 경계를 추가함

  • DB 변경과 message 발행을 연속 호출하는 dual write는 두 번째 호출이 실패하면 정합성이 깨짐

    • 비즈니스 변경과 outbox event를 같은 DB 트랜잭션에 저장함
    • 별도 publisher가 미발행 event를 broker로 전달함
    • publish 성공 직후 publisher가 죽을 수 있으므로 event 중복 전달을 허용함
  • consumer는 at-least-once 전달을 전제로 멱등하게 만들어야 함

    • 안정적인 message_id나 business key로 처리 이력을 구분함
    • 로컬 트랜잭션이 commit된 뒤에만 acknowledge함
    • 최대 재시도 뒤에는 dead-letter queue와 승인된 재처리 절차를 사용함
Ambiguous outcome and reconciliation path
  • 외부 API timeout에는 authoritative status 조회와 재조정 경로가 필요
    • 외부 제공자의 idempotency key와 내부 operation_id를 연결함
    • 일정 시간 이상 pending인 operation을 reconciler가 조회함
    • 자동 판정이 위험하면 review_required 상태와 운영자 확인 절차를 제공함

재시도와 종료를 하나의 계약으로 설계함

  • 모든 네트워크 호출에는 timeout, 재시도 가능 오류 분류, 최대 횟수, 전체 시간 예산이 필요

    • 연결 거부·일부 5xx·rate limit은 backoff와 jitter로 재시도할 수 있음
    • 검증 실패·권한 오류·재고 부족 같은 영구 오류는 반복하지 않음
    • 비멱등 쓰기 요청은 안정적인 키 없이 gateway가 자동 재시도하지 않게 함
  • graceful shutdown은 가용성을 높이지만 정합성의 전제조건이 아님

    • SIGTERM 수신 시 새 HTTP 요청과 queue lease 수신을 중지함
    • 처리 중 작업은 grace period 안에 끝내거나 안전한 checkpoint로 돌려놓음
    • OOM과 host 장애에서는 종료 hook이 실행되지 않을 수 있으므로 어느 명령 사이에서 죽어도 재실행 가능해야 함
  • 긴 작업은 HTTP 연결보다 오래 살아 있는 operation resource로 표현하는 편이 안전

    • 시작 요청은 202 Acceptedoperation_id를 반환할 수 있음
    • 조회 API는 pending, succeeded, failed, unknown, review_required를 구분함
    • 클라이언트는 새 작업을 만들지 않고 같은 operation의 결과를 확인함

실패 지점별 테스트가 배포 조건이 됨

  • 장애 주입은 임의의 코드 줄보다 부작용 경계의 바로 전과 뒤에서 수행해야 함

    • DB commit 직전과 직후에 프로세스를 강제 종료함
    • 외부 API 성공 직후 응답을 유실시킴
    • broker publish와 acknowledge 전후에 publisher·consumer를 종료함
    • 동일한 HTTP 요청과 message를 동시에 여러 번 전달함
  • 인프라 복구 뒤에도 비즈니스 측정값이 유지돼야 함

    • 중복 주문·중복 결제·음수 재고가 0건이어야 함
    • 만료 시간을 넘긴 pending operation이 탐지되고 재조정돼야 함
    • outbox oldest age, retry count, DLQ count, unknown outcome count가 경보에 연결돼야 함
    • log·trace·event가 같은 operation_id와 causation ID로 연결돼야 함

결론 및 다음 글

참고 문서