LabHub
배우기 러닝패스 코스

ICA — 이스티오 인증 어소시에이트 · 재시도와 업무 트랜잭션의 경계 · 이론

응답 실패와 업무 실패는 같은 사건이 아니다

LabHub 에서 이어서 보기

한 줄 요약

재시도는 응답을 다시 얻으려는 시도이지, 앞선 업무를 취소하는 명령이 아닙니다. 이 모듈은 실제 Istio 메시에서 HTTP 수신 횟수와 SQLite 업무 기록을 따로 세고, 응답이 유실되거나 프로세스가 죽어도 같은 업무를 중복 실행하지 않는 경계를 만듭니다.

왜 이게 필요했나

사용자가 결제 버튼을 한 번 눌렀는데 화면에는 시간 초과가 표시됐다고 합시다. 고객 지원 담당자는 실패했으니 다시 눌러 보라고 안내합니다. 서버 운영자는 프록시가 자동으로 두 번 더 재시도한다고 설명합니다. 그런데 업무 담당자는 같은 주문의 결제 기록이 세 건이라고 말합니다. 세 사람의 관측은 모두 사실일 수 있습니다.

화면의 요청 한 번, 프록시의 전송 시도 여러 번, 데이터베이스의 업무 쓰기는 서로 다른 사건입니다. HTTP 200만 세면 중복 업무가 보이지 않고, 504만 세면 이미 커밋된 업무를 취소된 것으로 오해합니다. 따라서 업무 키, 전송 요청 ID, 응답 코드, 실제 저장된 업무 ID를 함께 남겨야 합니다. 장애 보고서에 '세 번 실행됐다'고 쓰기 전에 무엇을 센 것인지부터 밝혀야 합니다.

LabHub의 선행 합성 실험에서도 이 차이를 관측했습니다. 같은 업무 키의 동시 요청 중 일부 클라이언트는 504를 받았습니다. 그중 한 요청은 서버 수신 기록의 마지막 상태가 200이었습니다. 업무 기록은 하나만 추가됐습니다. 이 비율을 모든 환경의 법칙으로 외우면 안 됩니다. 이번 실습은 부하에 따른 우연한 비율 대신 업무 커밋 뒤 응답을 2초 늦추는 비교 경로를 사용합니다.

어떻게 동작하나

세 개의 장부

client 파드에서 curl을 한 번 실행하면 요청 ID 하나를 붙입니다. Istio가 같은 요청을 재전송하면 업스트림의 receipts 장부에 같은 요청 ID가 여러 번 나타납니다. 업무 키는 별도 헤더입니다. 전송을 새로 시작하더라도 같은 주문을 회수하려는 요청이면 같은 업무 키를 유지합니다. 반대로 내용이 같아도 별개 주문이면 다른 키여야 합니다.

서버에는 고의로 잘못된 /naive 경로와 중복 방지 기록을 사용하는 /flaky 경로가 있습니다. 둘 다 첫 두 응답은 실제 업스트림 503, 다음 응답은 200입니다. 프록시에서만 만들어진 가짜 오류가 아닙니다. /naive는 수신할 때마다 업무를 추가하고, /flaky는 업무 키의 기존 응답을 재사용합니다. 같은 HTTP 응답열이어도 업무 효과가 다를 수 있습니다.

횟수 설정은 보장이 아니라 상한

VirtualService의 attempts는 최초 전송에 더할 재시도 상한입니다. attempts가 2이면 최초 시도를 포함해 최대 세 번입니다. 전체 timeout은 이 시도들이 공유하는 예산입니다. perTryTimeout은 각 시도가 기다릴 수 있는 상한이며, 빠르게 실패한 요청이 그 시간을 전부 사용한 것으로 계산하지 않습니다.

retryOn은 재시도할 실패 종류를 고릅니다. HTTP 503을 지정했다고 모든 연결 오류와 읽기 시간 초과까지 같은 조건으로 묶이는 것은 아닙니다. 이 실습은 단일 업스트림을 비교하기 위해 이전 호스트를 다시 고를 수 있게 설정합니다. 실제 서비스의 재시도 폭주, 백오프, 회로 차단과 용량 계획까지 이것으로 해결했다고 보지 마세요.

504 뒤에도 남아 있는 업무

/lost는 먼저 업무와 중복 방지 기록을 커밋하고 응답만 2초 늦춥니다. 이 경로의 프록시 예산을 500ms로 하고 자동 재시도를 끕니다. 클라이언트의 504와 서버의 업무 기록을 같은 키로 조회합니다. 이어 빠른 /charge 경로에 같은 키를 보내 기존 업무 응답을 회수합니다. 새 주문을 만들려는 것이 아니므로 여기서 새 키를 발급하면 안 됩니다.

이것은 외부 결제사의 API가 아닙니다. 합성 업무를 로컬 SQLite에 쓰는 서버입니다. 응답 기록의 200은 서버가 만들려던 응답이지 클라이언트가 그 바이트를 받았다는 증명도 아닙니다. 수신 기록과 클라이언트 원문을 구분하는 이유입니다.

원자성은 같은 트랜잭션 안에서

업무를 먼저 커밋하고 중복 방지 키를 나중에 쓰면 두 커밋 사이에 틈이 생깁니다. 그 틈에서 프로세스가 죽으면 업무는 남았지만 키는 없습니다. 같은 키의 재시도는 새로운 업무로 오인됩니다. 업무 쓰기와 키·내용 지문·응답 저장을 한 트랜잭션으로 묶어야 합니다.

SQLite는 한 시점에 하나의 쓰기 트랜잭션을 허용합니다. BEGIN IMMEDIATE로 쓰기 트랜잭션을 시작해도 영원히 잠금을 얻을 수 있는 것은 아니며 경쟁하면 BUSY나 시간 초과를 다뤄야 합니다. 실습은 하나의 로컬 DB 파일과 짧은 합성 작업만 사용합니다. 여러 DB나 외부 결제 호출을 한 번에 원자화하는 분산 트랜잭션을 구현한 것이 아닙니다.

현장에서 만나는 모습

서비스가 재시작해도 메모리 사전은 돌아오지 않습니다. 실습에서는 파드를 실제로 삭제·재생성하고 UID와 프로세스 instance가 바뀌었는지 확인합니다. 같은 VM 디스크에 남긴 SQLite 파일에서 기존 응답을 읽습니다. 파드 교체에 견뎠다는 사실을 VM 삭제·디스크 손상·전원 장애 복구로 확대하면 안 됩니다.

내용 지문도 필요합니다. 같은 키에 금액이나 고객을 바꿔 보내면 기존 응답을 무조건 돌려주는 대신 충돌로 거절해야 합니다. 반면 JSON 속성 순서가 바뀌었을 뿐이면 같은 내용으로 볼 수 있어야 합니다. 금액이 같은 다른 주문을 payload 지문만 보고 합치는 것도 오류입니다.

마지막에는 제공된 transaction.py의 분리 커밋 결함을 고칩니다. 검사기는 학생 구현을 새 임시 DB에서 실행하고 실제 자식 프로세스에 SIGKILL을 보냅니다. 커밋 전 두 지점에서는 업무와 키가 함께 없어야 하며, 커밋 후에는 함께 남아야 합니다. 그 후 새 연결로 재시도했을 때 업무가 하나인지 봅니다. 예외를 던지는 것만으로 강제 종료를 대신하지 않습니다.

다음 실습에서 할 것

파드 인벤토리, 재시도 라우트, 업무 중복 비교, 시간 초과, 내용 충돌, 파드 교체를 관측합니다. 이어 트랜잭션 코드를 수정하고 동시 요청과 다른 업무 키까지 검사합니다. 관측 도구는 실제 원문을 파일로 남기며 같은 관측 파일이 있으면 다시 실행하지 않습니다. 채점은 그 파일과 서버에 남은 수신 기록을 대조합니다.

업무 DB·테스트 파일·네임스페이스는 이 학습 VM 안에만 있습니다. 개인 키나 실제 결제 자료를 넣지 마세요. 실습 종료 시 모두 회수됩니다. 외부 시스템과 결합할 때 필요한 상대 API의 멱등 키, outbox, 재조정, 보상 처리는 후속 설계 과제이며 여기서 구현했다고 주장하지 않습니다.

공식 문서로 더 확인하기