· 박준성 (CEO / Junsung Park) · Technology  · 12 min read

버릴 것을 고르는 손잡이

앞선 글에서 저희는 '시간 상한 게이트'를 지연이 생기는 마지막 지점 뒤에 두었습니다. 그렇게 해서 '밖으로 나간 요청은 무조건 체류 시간 상한 이하'라는 구조적 보장을 얻어냈죠.

앞선 글에서 저희는 '시간 상한 게이트'를 지연이 생기는 마지막 지점 뒤에 두었습니다. 그렇게 해서 '밖으로 나간 요청은 무조건 체류 시간 상한 이하'라는 구조적 보장을 얻어냈죠.

앞선 글에서 저희는 ‘시간 상한 게이트’를 지연이 생기는 마지막 지점 뒤에 두었습니다. 그렇게 해서 ‘밖으로 나간 요청은 무조건 체류 시간 상한 이하’라는 구조적 보장을 얻어냈죠. 그리고 첫 번째 글은 이런 문장으로 끝맺었습니다. *“입구의 승인 제어가 가장 붐비던 큐의 깊이를 수만 건 대에서 수백 건 대로 줄여주었고, 큐가 짧아지자 시간 게이트가 버려야 할 요청도 함께 줄었다”*라고요. 이번 글은 바로 그 ‘입구’를 설계한 이야기입니다.

잘라내는 것과 유입 자체를 줄이는 것은 다르다

시간 상한 게이트는 타임아웃이 임박한 지연 요청을 잘라냅니다. 하지만 시스템 안에서 이미 늦어버린 요청을 쳐내는 일과, 애초에 시스템으로 들어오는 부하 자체를 줄이는 일은 전혀 다른 문제입니다. 게이트가 만료된 요청을 걷어내 Queue의 부담을 다소 덜어주기는 해도, ‘어떤 요청을 시스템에 받아들일지’까지 결정하지는 않으니까요.

그런데 이 둘은 결국 한 지점에서 맞닿습니다. 큐가 짧아지면 요청이 기다리는 시간도 줄고, 대기 시간이 줄면 상한선을 넘겨 폐기되는 요청도 자연히 줄어듭니다. 그래서 저희는 ‘시간’이라는 축에 더해, 시스템 입구에서부터 부하 자체를 덜어내는(Load Shedding) 계층을 하나 더 두기로 했습니다.

승인 판정은 파싱보다 먼저 일어나야 한다

시스템 입구에서 가치가 낮은 요청을 미리 걸러내는 일을 승인 제어(Admission Control)라고 부릅니다. 우선순위 기반의 부하 셰딩이나 무효 트래픽(IVT) 필터링과 결이 비슷한 기법이죠. 다만 저희에겐 까다로운 제약이 하나 있었습니다. 승인 여부를 요청이 큐에 들어가기 전, 그러니까 페이로드(본문)를 파싱하기도 전에 결정해야 한다는 점이었습니다. 그래야 큐를 붐비게 만드는 근본 원인을 입구에서 막을 수 있으니까요.

파싱 이전 단계라면, 요청의 가치를 판단하는 연산 비용부터 극도로 저렴해야 합니다. 정교한 가치 평가 모델을 돌리겠다고 요청을 뜯어보는 순간, 그 연산이 또 다른 병목이 됩니다. 그래서 저희는 Payload Size를 가치 판단 지표로 삼았습니다. 크기는 파싱 없이 네트워크 계층에서 바로 알 수 있으니, 추가 연산 없이 단순 비교 한 번으로 가치 등급을 매길 수 있습니다.

여기에 시스템 부하에 따라 움직이는 적응형 로직을 더했습니다. 평상시에는 Threshold이 0이라 모든 요청이 통과합니다. 걸러낼 일이 없으니 비용도 들지 않죠. 그러다 큐가 차오르기 시작하면 문턱이 올라가면서 우선순위가 낮은 요청부터 쳐냅니다. 이때 트래픽이 순간적으로 튀어도 문턱값이 요동치지 않도록, 진입과 해제 조건에 버퍼를 두어 일시적인 급증은 부드럽게 흡수하게 설계했습니다.

절벽을 만났다

여기까지는 설계대로 완벽해 보였습니다. 그런데 이 ‘손잡이’를 실제로 돌려보니 예상치 못한 문제가 튀어나왔습니다.

처음 설계한 크기 기반 등급은 사이즈가 두 배로 늘 때마다 한 칸씩 올라가는 계단식이었습니다. 문제는 검증에 쓴 트래픽 데이터의 요청 크기가 특정 구간에 촘촘하게 몰려 있었다는 것입니다. 그 탓에 문턱을 딱 한 등급 올렸을 뿐인데 폐기율이 33.75%에서 77.13%로 수직으로 치솟았습니다. 손잡이를 한 칸 돌렸더니 트래픽의 43.37%p가 한꺼번에 날아가 버린 셈이죠. (이 극단적인 절벽 수치는 해당 검증 데이터에 특화된 값입니다.)

“트래픽을 절반쯤만 버리고 싶다”고 해도 그렇게 할 중간 단계가 없었습니다. 한 칸 내리면 너무 적게 버리고, 한 칸 올리면 너무 많이 버렸습니다. 손잡이 눈금이 워낙 듬성듬성하다 보니, 부하에 맞춰 문턱을 미세 조정해야 하는 컨트롤러가 목표점을 찾지 못한 채 양 극단을 오가며 진동할 수밖에 없었습니다.

계단을 잘게 나눴다

해법은 등급 하나를 더 잘게 쪼개는 것이었습니다. 같은 등급 안에서도 상위 비트 몇 개를 추가로 읽어 들이면 요청들 사이에 미세한 순서를 매길 수 있습니다. 이렇게 하면 두 배 간격의 거친 계단이 훨씬 촘촘한 다이얼 눈금으로 바뀝니다.

핵심은 이 세분화의 연산 비용이 ‘공짜’에 가깝다는 점입니다. 나눗셈도 부동소수점 연산도 쓰지 않고, 이미 메모리에 올라와 있는 크기 값에 비트 연산만 몇 번 더 얹으면 끝납니다. 눈금을 이렇게 촘촘하게 만들자, 예전엔 닿을 수 없던 ‘절반만 버리는 지점’에 오차 1%p 이내로 정확히 안착할 수 있었습니다.

두 축이 서로 돕는다

과부하 상황을 재현한 실험에서 그 효과를 확인했습니다. 입구의 승인 제어를 켜자 꽉 막혀 있던 큐 깊이가 수만 건 대에서 수백 건 대로 시원하게 줄었습니다. 대기열이 짧아지니 앞선 시간 상한 게이트가 타임아웃으로 버려야 했던 지연 요청도 덩달아 급감했고요. (이 상관관계 자체는 분명히 증명됐지만, 큐 깊이나 폐기율 감소폭 같은 절대 수치는 해당 테스트 환경 기준입니다.)

‘시간’이라는 축과 ‘값’이라는 축은 서로 다른 기준입니다. 앞의 축은 요청이 얼마나 지연됐는지를 보고, 뒤의 축은 그 요청이 처리할 만한 가치가 있는지를 봅니다. 두 축이 서로 직교하면서도 완벽한 시너지를 낸다는 것—입구에서 가치를 기준으로 유입량을 조절하면 시간 축이 감당할 부담도 함께 준다는 것—을 실제 측정으로 확인했습니다. 정상적인 트래픽 처리 경로에서 이 승인 제어 게이트가 유발한 추가 연산 비용은 기저 노이즈와 구분하기 어려울 만큼 미미했습니다.

명확하게 밝히는 한계점

단, 한 가지 한계는 분명히 짚고 넘어가야 합니다. ‘요청의 크기’는 가치를 유추하는 근사치일 뿐 절대적인 정답은 아닙니다. 크기가 큰 요청이 대체로 가치도 높은 편이지만, 예외는 언제나 있습니다.

저희는 이 한계를 확인하려고 일부러 악조건을 만들어 봤습니다. 페이로드 크기와 실제 가치가 반비례하도록 조작한 검증 데이터(작지만 가치 높은 요청을 다수 섞은)를 밀어 넣었더니, 승인 제어에서 잘려 나간 것이 전부 그 작고 소중한 고가치 요청들이었습니다. 크기를 필터링 기준으로 삼는 이상 이 구조적 결함은 피할 수 없습니다. 그래서 이 시스템을 도입할 때는 실제 라이브 트래픽에서 ‘페이로드 크기’와 ‘가치’의 상관관계가 얼마나 되는지 면밀히 측정하는 일을 파일럿 테스트의 필수 항목으로 남겨 두었습니다.

한 줄로

조절하는 손잡이의 눈금이 듬성듬성한 계단이면, 제어 시스템은 안정점을 찾지 못하고 진동합니다. 트래픽을 차단하는 기준은 우리가 원하는 목표점을 정확히 때릴 수 있을 만큼 세밀한 해상도를 가져야 합니다.


우리는 실시간 입찰 인프라에서 꼬리 지연을 구조적으로 통제하는 저지연 트래픽 필터링 시스템을 만들고 있습니다. 핵심 필터링 방법은 특허 출원 중입니다.

이 게이트를 실제 서비스 트래픽에 영향 없이 테스트해 보고 싶다면, 트래픽 미러링 기반의 섀도 모드 파일럿으로 시작해 보실 수 있습니다.

Back to Blog

Related Posts

View All Posts »
Where Did 18.5 Milliseconds Come From

Where Did 18.5 Milliseconds Come From

In real-time bidding, a response that misses tmax is worth zero. No matter how sophisticated the bidder, a bid that arrives after the deadline is simply thrown away. So the number I chased was not the average but the tail latency. This post is the record of tracking down one such tail.

18.5밀리초는 어디서 왔는가

18.5밀리초는 어디서 왔는가

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