· 박준성 (CEO / Junsung Park) · Technology · 12 min read
18.5밀리초는 어디서 왔는가
실시간 입찰(RTB) 환경에서 응답 기한을 넘긴 입찰은 가치가 0입니다. 아무리 정교한 비더를 만들었더라도, 마감이 지난 뒤에 도착한 응답은 그대로 버려지기 때문입니다. 그래서 제가 추적해야 했던 숫자는 평균 지연 시간이 아니라 '꼬리 지연'이었습니다. 이 글은 그 꼬리 지연 하나를 집요하게 추적하고 해결한 기록입니다.

실시간 입찰(RTB) 환경에서 응답 기한을 넘긴 입찰은 가치가 0입니다. 아무리 정교한 비더를 만들었더라도, 마감이 지난 뒤에 도착한 응답은 그대로 버려지기 때문입니다. 그래서 제가 추적해야 했던 숫자는 평균 지연 시간이 아니라 ‘꼬리 지연’이었습니다. 이 글은 그 꼬리 지연 하나를 집요하게 추적하고 해결한 기록입니다.
마이크로초와 밀리초가 나란히 있었다
저는 회사 자체 파이프라인에 계속 과부하를 걸며 테스트를 진행했습니다. 개발 중인 판정 로직과 가치 필터링 기능이 모두 켜진 상태였습니다. 로그를 분석하다 보니 흥미로운 사실을 발견했습니다. 우리의 판정 연산 자체는 마이크로초(µs) 단위에서 끝났는데, 정작 꼬리 지연은 p99.9 기준으로 18.5밀리초(ms)까지 벌어져 있었던 것입니다.1
이 두 숫자의 공존이 문제의 원인을 알려주는 증거였습니다. 지연의 원인이 연산 자체였다면 판정 시간 역시 밀리초 단위로 늘어나야 했지만, 판정은 여전히 마이크로초 대에서 마무리되었습니다. 즉, 늦어진 것은 판정 과정이 아니라 그 이후의 단계였습니다. 요청들은 판정이 끝난 뒤 송신 대기열에서 순서를 기다리며 만료될 위기에 처해 있었습니다. 1
값의 문제가 아니라 시간의 문제였다
이런 상황에서 보통 두 가지 방식으로 대응합니다. 승인 제어를 통해 유입되는 부하를 조절하거나, 우선순위 기반의 부하 셰딩으로 가치가 낮은 요청을 먼저 덜어내어 큐를 비우는 것입니다. 무효 트래픽(IVT)을 필터링하는 것 역시 같은 맥락입니다. 셋 다 유효한 방법이지만, 이들은 모두 ‘무엇을 처리할지’ 즉 값의 문제를 다룹니다. 큐를 줄이면 꼬리 지연도 함께 줄어들지만, 지연 시간의 상한선을 보장하지는 못합니다. 유입량이 셰딩 용량을 넘어서는 순간, 살아남은 요청들마저 다시 대기열에 갇혀 지연되기 때문입니다.
저희가 마주한 문제는 값이 아니라 ‘시간’의 문제였습니다. 아무리 가치가 높은 요청이라도 큐에서 대기하다가 마감을 넘기면 무용지물이 됩니다. 그래서 우리에게 필요한 것은 요청의 값을 평가하는 필터가 아니라, 요청이 머문 ‘시간’을 확인하는 게이트였습니다. 시간과 값은 완전히 별개의 기준입니다. 값 기반 셰딩이 한계에 달했을 때 덜 중요한 요청을 미룬다면, 시간 기반 게이트는 지연된 요청을 값과 무관하게 잘라냅니다.
이후 연구를 통해 값 기준으로 필터링하는 계층도 별도로 구축했습니다. 그 결과 입구의 승인 제어가 가장 붐비던 큐의 깊이를 수만 건대에서 수백 건대로 줄여주었고, 큐가 짧아지자 시간 게이트가 버려야 할 요청도 함께 줄어들었습니다. 이 부분에 대한 자세한 이야기는 다음 글에서 다루겠습니다.
시간 상한 게이트를 달았다 — 그리고 세 번 옮겼다
병목의 원인이 큐 대기 시간이라면 처방은 명확했습니다. 시스템에 머문 시간이 설정된 상한선(T)을 넘긴 요청은 내보내지 않는 ‘시간 상한 게이트’를 두는 것입니다. 관건은 이 게이트를 ‘어디에’ 배치하느냐였고, 저희는 최적의 위치를 찾기 위해 세 번이나 위치를 옮겨야 했습니다
첫 번째, 판정 앞. 처리 직전에 시스템 체류 시간(도착 이후 경과 시간)을 측정하여 상한을 넘겼으면 연산 없이 폐기했습니다. 연산 리소스를 아끼는 데는 효과적이었습니다. 하지만 이 게이트가 통제하는 것은 ‘판정 시작 시점’의 체류 시간뿐이었습니다. 정작 18.5밀리초라는 지연이 발생하던 구간은 게이트보다 하류에 있었기 때문에, 판정이 끝난 후 요청은 다시 큐에서 정체되었습니다.
두 번째, 전달 시점. 판정이 끝난 뒤, 요청을 내보내기 위해 큐에서 꺼내는 지점으로 게이트를 옮겼습니다. 이제 판정 이후의 대기 시간까지 포함해 측정할 수 있게 되었습니다. 하지만 여전히 사각지대가 남았습니다. 큐에서 꺼낸 뒤 물리적 송신을 기다리는 마지막 버퍼에서의 대기 시간이 잡히지 않았던 것입니다. 병목이 바로 그 마지막 버퍼에 있을 때, 요청은 게이트를 통과하고도 그곳에서 계속 지연되었습니다.
세 번째, 물리 송신 직전. 마침내, 네트워크를 타기 직전 지점에서 도착부터 송신 순간까지의 총 체류 시간을 측정하도록 했습니다. 앞선 두 위치에서 놓쳤던 마지막 버퍼 구간까지 이제 상한선 안에 들어오게 되었습니다. 입찰 지연이 발생하던 최종 지점보다 더 하류에 게이트를 배치한 것입니다.
구조적 보장
세 번째 위치에 게이트를 두자 상황이 명확해졌습니다. 밖으로 나간 요청은 정의상 체류 시간이 상한(T) 이하가 됩니다. 체류 시간이 T를 넘은 요청은 외부로 나갈 경로 자체가 차단되기 때문입니다. 버려지거나, 나가게 된다면 무조건 체류 시간이 T 이하가 되며, 그 중간은 존재하지 않습니다.
이것은 통계적 보장이 아니라 구조적 보장입니다. “평균적으로 좋다”거나 “대체로 빠르다”는 의미가 아니라, 게이트를 지연이 발생하는 모든 지점 뒤에 배치했기 때문에 구조적으로 상한선을 넘길 수 없다는 뜻입니다. 실제로 파일럿 트래픽의 도착 타이밍을 게이트가 적용된 송신 경로에 그대로 리플레이하여 검증했을 때도, 최종 전달된 요청 중 체류 시간 상한을 넘긴 건은 단 하나도 없었습니다(위반 0건). 상한(T)을 어디에 설정하느냐에 따라 폐기되는 비율은 달라졌지만—이 절대값은 해당 트래픽과 환경에 종속된 값입니다—‘송신된 요청은 무조건 상한 시간 이하’라는 구조적 특성은 부하 상황과 무관하게 유지되었습니다.2
한 줄로
시간 상한 게이트는 지연이 발생하는 최종 지점보다 뒤에 있어야 합니다. 앞에 두면 그 이후에 발생하는 지연을 놓치게 됩니다. 우리가 게이트의 위치를 세 번이나 옮긴 이유가 바로 이것입니다.
우리는 실시간 입찰 인프라에서 꼬리 지연을 구조적으로 통제하는 저지연 트래픽 필터링 시스템을 만들고 있습니다.
이 게이트를 실제 서비스 트래픽에 영향 없이 테스트해 보고 싶다면, 트래픽 미러링 기반의 섀도 모드 파일럿으로 시작해 보실 수 있습니다.
Footnotes
18.5밀리초는 자체 파이프라인 과부하 실험에서 관측된 p99.9 꼬리 지연 값입니다. 이는 시간 상한 게이트를 적용하기 전의 측정값이며, 이후 이 기록된 도착 타이밍을 게이트가 적용된 경로에 리플레이하여 상한 검증에 사용했습니다. 송신기에서 수신기까지의 네트워크 구간을 포함한 End-to-End 측정값이므로, 당사 시스템 내부의 체류 시간만 고려하면 이보다 더 여유가 있는 보수적인 수치입니다. 판정 연산이 마이크로초 단위라는 점은 별도의 베어메탈 벤치마크를 통해 확인했으며, 모든 절대값은 특정 테스트 환경 기준입니다. ↩ ↩2
전달된 요청의 체류 시간이 상한선 이하(위반 0건)라는 사실과 상한별 폐기율(Drop Rate)은 파일럿 타이밍 리플레이와 라이브 환경 양쪽에서 모두 교차 검증했습니다. 폐기율과 같은 절대적인 수치는 해당 트래픽과 환경에서 측정된 결과이므로, 다른 환경에 단순 대입하여 예측할 수는 없습니다. ↩