이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- HTTP Keep-Alive는 어떤 비용을 줄이고 어떤 상태를 남기는가?
Connection: close, idle timeout, max requests는 연결 재사용에 어떤 영향을 주는가?- Keep-Alive 장애를 디버깅할 때 client, proxy, server 중 어디의 증거를 봐야 하는가?
개요
Keep-Alive는 하나의 TCP 연결 위에서 여러 HTTP 요청과 응답을 처리해 연결 생성 비용을 줄이는 방식이다. TCP 3-way handshake, TLS handshake, slow start 비용이 반복되지 않으므로 짧고 빈번한 API 호출에서 latency와 CPU 사용량을 크게 줄일 수 있다.
하지만 Keep-Alive는 단순한 성능 옵션이 아니다. 연결을 재사용한다는 것은 client, proxy, load balancer, server가 “이 연결을 언제까지 살려 둘지”에 대한 상태를 공유한다는 뜻이다. 한쪽은 살아 있다고 믿고 재사용했는데 다른 쪽은 이미 닫았다면 간헐적인 reset, broken pipe, 502가 발생한다.
왜 백엔드 개발자가 알아야 하는가
백엔드 개발자는 inbound 요청을 받는 서버이면서 outbound 요청을 보내는 클라이언트다. 사용자 요청은 LB나 reverse proxy의 Keep-Alive 정책을 타고 들어오고, 애플리케이션은 결제/인증/내부 API를 호출할 때 HTTP client pool의 Keep-Alive 정책을 사용한다.
- 트래픽이 늘 때 매 요청마다 TCP/TLS 연결을 새로 만들면 latency와 CPU가 증가한다.
- idle 연결이 과도하면 file descriptor, memory, NAT port, upstream connection slot을 점유한다.
- proxy와 application의 keepalive timeout이 다르면 stale connection이 생긴다.
- rolling deploy나 target draining 중 오래 열린 연결이 이전 target으로 계속 갈 수 있다.
Keep-Alive를 모르면 “가끔 reset이 난다”, “첫 요청만 느리다”, “배포 직후 일부 사용자만 실패한다” 같은 증상을 코드 예외로만 보게 된다.
요청 흐름에서의 위치
Keep-Alive는 HTTP 메시지를 실어 나르는 연결 생명주기다.
Client or Proxy
→ TCP connection
→ TLS session
→ HTTP request / response #1
→ idle
→ HTTP request / response #2
→ idle timeout or Connection: close사용자 inbound 경로에서는 브라우저와 CDN/LB/proxy, proxy와 application server 사이에 Keep-Alive가 있다. outbound 경로에서는 application의 HTTP client와 external API 또는 internal service 사이에 있다.
원리
HTTP/1.1은 기본적으로 persistent connection을 사용한다. 연결을 닫고 싶으면 Connection: close를 사용하거나, server/client가 자체 timeout과 request count 정책에 따라 연결을 종료한다.
연결 생명주기를 구성하는 값은 서로 다르다.
| 값 | 의미 | 운영 영향 |
|---|---|---|
| idle timeout | 요청이 없는 연결을 얼마나 유지할지 | 길면 자원 점유, 짧으면 재연결 증가 |
| max requests | 한 연결에서 처리할 최대 요청 수 | 너무 크면 per-connection memory가 오래 남음 |
| max life time | 연결의 절대 수명 | 오래된 연결과 배포 전 연결을 정리 |
| keepalive pool size | idle 연결을 몇 개 보관할지 | 너무 작으면 재연결, 너무 크면 upstream 압박 |
Connection: close | 현재 응답 뒤 연결 종료 신호 | 재사용 중단, graceful close |
여기서 중요한 구분이 있다. HTTP Keep-Alive는 애플리케이션 계층의 연결 재사용이고, TCP keepalive는 OS가 죽은 peer를 감지하기 위해 긴 주기로 probe를 보내는 기능이다. 이름은 비슷하지만 장애 대응에서 보는 위치가 다르다.
비용 구조
Keep-Alive가 줄이는 비용은 단순히 “연결 하나”가 아니다.
| 새 연결을 만들 때 드는 비용 | Keep-Alive 재사용 시 줄어드는 것 |
|---|---|
| DNS lookup | 같은 host 연결 재사용 시 매번 해석하지 않음 |
| TCP handshake | 왕복 지연 감소 |
| TLS handshake | CPU와 왕복 지연 감소 |
| TCP slow start | 기존 congestion window 활용 가능 |
| server accept 처리 | accept와 TLS 처리 부하 감소 |
반대로 Keep-Alive가 남기는 비용도 있다. idle socket, TLS session state, file descriptor, upstream slot, NAT mapping이 유지된다. 그래서 “무조건 길게”가 아니라 “재사용 이득이 있는 만큼만” 유지해야 한다.
실무에서 자주 만나는 문제
- NGINX upstream Keep-Alive가 꺼져 있어 app server로 매번 새 연결을 만든다.
- 외부 응답은 정상이어도 proxy to app 구간의 connect/TLS 비용이 반복된다.
- NGINX upstream
keepalive,proxy_http_version,Connectionheader 처리와 버전을 확인한다.
- 서버가
Connection: close를 자주 내려 connection reuse가 되지 않는다.- max requests, graceful shutdown, upstream error, protocol mismatch가 원인일 수 있다.
- access log에 upstream connection reuse 여부나 close reason을 남길 수 있는지 본다.
- idle 연결이 너무 오래 살아 배포 후 이전 target으로 요청이 간다.
- LB target draining, client keepalive duration, DNS TTL과 함께 봐야 한다.
- 배포 속도와 connection lifetime을 같이 설계한다.
- 첫 요청만 느리고 이후 요청은 빠르다.
- DNS/TCP/TLS handshake 비용이 첫 요청에만 들어간 것일 수 있다.
curl -w로 첫 요청과 재사용 요청의 connect/TLS 시간을 비교한다.
디버깅 방법
먼저 “재사용되는가”와 “누가 닫는가”를 분리한다.
# 같은 프로세스의 curl이 두 URL을 연속 호출하게 해서 연결 재사용 힌트를 본다.
curl -v --http1.1 \
https://example.com/api/health \
https://example.com/api/health \
-o /dev/null볼 지점은 다음이다.
- 두 번째 요청에서 새 TCP 연결을 다시 여는가?
- 응답에
Connection: close가 있는가? - 첫 요청과 두 번째 요청의
time_connect,time_appconnect차이가 있는가? - proxy나 app log에서 같은 client connection으로 여러 request가 처리되는가?
구간별 시간은 이렇게 비교한다.
curl -sS -o /dev/null \
-w "code=%{http_code}\nconnect=%{time_connect}\ntls=%{time_appconnect}\nfirst_byte=%{time_starttransfer}\ntotal=%{time_total}\n" \
https://example.com/api/health서버 쪽에서는 socket 상태와 리스너를 확인한다.
ss -tan state established '( sport = :8080 or dport = :8080 )'
ss -tan state time-wait '( sport = :8080 or dport = :8080 )'
lsof -nP -iTCP:8080TIME-WAIT가 급증하면 짧은 연결이 많이 만들어지는지 본다. ESTABLISHED가 계속 쌓이면 idle connection, long request, slow client, worker saturation을 나눠 본다.
코드로 확인하기
NGINX가 upstream application으로 HTTP Keep-Alive를 쓰게 하려면 upstream connection cache와 HTTP 버전, Connection 헤더 처리를 확인한다.
upstream app_backend {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
keepalive 32;
}
server {
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Request-ID $request_id;
}
}확인할 것은 세 가지다.
keepalive 32는 worker당 idle upstream connection cache 크기다. upstream 전체 연결 상한이 아니다.proxy_http_version 1.1과Connection헤더 정리는 오래된 NGINX 구성에서 특히 중요하다.- upstream server가 idle connection을 유지할 수 있어야 실제 재사용된다.
Spring Boot 내장 Tomcat에서는 connector 속성으로 keep-alive 관련 값을 명시할 수 있다.
@Bean
WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatKeepAliveCustomizer() {
return factory -> factory.addConnectorCustomizers(connector -> {
connector.setProperty("keepAliveTimeout", "20000");
connector.setProperty("maxKeepAliveRequests", "100");
connector.setProperty("connectionTimeout", "20000");
});
}값 자체보다 중요한 것은 LB/proxy/app의 timeout 순서와 배포 전략에 맞춰 문서화하는 것이다.
주니어가 자주 하는 오해
- Keep-Alive를 켜면 항상 빠르다고 생각한다.
- 재사용할 만큼 요청이 자주 오지 않으면 idle socket 비용만 늘 수 있다.
Connection: keep-alive헤더가 있어야 HTTP/1.1 Keep-Alive가 된다고 생각한다.- HTTP/1.1에서는 지속 연결이 기본이고, 종료 의도를
Connection: close로 표현하는 쪽이 더 중요하다.
- HTTP/1.1에서는 지속 연결이 기본이고, 종료 의도를
- TCP keepalive와 HTTP Keep-Alive를 같은 기능으로 본다.
- TCP keepalive는 죽은 연결 감지이고, HTTP Keep-Alive는 요청/응답 재사용 정책이다.
- reset이 보이면 네트워크 장비 문제로만 본다.
- 상대가 idle timeout으로 정상 종료한 연결을 재사용했을 수도 있다.
시니어의 설계 판단 기준
- Keep-Alive는 client, proxy, LB, app server의 timeout 표를 만든 뒤 정렬한다.
- connection lifetime은 배포, target draining, 장애 지역 회피와 함께 본다.
- upstream keepalive cache는 downstream이 처리할 수 있는 connection 수를 넘지 않게 잡는다.
- first request latency와 steady-state latency를 분리해서 측정한다.
- “연결을 오래 유지”와 “장애를 빨리 우회”는 trade-off이므로 서비스 특성에 맞춘다.
인프라 협업 포인트
- LB의 idle timeout, client keepalive duration, target draining 값을 확인한다.
- NGINX/Envoy가 upstream connection을 재사용하는지, request별 새 connection을 만드는지 확인한다.
- rolling deploy 중 기존 연결이 어느 target으로 유지되는지 물어본다.
- NAT gateway나 firewall이 idle connection을 언제 제거하는지 확인한다.
- 장애 제보에는 reset 발생 시각, upstream host, connection reuse 여부,
Connection헤더, target id를 포함한다.
실전 팁
- 개인 프로젝트라도 reverse proxy와 app의 keepalive timeout을 명시하고 access log에 request time을 남긴다.
- 외부 API client는 dependency별 pool과 timeout을 두고 첫 호출 latency와 재사용 latency를 따로 측정한다.
- 배포 직후 간헐 502가 생기면 app crash만 보지 말고 draining, stale upstream connection, health check를 함께 본다.
- Keep-Alive를 늘리기 전에 file descriptor, memory, upstream max connection, NAT port 한계를 본다.
위험 신호!
- proxy와 app의 idle timeout 값을 아무도 모른다.
Connection reset by peer가 간헐적으로 나오는데 timeout 표가 없다.- upstream keepalive cache를 키웠지만 downstream max connection은 그대로다.
- rolling deploy 때 오래 열린 연결이 어느 target으로 가는지 관측할 수 없다.
TIME-WAIT또는ESTABLISHED증가를 보고도 요청량과 연결 재사용률을 분리하지 않는다.
확인 질문
확인 질문
- HTTP Keep-Alive와 TCP keepalive는 어떻게 다른가?
- HTTP Keep-Alive는 한 TCP 연결에서 여러 HTTP 요청을 재사용하는 정책이고, TCP keepalive는 OS가 유휴 TCP 연결의 peer 생존 여부를 확인하는 기능이다.
- Keep-Alive를 너무 길게 잡으면 어떤 문제가 생길 수 있는가?
- idle socket, file descriptor, upstream slot, NAT mapping을 오래 점유하고 배포나 장애 지역 우회가 늦어질 수 있다.
- 첫 요청만 느리고 두 번째 요청부터 빠르면 무엇을 의심하는가?
- DNS, TCP, TLS handshake 비용이 첫 요청에만 들어가고 이후 요청은 기존 연결을 재사용했는지 확인한다.