부하 테스트 · 병목 찾기 · 이론
거절은 실패가 아니라 보호다
한 줄 요약
설계 문제는 '큐를 둘 것인가'가 아니라 어느 큐가 한계에 먼저 도달하고 그때 무엇을 할 것인가다.
왜 이게 필요했나
요청 하나가 처리되기까지 지나는 길에는 큐가 줄지어 있다. 소켓 버퍼 → 스레드 풀 큐 → 커넥션 풀 대기 → DB 락 대기 → 처리. 어느 하나가 먼저 포화되면 그 뒤는 전부 대기로 바뀐다. 그래서 병목 조사는 이 사다리를 순서대로 훑는 일이다.
큐 길이의 상한은 계산할 수 있다. 허용 대기 시간을 평균 처리 시간으로 나눈 값이다. 목표 200ms, 처리 20ms, 워커 10개라면 상한은 대략 100 이다. 이 상한을 넘겨 무한 큐를 두면 장애를 지연 시간으로 바꿀 뿐이다. 거절은 실패가 아니라 보호다. 빨리 503 을 내는 편이 낫다.
어떻게 동작하나
가장 유명한 전투 사례는 기본값 두 개의 충돌이다. HikariCP maximum-pool-size 기본값 10 과 Tomcat threads.max 기본값 200. 트래픽이 늘면 대부분의 스레드가 커넥션 대기에 빠지고 Connection is not available, request timed out after 30000ms 가 쏟아진다. 해결은 세 가지를 함께 바꾸는 것이다. 풀을 30으로 올리고, connection-timeout 을 30초에서 5초로 줄여 빠르게 실패시키고, Tomcat threads.max 는 오히려 50으로 축소한다. 재발 방지 알림은 hikaricp.connections.pending > 0 이 30초 이상 지속될 때 건다.
포화 지점을 찾는 절차는 다섯 단계다.
1. 공식으로 초기값을 잡는다. 풀 크기 = (코어 수 × 2) + 디스크 축 수. 8코어면 20 근처다.
2. 목표 부하에서 풀 대기 p99 와 처리량을 기록한다.
3. 절반으로 줄여 본다. 처리량이 유지되면 원래가 컸다는 뜻이다.
4. 처리량이 늘지 않는 지점까지 늘려 보고, 평평해지기 시작한 값보다 조금 작은 곳이 적정이다.
5. 그 값에서도 대기가 길면 풀이 아니라 쿼리를 고쳐야 한다.
커넥션 대기가 길다는 것은 점유 시간이 길다는 뜻이고 그것은 대개 느린 쿼리이거나 긴 트랜잭션이다. 풀 크기는 그 증상을 잠시 가릴 뿐이다. 그래서 원칙은 이렇다. 풀 크기는 데이터베이스가 동시에 잘 처리할 수 있는 건수여야 하고, 애플리케이션이 보내고 싶은 건수여서는 안 된다.
현장에서 만나는 모습
한계에 도달한 뒤 동시성을 더 늘리면 처리량 X 는 늘지 않고 응답 시간 W 만 비례해 늘어난다. 곧 컨텍스트 스위칭, 동시성의 제곱에 가까운 잠금 경합, 캐시 적중률 하락, 재시도 부하가 겹쳐 오히려 줄어든다. 이 식이 주는 메시지는 숫자 자체가 아니라 자릿수다. 정답은 수백이 아니라 수십이다.
다음 실습에서 할 것
한 번에 하나씩만 처리하는 파이썬 HTTP 서버를 일부러 만들어 띄웁니다. 동시성 1과 10에서 측정해 처리량은 그대로인데 지연만 늘어나는 직렬화 신호를 확인하고, 병목 가설 세 개를 계층별로 세운 뒤, 동시 처리 가능한 서버로 바꿔 처리량이 몇 배로 뛰는지 비교합니다. 마지막에 목표 트래픽을 감당할 인스턴스 수를 계산합니다.