HTTP 클라이언트의 “동시 요청 수”는 TCP 연결 수와 같지 않습니다. 풀·HTTP/2 stream 자리 대기, 새 연결 준비, 응답 본문 소비, 안전한 반환을 각각 시간과 자원 점유로 추적해야 풀 증설이 하위 서비스 포화를 가리는지 판단할 수 있습니다.
풀 대기와 풀 증설 판단 기준
연결을 얻은 뒤 응답은 10ms인데 풀에서 500ms 기다린다면 클라이언트의 상한이 낮을 수 있습니다. 반면 연결 후 응답도 2초로 느려지고 서버의 오류·DB 대기가 함께 늘었다면 풀을 늘려 더 많은 요청을 보내는 것이 포화를 악화시킬 수 있습니다.
연결 풀은 재사용과 용량 관리 장치입니다. 처리 능력을 새로 만드는 장치가 아니므로 어디에서 기다리는가와 하위 서비스의 여유를 함께 봐야 합니다.
HTTP 호출 단계별 시간 예산 분리
그림을 글로 읽기
- 풀·스트림 자리 대기
- 연결 준비 또는 재사용: DNS · TCP/TLS
- 송신·응답·본문 읽기
- 정리 후 풀 반환: 재사용 가능한 상태인지 확인
- 풀·스트림 자리 대기 → 연결 준비 또는 재사용: 허가 획득
- 연결 준비 또는 재사용 → 송신·응답·본문 읽기: 요청 시작
- 송신·응답·본문 읽기 → 정리 후 풀 반환: 실제 소비·정리
응답 헤더를 받은 순간과 본문을 모두 소비하거나 안전하게 닫은 순간은 다를 수 있습니다. 스트리밍 본문을 붙잡으면 연결·스트림 슬롯도 오래 점유합니다. 예외 경로에서 본문·응답 객체 정리를 빼먹는 것이 풀 고갈의 원인일 수 있습니다.
증설 판단과 하위 지표별 병목 구분
| 관찰 | 우선 조치 |
|---|---|
| 풀 대기 큼, 하위 처리 빠름·여유 있음 | 상한을 단계적으로 올리며 재측정 |
| 하위 지연·거절·CPU도 증가 | 유입·실행·대기 제한, 병목 원인 개선 |
| 요청은 끝났는데 슬롯이 안 돌아옴 | 본문 소비·예외·취소 정리 검사 |
| 새 연결만 느림 | DNS·TLS·NAT·수락 지연 분리 |
인스턴스당 100연결은 20인스턴스에서 최대 2,000연결입니다. 자동 확장이 같은 하위 서버에 부하를 늘릴 수 있으므로 전체 예산으로 환산해야 합니다. 연결 수가 적어도 HTTP/2 스트림이 많으면 동시 DB 작업이 많을 수 있습니다.
연결 수와 스트림 수의 자원 구분
HTTP/2의 peer SETTINGS, 클라이언트 풀, 프록시 upstream 정책이 스트림 동시성에 영향을 줍니다. TCP 연결 두 개에서 각각 100스트림을 허용하면 요청 동시성은 최대 200에 이를 수 있습니다. 그러나 서버가 실제로 감당하는 CPU·메모리·DB 예산은 더 작을 수 있습니다.
연결을 여러 개로 나누면 손실 영향 범위나 스트림 상한을 바꿀 수 있지만 TLS 상태·FD·혼잡 경쟁이 늘어납니다. 연결당 상한뿐 아니라 서비스 전체 in-flight와 대기자·바이트 예산을 둬야 합니다. 스트림 reset이 외부 DB 작업을 자동으로 중단하지도 않습니다.
단계별 timeout과 전체 기한 전달
전체 기한이 1초인데 풀에서 300ms를 기다렸다면 연결 단계에 들어갈 때 남은 예산은 700ms입니다. 연결에 200ms를 썼다면 응답 단계는 처음부터 새로 1초를 받는 것이 아니라 남은 500ms와 응답 단계 상한 중 작은 시간 안에 끝나야 합니다. 절대 기한을 한 번 정하고 각 단계에 남은 예산을 전달해야 풀 대기·연결·응답 시간을 더해 3초 이상으로 늘리는 실수를 피할 수 있습니다.
request(deadline):
lease = acquirePool(min(stageCap, deadline-now()))
try:
require now() < deadline
connection = prepareConnection(lease, deadline)
response = sendAndRead(connection, deadline)
return consumeAccordingToContract(response, deadline)
finally:
cleanup_response_and_io()
return_or_discard_connection_when_safe()
이 슈도코드는 정리 API의 실제 완료 계약을 추상화합니다. timeout으로 사용자 대기가 끝나도 실행 I/O가 남으면 연결을 즉시 다른 요청에 재사용해서는 안 됩니다. 취소된 풀 대기자도 제거해 이미 끝난 요청이 나중에 새 연결을 얻지 않게 합니다.
유휴 연결 실패와 변경 결과의 구분
NAT·로드밸런서·상대 서버가 유휴 연결을 먼저 닫았는데 로컬 풀에는 아직 남을 수 있습니다. 재사용 첫 요청의 오류가 확실한 미전송인지, 일부 바이트 전송·원본 처리 후 응답 유실인지 라이브러리 계약을 확인합니다.
멱등하지 않은 변경을 새 연결로 무조건 재시도하지 않습니다. 같은 논리 키의 결과 조회와 재전송 정책을 사용하고, 유휴·최대 수명을 조정하되 동시에 만료되지 않게 분산합니다. DNS 새 주소와 풀의 실제 peer도 따로 관찰합니다.
대기·성공·회복 구간 시험
큰 응답을 천천히 읽는 호출과 짧은 RPC를 섞고, 풀 상한·인스턴스 수·스트림 수를 바꿉니다. 획득 p99·본문 소비 시간·활성 요청·새 연결률·하위 오류를 기록합니다. timeout·취소·본문 파싱 예외 뒤 슬롯이 반환되는지도 확인합니다.
풀을 늘려 잠깐 대기가 줄어도 하위 큐와 오류가 늘면 개선이 아닙니다. 부하가 낮아졌을 때 오래된 대기와 연결이 정리되어 정상 지연으로 돌아오는지까지 확인해야 합니다.
예상 trace를 하나 정하면 진단이 쉬워집니다. 전체 기한 1초에서 풀 대기 300ms와 연결 200ms가 발생하면 응답 단계에 남는 상한은 500ms보다 작거나 같아야 하며, 각 단계가 다시 1초를 받으면 안 됩니다. 큰 본문을 읽는 중 timeout을 발생시킨 뒤 해당 연결이 즉시 재사용되지 않고 폐기 또는 안전한 drain을 거치는지도 확인합니다.