..

CPU Throttling

부하 테스트를 하다가 이상한 결과를 계속 보게 되어서 정리하게 되었다. 크레딧 관련 서비스 파드 하나를 대상으로, 여러 명이 동시에 채팅치듯 계속 요청을 보내면 몇 명까지 버티는지 확인하는 테스트였는데, 에러율은 끝까지 0%인데 동시 접속자가 늘어날수록 응답 시간만 계속 늘어나는 패턴이 나왔다.

에러율은 0%인데 왜 느려질까

deduct(크레딧 차감)랑 폴링 요청들의 지연 p95를 기준으로 봤을 때, 20명까지는 완전히 깨끗했다(deduct p95 497.8ms, poll p95 399.5ms, 에러 0%). 근데 30명부터 poll p95가 658.6ms로 SLO(500ms)를 넘기 시작하고, 40명이면 deduct·poll 둘 다 1.2~1.3초까지 늘어난다. 그런데도 에러율은 계속 0%다. 타임아웃도 5xx도 없이 그냥 느려지기만 하는 거다.

처음엔 그냥 리소스가 부족한가보다 싶었는데, 계속 이 패턴이 반복되니까 뭔가 이상하다는 생각이 들었다. 진짜로 처리 용량 자체가 부족하다면 큐가 쌓이면서 결국 어딘가에서 타임아웃이나 에러가 나야 할 것 같은데, 여기선 그냥 “잠깐 느려졌다 다시 도는” 정도로만 그치고 있었다.

CPU를 두 배로 올려봤다

CPU request/limit을 500m에서 1000m로 패치하고(Guaranteed QoS 유지를 위해 memory는 그대로 둠) 동일한 40명 테스트를 다시 돌렸다. deduct p95가 1326.4ms → 361.5ms로, poll p95는 1200.1ms → 295.9ms로 뚝 떨어졌다. FLAG였던 SLO가 전부 PASS로 바뀐 거다.

60명, 80명까지 올려봐도 비슷했다. 500m일 때 20명/30명/40명이던 각 단계의 임계점이 정확히 2배로 밀려서 1000m에서는 40명/60명/80명에서 똑같은 패턴이 반복됐다. CPU를 2배로 늘리면 버틸 수 있는 인원도 거의 2배로 늘어나는, 꽤 선형에 가까운 관계였다.

근데 이게 “문제가 해결됐다”는 아니었다. Grafana로 스로틀링 지표를 같이 봤는데, CPU를 2배로 늘려도 스로틀링 자체가 사라지는 게 아니라 스로틀링이 걸리기 시작하는 동시 유저 수가 뒤로 밀리는 것뿐이었다(500m에선 20명부터, 1000m에선 40명부터 스로틀링이 잡히기 시작). 용량이 늘어난 거지 근본 원인이 없어진 건 아니라는 뜻이다.

메모리도 늘려봤다

혹시 메모리 쪽 문제일 수도 있으니 CPU는 500m 그대로 두고 memory만 1Gi에서 2Gi로 늘려서 같은 4단계(Smoke/Load/Stress/Spike) 테스트를 다시 돌려봤다. Load 단계는 좀 나아졌는데(deduct p95 1200.1ms → 1022.7ms) Stress 단계는 오히려 더 나빠졌다(4403.8ms → 이전보다 지연 증가). 실행마다 편차가 커서 이걸 일관된 개선이라고 단정하기는 어려웠다.

테스트 중 실측 메모리 사용량 자체가 최대 220Mi 정도, 2Gi의 10% 수준밖에 안 됐다. 애초에 메모리가 모자란 상황이 아니었던 거다. 메모리를 늘리는 건 근본적인 해결책이 아니라는 결론이 나왔다.

초당 한 명씩 늘려가며 정확한 임계점을 찾다

이전 램프 테스트들은 세션이 30초마다 만료되고 재생성되면서 “초당 도착률”과 “실제 동시 활성 인원”이 뒤섞여 정확히 몇 명에서 무너지는지 읽기가 어려웠다. 그래서 이번엔 세션이 절대 만료되지 않게 하고(300초 동안 계속 살아있게) 정확히 초당 1명씩만 새로 추가했다. 이러면 경과 시간(초)이 곧 동시 활성 세션 수랑 1:1로 일치하게 된다.

결과가 꽤 선명했다. 145명까지는 완전히 깨끗했다(CPU는 이미 90~95명 근처부터 100% 포화 상태였는데도 에러가 하나도 없었다). 그러다 155명에서 처음으로 타임아웃이 나타났다(65건 성공, 68건 타임아웃). 165명이면 31/110으로 급격히 악화되고, 185명쯤 되면 사실상 전멸(1건 성공, 158건 타임아웃)이었다. 235명 이후로는 완전히 전멸 상태(성공 0건)로 굳어져서 더 나빠지지도 회복되지도 않았다.

이 구간 전체에서 5xx도, network 에러도 단 한 건도 없었다. 전부 클라이언트 쪽 타임아웃(10초)이었다. 파드 재시작도 0회였다.

왜 에러가 아니라 지연으로만 나타나는가

Grafana에서 확인해보니 CPU/메모리 평균 사용량 자체는 낮게 나오는데 CPU throttling 지표는 90~100%까지 치솟아 있었다. 이게 딱 들어맞는 설명이었다. Kubernetes의 CPU limit은 평균이 아니라 100ms 같은 아주 짧은 주기마다 정해진 쿼터를 다 쓰면 남은 시간 동안 강제로 멈추는 방식(Linux CFS bandwidth controller)이다. 동시 요청이 몰리면 그 짧은 창 안에서 쿼터를 순식간에 다 쓰고 나머지 시간은 얼어붙는데, 1분/5분 단위로 평균 내는 Grafana 그래프에서는 이게 그냥 “사용률 낮음”으로 보이는 거다. 실제로는 확 쓰고 멈추기를 반복하고 있었던 거고.

throttle이 걸린다고 프로세스가 죽는 건 아니다. 그냥 그 주기 동안 실행이 멈췄다가 다음 주기에 다시 도는 것뿐이라, 요청이 실패하는 게 아니라 큐에 쌓여서 응답만 늦게 나가는 거였다. 결국 클라이언트 쪽 타임아웃에 걸려야 비로소 실패로 관측되는 거고, 서버 입장에서는 애초에 5xx를 보낼 일이 없었던 셈이다. 에러율만 보고 있었으면 이 문제를 한참 늦게 알아챘을 것 같다.