핵심 요약
- 분산 환경의 가장 어려운 문제는 실패 자체보다 요청이 성공했는지 실패했는지 알 수 없는 결과 불명확성임
- 프로세스는 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 후 재시도는 저장된 결과를 찾도록 구성함
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와 승인된 재처리 절차를 사용함
- 안정적인
- 외부 API timeout에는 authoritative status 조회와 재조정 경로가 필요함
- 외부 제공자의 idempotency key와 내부
operation_id를 연결함 - 일정 시간 이상
pending인 operation을 reconciler가 조회함 - 자동 판정이 위험하면
review_required상태와 운영자 확인 절차를 제공함
- 외부 제공자의 idempotency key와 내부
재시도와 종료를 하나의 계약으로 설계함
-
모든 네트워크 호출에는 timeout, 재시도 가능 오류 분류, 최대 횟수, 전체 시간 예산이 필요함
- 연결 거부·일부
5xx·rate limit은 backoff와 jitter로 재시도할 수 있음 - 검증 실패·권한 오류·재고 부족 같은 영구 오류는 반복하지 않음
- 비멱등 쓰기 요청은 안정적인 키 없이 gateway가 자동 재시도하지 않게 함
- 연결 거부·일부
-
graceful shutdown은 가용성을 높이지만 정합성의 전제조건이 아님
SIGTERM수신 시 새 HTTP 요청과 queue lease 수신을 중지함- 처리 중 작업은 grace period 안에 끝내거나 안전한 checkpoint로 돌려놓음
- OOM과 host 장애에서는 종료 hook이 실행되지 않을 수 있으므로 어느 명령 사이에서 죽어도 재실행 가능해야 함
-
긴 작업은 HTTP 연결보다 오래 살아 있는 operation resource로 표현하는 편이 안전함
- 시작 요청은
202 Accepted와operation_id를 반환할 수 있음 - 조회 API는
pending,succeeded,failed,unknown,review_required를 구분함 - 클라이언트는 새 작업을 만들지 않고 같은 operation의 결과를 확인함
- 시작 요청은
실패 지점별 테스트가 배포 조건이 됨
-
장애 주입은 임의의 코드 줄보다 부작용 경계의 바로 전과 뒤에서 수행해야 함
- DB commit 직전과 직후에 프로세스를 강제 종료함
- 외부 API 성공 직후 응답을 유실시킴
- broker publish와 acknowledge 전후에 publisher·consumer를 종료함
- 동일한 HTTP 요청과 message를 동시에 여러 번 전달함
-
인프라 복구 뒤에도 비즈니스 측정값이 유지돼야 함
- 중복 주문·중복 결제·음수 재고가
0건이어야 함 - 만료 시간을 넘긴
pendingoperation이 탐지되고 재조정돼야 함 - outbox oldest age, retry count, DLQ count, unknown outcome count가 경보에 연결돼야 함
- log·trace·event가 같은
operation_id와 causation ID로 연결돼야 함
- 중복 주문·중복 결제·음수 재고가
결론 및 다음 글
- 첫 배포 기준은 복잡한 workflow 엔진이 아니라 불변식, 로컬 트랜잭션, 멱등성, Outbox, 결과 조회, 재조정의 증거임
- 이 최소선으로 해결되지 않는 여러 서비스의 장기 작업에서만 Saga와 명시적 상태 관리가 후보가 됨
- 다음 글에서는 Saga·상태 머신·오케스트레이터가 어려운 이유를 정상 흐름 밖의 비용으로 분석함
- 마지막 글에서는 OpenStack와 Kubernetes가 실패를 수렴시키는 방식을 애플리케이션 설계와 비교함
참고 문서
관련 글
분산 환경의 실패 처리분산 장애 처리 2편: Saga와 오케스트레이터의 숨은 비용Saga, 상태 머신, choreography, orchestrator가 정상 흐름 밖에서 만드는 전이·보상·버전·운영 비용과 도입 조건을 분석합니다.분산 환경의 실패 처리분산 장애 처리 3편: OpenStack와 Kubernetes는 실패를 어떻게 수렴시키는가OpenStack Nova와 Kubernetes가 전역 트랜잭션 대신 영속 상태, 비동기 명령, 멱등 조정 루프, fencing으로 분산 장애를 다루는 방식을 비교합니다.기술 글우리는 정말 페이지네이션을 잘 알고 있을까?offset, cursor, tombstone, scatter-gather와 fanout 관점에서 페이지네이션을 분석합니다.