쌓인 미확정 결제는 언제 풀리는가: 복구 시간의 하한과 상한, 그리고 알림을 나이에 거는 이유
목차
PG 타임아웃으로 쌓인 미확정 결제 887건이 전부 확정되기까지 걸린 시간은 복구 배치의 속도가 아니라 코드에 박힌 상수 1분이 정했습니다. 청크를 50건으로 줄이자 그때부터는 청크 ÷ 주기가 정했고 곡선이 틱마다 정확히 50건씩 줄어드는 직선이 됐습니다. 두 식을 합치면 기본 설정에서 미확정 1,351건은 약 3.7분, 1만 건은 약 20분 뒤에 전부 풀린다고 미리 계산됩니다.
결론부터
- 미확정 결제의 해소 시간은
MIN_AGE1분과 청크·주기가 결정했습니다. - 청크 500건·주기 5초에서는 61초, 청크 50건에서는 106초에 전량 해소됐습니다.
- 운영 기본값에서는 1,351건이 약 3.7분, 1만 건이 약 20분 걸린다고 미리 계산할 수 있습니다.
적체 알림은 건수보다 가장 오래된 미확정 결제의 나이에 걸어야 새 작업이 계속 들어오는 정상 상태와 오래 방치된 상태를 구분할 수 있습니다.
미확정 결제는 왜 쌓이는가
미확정(UNKNOWN) 결제는 PG에 승인 요청을 보냈는데 응답이 오기 전에 타임아웃이 난 결제입니다. PG가 실제로 승인했는지 우리 쪽에서는 모르기 때문에 성공으로도 실패로도 적지 않고 남겨 둡니다. 복구 배치가 주기적으로 PG에 그 결제의 상태를 다시 물어 확정합니다.
PG 읽기 타임아웃을 5초에서 2초로 조이는 실험에서 결제 p95는 14.26초에서 2.13초로 줄었지만 그 구간 결제 1,351건이 전부 미확정이 됐습니다. 그때 기록에는 “복구 배치 일거리가 0건에서 1,351건이 된다”까지만 적었고 그 일거리가 언제 어떻게 풀리는지는 재지 않았습니다. 이 글은 그 빈칸을 잰 기록입니다. 이어서 같은 결론이 알림 설계에 어떻게 쓰였는지를 다룹니다.
측정은 가짜 PG에 지연을 넣고 한 대의 맥에서 했습니다. 가짜 PG는 타임아웃이 난 요청을 모두 실제로는 승인한 것으로 응답하므로, 실제 PG 장애에서 섞여 나올 다른 상태는 이 측정에 없습니다.
어떻게 쟀나
PG 지연 5초, 읽기 타임아웃 2초로 45초 동안 결제 부하를 줘서 미확정을 만들었습니다. 복구 배치를 켠 채로 payments 테이블의 상태별 건수를 2초마다 DB에서 직접 셌습니다. 응답 코드나 애플리케이션 지표는 쓰지 않았습니다. 결제가 확정됐는지를 판정하는 진실은 그 테이블이기 때문입니다.
| 청크 | 주기 | 만든 미확정 | 첫 해소 | 전량 해소 | 최종 상태 |
|---|---|---|---|---|---|
| 500 | 5초 | 887 | t+18s | t+61s | 전량 DONE |
| 500 | 5초 (재현) | 886 | t+18s | t+62s | 전량 DONE |
| 50 | 5초 | 885 | t+18s | t+106s | 전량 DONE |
t는 부하가 끝난 시각입니다. 실험에서는 복구 주기를 5초로 두었고 운영 기본값은 60초입니다.
하한은 MIN_AGE가 정한다
복구 서비스에는 MIN_AGE = 1분이라는 상수가 있습니다. 요청한 지 1분이 안 된 미확정은 복구 대상에서 뺍니다. 아직 진행 중인 정상 요청을 복구가 건드리지 않게 하려는 값입니다.
청크가 넉넉하면 해소 곡선은 미확정이 생긴 곡선을 정확히 1분 뒤로 민 모양이 됩니다. 부하는 45초였으므로 첫 미확정은 t−45s에 생겨 t+15s에 복구 자격을 얻습니다. 실측 첫 해소는 t+18s였습니다. 마지막 미확정은 t에 생겨 t+60s에 자격을 얻고 실측 전량 해소는 t+61s였습니다. 몇 초 차이는 5초 주기의 틱 간격 안에 들어갑니다.
이 구간에서 복구 배치는 병목이 아니었습니다. 배치를 아무리 빨리 돌려도 1분이 지나기 전에는 풀 수 있는 결제가 없습니다.
상한은 청크 ÷ 주기가 정한다
틱마다 자격을 얻는 결제가 청크보다 많아지면 그때부터는 배치가 병목입니다. 청크를 50건으로 줄여 이 상황을 만들었습니다. 곡선은 885, 835, 785, 735로 틱마다 정확히 50건씩 줄어드는 직선이 됐습니다.
해소율 = 청크 ÷ 주기 = 50 ÷ 5초 = 10건/초전량 해소 = 첫 자격(t+18s) + 885 ÷ 10 = t+106.5s 실측 t+106s두 식을 합치면 전량 해소 시각은 둘 중 늦은 쪽입니다. 하나는 마지막 미확정이 생기고 1분 뒤이고 다른 하나는 첫 자격 시각에 적체 ÷ 해소율을 더한 시각입니다. 청크 500건 실험에서는 앞의 것(t+60s)이, 청크 50건 실험에서는 뒤의 것(t+106.5s)이 늦었고 실측이 각각 그 값을 따랐습니다. 기본값인 주기 60초, 청크 500건을 넣으면 해소율은 초당 약 8.3건입니다.
| 쌓인 미확정 | 전량 해소까지(기본값) |
|---|---|
| 500 | 1분 (청크에 안 걸리고 MIN_AGE만) |
| 1,351 | 약 3.7분 |
| 10,000 | 약 20분 |
적체가 청크를 넘기 시작하면 복구 시간은 적체에 비례해 길어집니다. 청크나 주기를 바꿀 때 이 표를 다시 재지 않고 식으로 고칠 수 있게 된 것이 이 측정의 쓸모입니다.
887건 전부 DONE이었다
복구하는 동안 ABORTED는 0건이었고 887건이 전부 DONE으로 확정됐습니다. 우리 쪽에서는 타임아웃이었지만 PG 쪽에서는 승인이었기 때문입니다. 가짜 PG가 바로 그 상황을 재현하도록 만들어져 있습니다.
타임아웃을 실패로 단정하지 않는 이유가 이 숫자에 있습니다. 복구가 PG에 다시 묻지 않고 실패로 적었다면 887건이 전부 잘못된 실패가 됩니다. 고객 카드에서는 돈이 나갔는데 우리 기록은 실패인 결제 887건입니다.
그래서 알림은 가장 오래된 건의 나이에 건다
복구 시간이 식으로 나온다는 것은 알림에 무엇을 걸어야 하는지도 정해 줍니다. 미확정 건수는 “많다”는 것만 알려 줍니다. 1,351건이 쌓여도 3.7분 안에 풀리면 운영자가 할 일은 없습니다. 문제는 청크가 적체를 못 따라가는 순간인데, 그때 늘어나는 값은 가장 오래 기다린 결제의 나이입니다. 청크가 충분하면 그 나이는 1분 근처에 머물고 밀리기 시작하면 1분을 넘어 계속 자랍니다.
미확정 결제 알림(UnknownPaymentAging)은 이미 이렇게 걸려 있었습니다. 가장 오래된 미확정의 나이가 600초를 넘은 상태가 5분 이어지면 울립니다.
같은 성격의 큐 가운데 대사 적체 알림 하나만 건수에 걸려 있었습니다. 대사는 내 결제 기록과 PG 정산 파일을 대조하는 작업이고 규칙으로 가르지 못한 불일치는 사람이 확인하는 큐(PENDING)로 갑니다. 그 알림(ReconPendingBacklog)의 식은 이랬습니다.
# 전expr: recon_pending_count > 0for: 15m# 후expr: recon_pending_oldest_age_seconds > 900for: 5m건수 조건에는 두 가지 문제가 있었습니다. 먼저 사람이 제때 종결해도 새 불일치가 계속 들어오면 큐가 비지 않으므로 알림이 울립니다. 아무것도 방치되지 않았는데 울리는 오탐입니다. 반대로 스크레이프가 한 번 비면 for가 처음부터 다시 세기 때문에, 실제로 방치된 건이 있어도 알림이 늦어질 수 있습니다.
나이로 바꾸면서 정한 것은 셋입니다.
- 예상 시간은 1시간이었고 나이 게이지도 새로 만들어야 한다고 봤습니다. 게이지(
recon.pending.oldest.age.seconds)는 이미 노출되고 있어서 알림 식과 테스트만 바꾸는 작업이었고 실제로 약 1시간이 걸렸습니다. - 임계 900초는 예전 약속(“15분 넘게 남아 있음”)을 그대로 옮긴 값입니다. 검토 SLA가 정해진 적이 없어 새 숫자를 만들지 않았습니다.
- 건수와 나이를 AND로 묶는 안은 버렸습니다. 나이가 0보다 크면 건수도 0보다 크므로 조건이 중복됩니다.
검증은 Prometheus 규칙 테스트로 했습니다. “계속 들어오지만 방치되지 않는 큐는 조용하다”와 “한 건이 방치되면 21분에 울린다” 두 케이스를 추가했습니다. 알림 식을 옛 식으로 되돌리면 두 케이스가 모두 실패하는 것을 확인했습니다. 테스트가 새 식이 아니어도 통과한다면 그 테스트는 아무것도 지키지 않기 때문입니다.
이 측정이 말하지 않는 것
- 실제 PG 장애의 상태 분포가 없습니다. 여기서는 PG 쪽이 전부 승인이었습니다. 실제로는
NOT_FOUND,CANCELED,IN_PROGRESS가 섞입니다.IN_PROGRESS는 다음 주기로 미뤄지므로 해소가 더 걸립니다. MIN_AGE는 코드 상수라 설정으로 줄일 수 없고 이 측정으로 바꾸지도 않았습니다. 그 값은 진행 중인 정상 요청과 겹치지 않으려고 둔 것이고 복구 속도와는 관계가 없습니다.- 복구 조회에는
ORDER BY가 없습니다.(status, requested_at)인덱스를 타면 오래된 순으로 읽힐 가능성이 높지만 보장된 순서는 아닙니다. 청크를 넘는 적체에서 어느 결제가 먼저 풀리는지는 이 측정이 따로 보지 않았습니다. - 대사 적체 나이의 기준은 대사가 수행된 시각(
reconciledAt)입니다. 같은 거래일 파일을 다시 올리면 기존 행을 지우고 다시 쓰므로 나이가 0부터 다시 시작하고 그만큼 알림이 늦어질 수 있습니다. 알림 규칙 주석에 적어 두고 이번에는 고치지 않았습니다.
이어서 읽기
이 글에서 이어진 작업은 따로 적었습니다.
참고
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.