모든 태그

# 서킷브레이커

2개의 글

감사가 찾은 풀 고갈 3중 오작동과 부하가 드러낸 서킷 stale 레이스를 k6 실측으로 잡다

1부는 방향을 뒤집어 전체 코드 감사를 돌립니다. 최대 발견은 풀 고갈의 3중 오작동입니다. PoolExhaustedException이 RuntimeException으로 모든 처리 경로를 관통해서, 계정계가 멀쩡한데 내부 풀 고갈 3연속이면 서킷이 열리고(오보), 고갈 요청은 500으로 터지며 원장에서 통째로 증발하고, 재시도 경로도 안 탑니다. 동일 시나리오(동시 8건 슬로우 계좌)를 수정 전과 후로 실측했습니다. 수정 전에는 500이 4건에 원장 5행(4건 증발), 서킷 OPEN, 직후 멀쩡한 계좌까지 503 거절이었고, 수정 후에는 503 4건('포화 상태' + 거래ID), 원장 9행 완결, 서킷 CLOSED, 직후 멀쩡한 계좌는 200이었습니다. 유휴 커넥션 TTL, 채번기 자정 재시드, EUC-KR 무음 '?' 치환의 fail-closed 전환, API 키 기동 로그 노출 제거 등 확정 결함을 소탕하고 회귀 테스트 18건으로 고정했습니다(129→147). 2부는 감사가 '난이도 대비 실익이 낮다'며 미룬 A3(서킷 stale 결과 귀속)를 부하가 실증한 이야기입니다. 서킷이 열렸다 닫혔다 하는 15초 부하 창에서 stale 결과 보고가 197번 발생했고(staleResultsTotal=197), acquire()가 상태 세대를 담은 permit 토큰을 발급해 세대가 일치할 때만 상태에 반영하게 고쳤습니다. 그리고 k6로 세 가지를 실측했습니다. 무릎 약 10~12k req/s(6k까지 p95<1ms, 실패율 전 구간 0%), 게이트웨이 경유 오버헤드 약 0.21ms/req(병목은 TCP 풀이 아니라 웹 계층으로, 커넥터 40k vs 전체 경로 12k req/s), 죽은 백엔드에서 서킷 off 351 req/s(p50 8.11s) vs on 9,425 req/s(p50 0.68ms)입니다. CI(GitHub Actions), MIT LICENSE, Spring Boot 3.5.4 업그레이드(150건 그린)까지 함께 소진했습니다.

타임아웃을 실패로 단정하지 않는 3값 원장, 그리고 손으로 짠 서킷브레이커와 망취소

1부는 아무것도 기억하지 못하던 게이트웨이에 기억을 붙입니다. 모든 거래에 거래고유번호를 채번하고(스레드 안전 + 재기동 안전), 결과를 3값 상태(SUCCESS/FAILED/UNKNOWN)로 원장에 적재합니다. 핵심 규칙은 타임아웃을 임의로 실패 처리하지 않는 것입니다. 응답을 못 받은 거래는 계정계에서 처리됐을 수 있어 FAILED가 아니라 UNKNOWN으로 적습니다. 목업 계정계에 응답 지연 모드를 넣어 진짜 read 타임아웃(3.06초 → 504)을 일으키고 원장에 UNKNOWN이 남는 것을 실측했습니다. 적재는 비동기라 거래를 막지 않고, 계좌는 저장 직전 마스킹되며, correlation ID가 로그와 원장을 한 줄로 꿰고, 토큰버킷·커넥션 풀·TCP 왕복이 Prometheus 커스텀 메트릭으로 노출됩니다. 2부는 남은 폭탄 둘을 처리합니다. 계정계 프로세스를 실제로 죽여 손으로 짠 서킷브레이커(CLOSED→OPEN→HALF_OPEN)가 502 → 503×4 즉시 거절로 장애를 격리하고 재기동 후 탐침으로 닫히는 전 과정을 확인했고, UNKNOWN 해소도 완성했습니다. 원거래 전문에 거래고유번호를 실어(30→52byte) 거래상태조회로 처리 여부를 확인해, 처리됐으면 망취소로 CANCELED, 미처리면 그제야 FAILED로 확정합니다. 조회성 거래만 지수 백오프로 제한 재시도하고(변경성 재시도는 이중 거래라 코드가 0회를 강제), 거래 단위 데드라인이 마지막 재시도의 read 제한까지 깎습니다. ddl-auto:update가 체크 제약을 갱신하지 않아 CANCELED UPDATE가 거부된 함정과, 그 실패 순간 이미 나간 망취소를 멱등성이 구해 준 이야기도 정직하게 적었습니다.