두 시스템에 데이터를 동시에 넣어야 했는데 각각 다른 DB 다.
A에 기록 → B에 기록
A 는 됐는데 B 가 실패하면 어떻게 되나.
Table of contents
Open Table of contents
트랜잭션이 안 걸렸다
한 DB 안이면 COMMIT 과 ROLLBACK 으로 묶어서 둘 다 실패하거나 둘 다 성공하게 만든다. DB 가 둘이면 그것이 안 된다.
묶어 주는 방식이 아예 없는 것은 아니다. XA 가 그 자리에 있고 매뉴얼도 여러 트랜잭션 자원이 하나의 전역 트랜잭션에 참여하게 해 준다고 적는다.
다만 준비할 것이 많다. 먼저 모든 갈래에 PREPARE 를 걸어 커밋할 수 있는지 묻는다. 그다음에 COMMIT 이나 ROLLBACK 을 보내는 두 단계로 돌아간다. 그 순서를 지휘하는 트랜잭션 매니저가 따로 있어야 한다. 양쪽 DB 가 다 지원해야 하는데 이 규모에는 과했다.
순서를 정했다
먼저 어느 쪽이 먼저인가를 정했다. 기준은 되돌리기 쉬운 쪽을 나중에 두는 것이었다.
1. B에 기록 (되돌리기 어려움 — 외부 시스템)
2. A에 기록 (되돌리기 쉬움 — 우리 DB)
이러면 2번이 실패했을 때 1번을 취소하는 것보다 1번이 실패하면 2번을 안 하는 것이 쉽다. 순서 하나로 나쁜 경우의 무게가 달라진다.
대기 테이블을 만들었다
순서를 정해도 1번은 성공하고 2번이 실패하는 경우가 남는다. 이때 선택지가 몇 가지였다.
보상 처리 1번을 취소하는 요청을 보낸다 — 그것도 실패할 수 있다
재시도 2번을 다시 시도한다 — 언젠가 되면 정합이 맞는다
기록하고 사람이 불일치를 남기고 사람이 처리한다
셋을 조합했다. 2번이 실패하면 대기 테이블에 넣었다.
pending_sync
ref_id
payload
attempts
last_error
created_at
배치가 주기적으로 pending_sync 를 처리해서 성공하면 지우고 몇 번 실패하면 상태를 바꾸고 알린다.
즉시 정합은 포기하고 최종 정합으로 갔고 몇 초에서 몇 분 뒤에 맞는다. 이것이 이 업무에 허용됐다. 실시간 정합이 필요한 것이었으면 다른 설계를 해야 했다.
검증 — 중복 방지와 불일치 집계
재시도하면 같은 요청이 두 번 갈 수 있다. 2번이 실제로는 성공했는데 응답이 유실돼서 실패로 보인 경우다.
{ "request_id": "a3f2c1...", "data": {...} }
요청에 request_id 를 넣어 같은 키를 이미 처리했으면 무시하게 했다. 받는 쪽이 그것을 지원해야 한다. 안 하면 우리가 확인 조회를 먼저 해야 하는데 그것도 완벽하지는 않다.
이 구조는 일시적 불일치를 전제하므로 그것이 얼마나 되는지 알아야 한다. 야간 배치로 양쪽을 대조했다.
A에만 있음: N건
B에만 있음: M건
평소 수치를 알아 두니 갑자기 늘면 신호가 된다. 그리고 pending_sync 에 오래 남아 있는 건이 있는지 봤는데 며칠째 재시도 중인 건은 자동으로 안 풀리는 것이다.
순서를 바꿀 수 없는 경우
한 가지는 순서를 못 정했다. 상대가 우리 쪽 ID 를 요구하는데 그 ID 는 우리가 기록해야 생긴다.
A에 기록 → ID 생성 → B에 그 ID로 기록
A 가 먼저일 수밖에 없다. 이때는 A 에 임시 상태로 넣고 B 가 성공하면 확정으로 바꿨다.
A: status = 'pending'
B: 기록 성공
A: status = 'confirmed'
미확정 상태가 조회에서 빠지도록 했다. 그러면 B 가 실패해도 사용자에게는 안 보이고 중간에 실패한 것만 골라내면 된다.
정리
- DB 가 둘이면 트랜잭션으로 못 묶는다
XA는 두 단계로 돌고 트랜잭션 매니저가 따로 있어야 해서 대개 과하다- 되돌리기 어려운 쪽을 먼저 한다
- 순서 하나로 나쁜 경우의 무게가 달라진다
- 중간 실패는 대기 테이블과 배치 재시도로 최종 정합을 만든다
- 순간적으로는 어긋나도 결국 같아지는 것을 목표로 한다
- 재시도하면 중복이 갈 수 있으니 고유 키로 막는다
- 고유 키는 받는 쪽이 지원해야 한다
- 불일치를 정기적으로 세고 평소 수치를 안다
- 순서를 못 바꾸면 미확정 상태로 넣고 확정 단계를 둔다