BE-commerce 프로젝트
약 9분 분량 결제·정산성능·용량

떠난 고객의 결제를 빼도 PG 호출은 8.4%만 줄었다

목차

고객이 이미 떠난 결제는 PG를 부르지 않게 했더니 PG 호출은 8.4%만 줄었고 상한 거절은 609건에서 169건으로 줄었습니다. 요청의 30%를 떠난 결제로 보냈으니 PG 호출도 30% 줄 거라고 적었는데 틀렸습니다. PG 동시 호출 상한이 이미 차 있어서 떠난 결제가 비운 자리를 남은 결제가 바로 채웠습니다. 줄어든 것은 PG 호출보다 남은 고객이 상한에 막히는 일이었습니다.

이 수치는 요청의 30%에 “남은 시간 0”을 실은 부하 스크립트로 잰 것입니다. 실제 결제 화면과 똑같이 움직이는 고객으로 다시 재자 PG 호출을 뺀 결제는 0건이었습니다. 이 글의 뒤쪽 절반은 그 이유와 그 뒤에 고친 것입니다.

결론부터

  • 데드라인을 검사해도 PG 호출은 30%가 아니라 8.4%만 줄었습니다. 비운 동시 호출 자리를 아직 유효한 결제가 바로 채웠기 때문입니다. 대신 상한 거절은 609건에서 169건으로 줄었습니다.
  • 부하 스크립트에 남은 시간 0을 직접 넣은 실험은 서버의 자원 회수 효과를 보여 줬지만 실제 화면의 동작을 재현하지 못했습니다. 결제 화면과 같은 고객 흐름으로 다시 재자 PG 호출을 생략한 결제는 0건이었습니다.
  • 따라서 데드라인 전파는 “요청 수를 비율대로 줄이는 기능”이 아니라, 더 이상 기다리지 않는 요청이 PG 동시성 자원을 차지하지 않게 하는 입장 제어로 봐야 합니다. 클라이언트가 보내는 값은 절대 시각이 아니라 중간 계층에서 줄여 전달하는 남은 시간이어야 합니다.

떠난 고객의 결제를 빼도 PG 호출은 8.4%만 줄었다

떠난 고객의 결제가 자리를 차지한다

결제 확정은 재고 예약, PG 승인, 확정 순서로 돌고 PG 승인은 트랜잭션 밖에서 부릅니다. PG가 느려지면 승인 하나가 읽기 타임아웃(2초)까지 PG 동시 호출 상한 40의 자리를 쥡니다. 그 사이 고객이 기다리다 화면을 떠나도 서버는 그것을 모릅니다. 떠난 고객의 결제가 아직 기다리는 고객의 자리를 쓰는 구조였습니다. Google SRE 책이 데드라인 전파로 다루는 낭비와 같은 모양입니다.

앞 글의 재전송 예산이 “같은 결제가 자리를 두 번 쥐는 것”을 막았다면(재전송을 켰더니 느린 PG에서 결제 실패가 늘었다) 이번에는 “이미 쓸모없어진 결제가 자리를 쥐는 것”을 막으려 했습니다.

어디서 끊고 무엇을 받을지

끊는 자리. 상한의 자리를 실제로 쥐는 곳은 PG 클라이언트입니다. 거기서 데드라인을 보려면 승인 요청 객체에 필드를 더해야 합니다. 이 객체는 토스 · 카카오페이 · 가짜 PG 어댑터가 함께 씁니다. 그래서 PG를 부르기 바로 앞인 결제 서비스에서 끊었습니다. 어댑터를 건드리지 않고 상한에 들어가기 전에 막으니 “PG에 닿지 않은 것이 보장된다”는 기존 규칙을 그대로 탑니다. 상한 초과나 서킷 열림처럼 UNKNOWN으로 두지 않고 확정 실패로 적습니다.

받는 값. 처음에는 X-Client-Deadline-Ms 헤더로 클라이언트 시계의 절대 시각을 받았습니다. “지금부터 N밀리초” 같은 상대 시간은 중간 서버를 거치며 다시 채워질 수 있다고 봤기 때문입니다. 그런데 이 방식은 클라이언트 시계가 서버보다 빠르면 아직 기다리는 고객의 결제도 지난 것으로 판정합니다. gRPC의 grpc-timeout은 남은 시간을 넘기고 중간 서버는 그 남은 시간을 줄여서 다음으로 넘깁니다. 제가 걱정한 “다시 채워지는” 문제는 이 방식에서 생기지 않았습니다.

그래서 X-Request-Timeout-Ms(남은 밀리초)를 받게 바꿨습니다. 서버는 요청을 받은 순간 자기 시계에 남은 시간을 더해 마감을 계산하고 그 뒤로는 서버 시계끼리만 비교합니다. 두 시계가 달라도 판정이 흔들리지 않습니다. 0 이하는 이미 지난 것으로 봅니다. 헤더가 없으면 예전처럼 확인하지 않습니다.

비교 방법. 같은 코드와 같은 jar로 “넣기 전”을 재려고 확인을 켜고 끄는 설정을 뒀습니다. 두 커밋을 따로 빌드해 비교하면 다른 변경이 섞일 수 있습니다.

측정 전에 적은 예상

가짜 PG에 승인마다 5초 지연을 넣고 읽기 타임아웃은 2초로 뒀습니다. 모든 승인이 타임아웃되니 성공은 0건이고 이 실험은 누가 PG의 자리를 쓰는지를 봅니다. 초당 30건을 60초 보내면서 요청의 30%에는 남은 시간 0을, 나머지에는 30초를 실었습니다. 이슈에 적은 예상의 핵심은 셋이었습니다.

  • PG 호출 수가 떠난 결제의 몫만큼(약 30%) 준다
  • 확인을 끄면 떠난 결제는 대부분 UNKNOWN으로 남는다
  • 확인을 켜면 떠난 결제는 모두 확정 실패가 되고 PG를 부르지 않은 수가 떠난 결제 수와 같다

결과

지표확인 끔켬
PG로 나간 승인 호출(재전송 포함)1,5061,379(−8.4%)
상한 거절609169(−72.2%)
남은 시간이 다 돼 PG를 부르지 않은 결제0615
떠난 결제 중 UNKNOWN(PG에 닿음)69.9%0%
남은 결제 중 PG에 닿아 결과를 기다린(UNKNOWN) 비율68.3%87.4%

PG를 부르지 않은 615건은 이번 실행의 떠난 결제 수와 정확히 같았습니다. 확인을 끈 실행에는 이 카운터가 한 번도 오르지 않았습니다. 기능이 실제로 작동했다는 증거를 수치 안에 남기려고 카운터를 따로 뒀습니다.

이 측정은 같은 노트북에서 다른 부하 스크립트가 도는 동안 진행됐습니다. 판정은 CPU 경합에 둔감한 건수로 했습니다.

왜 8.4%였나

두 실행 모두 PG 동시 호출이 평균 37~38개로 상한 40에 붙어 있었습니다. 총 호출 수는 떠난 결제의 수보다 상한이 정하고 있었습니다. 떠난 결제가 놓은 자리는 남은 결제가 곧바로 채웠습니다. 그래서 호출 수는 거의 그대로이고 누가 그 자리를 쓰는지가 바뀌었습니다. 남은 결제가 PG에 닿은 비율이 68.3%에서 87.4%로 늘었습니다.

상한 거절이 72% 줄어든 것도 같은 이유입니다. 떠난 결제 615건이 상한 앞에서 빠지면서 남은 결제가 막힐 일이 그만큼 줄었습니다. 나머지 두 예상은 맞았습니다.

처음 측정과 수치가 다른 이유

헤더를 바꾸기 전 처음 측정에서는 PG 호출이 1,517건에서 1,391건(−8.3%), 상한 거절이 1,094건에서 563건이었습니다. 확인을 끈 조건의 상한 거절부터 두 배 가까이 달랐습니다.

헤더 형식 때문이 아니었습니다. 처음 측정은 재전송 예산의 기본값을 켜기 전이라 예산 없이 재전송하고 있었습니다. 이번 “끔”의 상한 거절 609건은 재전송 예산 실험에서 재전송을 안 한 조건(616건)과 거의 같습니다. 그때는 재전송이 자리를 한 번 더 쥐어 상한 거절이 많았습니다. 떠난 결제도 55.4%가 상한 거절로 먼저 걸러져 “떠난 결제는 대부분 UNKNOWN”이라는 예상이 틀렸습니다. 예산이 켜진 이번에는 69.9%가 UNKNOWN이라 그 예상도 맞았습니다.

PG 호출이 8%대만 준다는 결론은 두 측정에서 같았습니다. 앞 글의 재전송 예산과 이번 데드라인이 같은 자원을 두고 서로 영향을 준다는 것이 수치로 드러났습니다.

결제 화면에도 실었다

처음 이 글에는 저장소에 결제를 부르는 화면이 없다고 적었는데 틀렸습니다. 주문/결제 화면(checkout.html)이 있었고 헤더 없이 응답을 끝까지 기다리고 있었습니다. 헤더가 없으면 서버는 확인을 건너뛰니 이 기능은 부하 스크립트에서만 작동하고 있었습니다.

화면이 승인 요청에 남은 시간 15초를 싣고 같은 15초가 지나면 기다림을 멈추게 했습니다. 15초는 당시 운영 설정의 PG 읽기 타임아웃 5초로 첫 시도와 재전송 한 번을 기다리고 5초를 더한 값입니다. 서버에 보내는 값은 아래 측정 뒤 27초로 고쳤습니다. 읽기 타임아웃을 60초로 바꾸면서 조회 창과 함께 65초가 됐습니다(짧은 PG 타임아웃은 우리 쪽 대기만 줄였다).

기다림을 멈춘 뒤의 처리가 더 중요했습니다. 화면이 포기해도 서버는 PG 호출을 계속하고 있을 수 있습니다. 여기서 결제 실패를 보여 주면 고객이 다시 눌러 같은 주문에 결제를 두 번 시도합니다. 응답이 늦은 경우는 결과를 모르는 UNKNOWN(202 응답)과 같게 다루고 주문 조회로 결과를 확인하게 했습니다.

가짜 PG 지연을 20초로 두고 브라우저에서 결제하자 15.5초에 “응답이 늦어 결제 결과를 확인하고 있습니다”로 바뀌었고 21.6초에 조회로 결제 완료를 받았습니다. 주문마다 결제는 한 건이었습니다.

화면과 똑같이 움직이는 고객으로 다시 쟀다

브라우저 확인은 한 명이 한 번 결제한 것입니다. 화면 경로에서 데드라인이 실제로 서는지는 부하로 재야 했습니다. 새 k6 스크립트는 화면이 하는 일을 그대로 합니다. 헤더에 15초를 싣고 15초에 응답을 끊고 결과를 모르면 1.2초 간격으로 10번 주문을 조회합니다. 요청의 2%에는 남은 시간 0을 실어 데드라인 확인 자체가 작동하는지도 함께 봤습니다.

재기 전에 예상을 적었습니다. 서버 안에서 PG를 부르기 전까지 쓰는 시간은 짧습니다. DB 커넥션 대기는 3초에서 끊고 PG 동시 호출 상한은 기다리지 않고 바로 거절합니다. 화면이 보낸 15초가 서버 안에서 다 쓰일 일이 없고 데드라인은 서지 않을 거라고 봤습니다.

조건화면 요청15초에 끊음끊은 뒤 조회로 결제 완료데드라인으로 뺀 화면 결제뺀 대조군 / 대조군
운영 설정, PG 5초 지연2,9400062 / 62
운영 설정, PG 8초 지연(5초에 끊겨 결과 모름)2,9350072 / 72
PG 20초 지연, 읽기 타임아웃 25초293120120010 / 10
상한을 끄고 톰캣 워커가 마름2,1051,3001,300049 / 49

운영 설정은 당시 값인 읽기 타임아웃 5초 · 상한 40 · 초당 50건 · 60초입니다. 대조군은 매번 전부 빠져서 0이 “확인이 돌지 않았다”는 뜻이 아닙니다. 다른 부하 스크립트가 같은 노트북에서 도는 중이라 판정은 건수로만 했습니다.

예상대로였습니다. 운영 설정에서는 읽기 타임아웃과 재전송 예산이 응답을 10.1초 안으로 묶어 화면이 기다림을 멈출 일이 없었습니다. 워커가 말라 고객의 62%가 15초를 넘겨 끊었을 때도 서버는 몰랐습니다. 서버는 헤더를 받은 순간부터 시간을 재서 톰캣 대기열에서 기다린 시간이 빠집니다.

기다림을 멈춘 고객은 떠나지 않았다

표의 마지막 줄에서 예상하지 못한 것이 보였습니다. 15초에 끊은 1,300명은 모두 조회로 결제 완료를 받았습니다. 화면은 응답을 끊은 뒤에도 12초 동안 조회로 결과를 기다립니다. 데드라인이 15초 기준으로 섰다면 이 1,300건은 PG를 부르지 않고 “결제가 진행되지 않았습니다”로 끝났습니다.

화면이 보내던 15초는 고객이 결과를 기다리는 시간보다 짧았습니다. 남은 시간을 응답 대기 15초와 조회 창 12초를 더한 27초로 고쳤습니다. 값이 커지면 뺄 결제는 늘 수 없으니 위 측정은 그대로 유효합니다. 브라우저에서 헤더에 27000이 실리는 것을 확인했습니다.

서버의 확인은 켜 두었습니다. 비용은 시각 비교 한 번이고 이미 지난 요청을 싣는 클라이언트가 생기면 바로 작동합니다. 다만 지금 결제 화면에 대해서는 효과가 있다고 말하지 않습니다.

대가와 재지 않은 것

  • 남은 시간은 요청이 서버에 닿기까지 걸린 네트워크 시간과 톰캣 대기열 시간을 빼지 못합니다. 대기열 시간을 세려면 앞단 프록시가 요청을 받은 시각을 헤더로 찍어 줘야 하는데 지금은 프록시가 없습니다
  • PG 8초 지연에서 결과를 모르는 응답(202)을 받은 480명은 전원 12초 조회 안에 결과를 받지 못했습니다. 복구 배치는 60초 간격입니다. 브라운아웃 중 조회마다 PG를 부르면 느린 PG에 부하를 더해 바꾸지 않았고 화면은 주문내역에서 확인하라고 안내합니다
  • 화면이 기다림을 멈췄을 때 서버가 이미 PG를 부르고 있으면 그 호출은 끝까지 갑니다. 데드라인은 PG를 부르기 전에만 봅니다
  • 떠난 결제의 비율(30%)을 바꿔 가며 재지 않았습니다
  • 상한이 차지 않은 평소 구간은 재지 않았습니다. 그 구간에서는 고객이 떠나기 전에 응답이 가서 이 확인이 거의 관여하지 않을 것으로 보지만 확인하지 않았습니다

정리

  • 상한이 찬 상태에서는 떠난 결제를 빼도 총 호출 수는 줄지 않고 그 자리를 누가 쓰는지만 바뀝니다. 호출 수로 효과를 재면 8.4%로 작게 보이고 상한 거절과 남은 결제의 PG 도달로 재야 효과가 보였습니다
  • 데드라인은 절대 시각보다 남은 시간으로 받는 것이 맞았습니다. 받는 쪽이 자기 시계로 마감을 계산하면 시계 차이라는 대가가 사라집니다
  • 같은 실험을 다시 재자 다른 PR이 바꾼 기본값 때문에 기준선부터 달라졌습니다. 수치를 옮겨 적기 전에 무엇이 바뀐 뒤의 측정인지를 먼저 봐야 했습니다
  • 화면이 기다림을 멈춘 결제는 결과를 모르는 상태로 보고 조회로 확인해야 고객이 다시 눌러 결제를 두 번 시도하지 않습니다
  • 부하 스크립트가 만든 “이미 떠난 고객”은 실제 화면이 만들지 않는 요청이었습니다. 앞의 상한과 타임아웃이 응답 시간을 이미 묶고 있어 화면 경로에서는 데드라인이 설 자리가 없었습니다. 효과를 적기 전에 실제 클라이언트와 똑같이 움직이는 부하로 재야 했습니다
  • 남은 시간은 고객이 결과를 기다리는 전체 시간이어야 합니다. 응답을 끊은 뒤에도 조회로 기다리는 화면은 그 조회 창까지 더해 보내야 합니다
프로필 사진
작성자 @범수

오늘의 노력이 내일의 전문성을 만든다고 믿습니다.

댓글

댓글 수정/삭제는 GitHub Discussions에서 가능합니다.