이 문서는 의문을 가지고 있던 지점을 AI 에이전트와 대화하며 정리한 메모입니다. TCP 커넥션이 바로 끊기지 않는 이유와 TCP keepalive, HTTP keep-alive, HikariCP keepalive의 차이를 정리합니다.
먼저 구분해야 할 것#
이 문서에서 “keepalive"라는 단어는 사실 서로 다른 세 가지를 가리킨다.
| 이름 | 레이어 | 실제 의미 |
|---|---|---|
| TCP keepalive | L4 / OS | 유휴 TCP 소켓에 대해 커널이 생존 확인 probe를 보냄 |
| HTTP keep-alive | L7 / HTTP | 응답 후 TCP 연결을 닫지 않고 다음 HTTP 요청에도 재사용 |
| Pool keepalive | 애플리케이션 | HikariCP 같은 풀 구현이 idle connection을 주기적으로 ping |
이 셋을 섞어서 이해하면 “TCP keepalive = HikariCP keepalive = Nginx keepalive“처럼 보이는데, 실제로는 담당하는 레이어와 목적이 다르다.
TCP 커넥션은 아무것도 안 보내도 유지되는가?#
프로토콜 차원에서는 Yes.
TCP 커넥션은 3-way handshake가 끝나고 ESTABLISHED 상태에 들어가면, 데이터가 없다는 이유만으로 저절로 끊어지지는 않는다. 한쪽이 FIN 또는 RST를 보내거나, 커널/애플리케이션/중간 장비가 timeout 정책으로 정리할 때 끊어진다.
즉, “아무 패킷도 안 보내면 TCP가 알아서 종료한다"는 설명은 틀리다.
하지만 실무에서는 아래 주체들이 idle connection을 정리한다.
- DB 서버 자체의 idle timeout
- 프록시 / 로드밸런서의 idle timeout
- NAT / 방화벽의 connection tracking timeout
- 애플리케이션 또는 connection pool의 자체 수명 정책
그래서 TCP는 유지할 수 있지만, 실제 서비스 경로 전체가 유지해 준다고 볼 수는 없다.
실제로 idle connection을 끊는 주체들#
유휴 커넥션을 누가 끊는지는 환경마다 다르다. 숫자를 하나 외워서 모든 구간에 적용하면 틀리기 쉽다.
대표 예시는 이 정도다.
| 주체 | 예시 |
|---|---|
| DB 서버 | MySQL wait_timeout 기본값은 8시간 |
| Application Load Balancer | connection idle timeout 기본값 60초 |
| Network/Gateway Load Balancer | TCP idle timeout 기본 350초 |
| NAT Gateway | 350초 idle 후 timeout |
| AWS Security Group connection tracking | Nitro 기반 인스턴스에서 설정 가능하며, Nitrov6는 기본 350초이고 다른 인스턴스 타입은 기본 5일(432000초)일 수 있음 |
특히 ALB는 connection idle timeout(기본 60초)과 HTTP client keepalive duration(기본 1시간)을 따로 가진다.
TCP Keepalive#
TCP keepalive는 OS 레벨 기능이다. idle 상태의 TCP socket에 대해 커널이 probe를 보내서 연결이 아직 살아 있는지 확인한다.
중요한 포인트는 다음이다.
- TCP keepalive는 자동으로 항상 켜져 있지 않다.
- socket에
SO_KEEPALIVE가 켜져 있어야 동작한다. - probe는 애플리케이션 데이터가 아니라 커널 레벨의 TCP probe 다.
Linux 기본 sysctl 값은 보통 아래와 같다.
tcp_keepalive_time = 7200 # 2시간 idle 후 시작
tcp_keepalive_intvl = 75 # 75초 간격
tcp_keepalive_probes = 9 # 9번 실패 시 dead 판단이 기본값은 대부분의 프록시/LB/NAT idle timeout보다 길다. 그래서 OS 기본값을 그대로 두면, TCP keepalive가 동작하기도 전에 중간 장비가 먼저 idle connection을 정리해 버릴 수 있다.
여기서 말하는 중간 장비 timeout 은 애플리케이션과 서버 사이에 있는 NAT, 로드밸런서, 방화벽, 프록시 같은 장비가 “너무 오래 조용한 연결"을 자체 정책으로 정리하는 시간을 뜻한다.
예를 들어 NAT가 350초 동안 아무 트래픽이 없으면 연결 상태를 삭제하는데, Linux 기본 TCP keepalive는 7200초(2시간) 가 지나야 첫 probe를 보낸다. 그러면 keepalive probe를 보내기도 전에 NAT가 먼저 “이 연결은 오래 idle이었다"고 판단해 정리할 수 있다.
또 하나 중요하다.
- TCP keepalive가 많은 NAT/방화벽/NLB 계층에는 도움을 줄 수 있지만
- 모든 제품이 keepalive probe를 “유효한 트래픽"으로 보고 idle timer를 리셋하는 것은 아니다
즉, “TCP keepalive만 켜면 어떤 middlebox(중간 장비) idle timeout도 무조건 안 끊긴다"는 식으로 이해하면 안 된다. 제품마다 keepalive probe를 어떻게 처리하는지가 다르기 때문이다.
HikariCP는 무엇을 담당하나?#
HikariCP는 TCP 기능이 아니라 JDBC connection pool 이다. 담당 역할은 크게 두 가지다.
- 이미 열어 둔 DB connection을 재사용한다.
- 죽은 connection을 감지하거나 오래된 connection을 교체한다.
즉, HikariCP는 커넥션을 “계속 들고 있을지”, “언제 버릴지”, “언제 ping할지"를 관리하는 쪽이다.
핵심 설정#
| 설정 | 의미 | 보완 설명 |
|---|---|---|
maxLifetime | 풀 안에서 connection이 머무를 수 있는 최대 수명 | 생존 확인 기능이 아니다. DB/LB timeout보다 조금 짧게 두는 게 권장된다. |
keepaliveTime | idle connection에 대해 주기적 ping 수행 | maxLifetime 보다 작아야 한다. |
connectionTestQuery | 커스텀 validation SQL | JDBC4 isValid() 를 지원하지 않는 legacy driver 에 주로 필요하다. |
idleTimeout | idle connection 제거 기준 | minimumIdle < maximumPoolSize 일 때만 의미가 있다. |
connectionTimeout | 풀에서 connection 대기 시간 | 이 시간 안에 connection을 얻지 못하면 실패한다. 생존 유지와는 다른 설정이다. |
현재 HikariCP README 기준으로는 connectionTimeout=30s, idleTimeout=10m, maxLifetime=30m, keepaliveTime=2m 이 기본값으로 문서화되어 있다. 다만 keepaliveTime 은 예전 버전 문서나 글에서 0으로 설명되는 경우가 있으므로, 운영 중인 라이브러리 버전 문서를 함께 확인하는 편이 안전하다.
HikariCP가 실제로 ping하는 방법#
HikariCP는 idle connection을 ping할 때 아래 둘 중 하나를 쓴다.
- JDBC4
Connection.isValid() connectionTestQuery
즉, “HikariCP는 validation query 없이 전혀 검사하지 않는다"라고 하면 부정확하다. 더 정확한 설명은 이렇다.
- 최신 JDBC driver라면 보통
isValid()를 쓴다. - legacy driver면
connectionTestQuery를 쓸 수 있다. keepaliveTime이나 풀에서 꺼내는 시점의 검증에서 필요할 때 ping이 발생한다.
maxLifetime=30분 이라서 30분 유지되는가?#
아니다.
maxLifetime 은 “30분 동안 살아 있게 보장"하는 값이 아니라, 풀에서 connection을 최대 얼마 동안 보관할지를 정하는 값이다. 즉, HikariCP의 보관 정책이지, 네트워크나 DB가 실제 connection을 30분 동안 보존해 준다는 보장은 아니다.
예를 들어 DB connection 경로가 아래처럼 생겼다고 해보자.
App -> NAT -> DB그리고 정책이 이렇다고 하자.
- NAT: 350초 idle이면 상태 삭제
- DB 서버: 10분 idle이면 connection 종료
- HikariCP:
maxLifetime=30분
이 경우 실제 connection의 수명은 30분이 아니라, 가장 먼저 끊는 쪽에 의해 결정된다.
- 애플리케이션이 DB connection을 하나 만들어 pool에 넣는다.
- 한동안 쿼리가 전혀 없다.
- 350초쯤 지나면 NAT가 먼저 상태를 지울 수 있다.
- 10분쯤 지나면 DB 서버도 idle connection을 닫을 수 있다.
- HikariCP는 30분까지 보관하려 했더라도, 실제 connection은 그 전에 이미 죽어 있을 수 있다.
반대로 HTTP connection 경로에서는 ALB의 60초 idle timeout 같은 별도 정책이 적용된다.
그래서 운영에서는 보통 이렇게 맞춘다.
- 해당 connection이 실제로 지나는 DB / LB / NAT / 방화벽 timeout을 확인한다.
maxLifetime을 그보다 약간 짧게 둔다.- 필요하면
keepaliveTime이나 드라이버/OS의 TCP keepalive를 함께 설정한다.
또한 HikariCP 공식 README는 풀 안정성을 위해 TCP keepalive도 별도로 구성하라고 권장한다. 즉, HikariCP 설정만으로 OS socket keepalive가 자동 구성되는 것은 아니다.
HTTP Keep-Alive vs TCP Keepalive#
이름이 비슷할 뿐, 실제로는 완전히 다른 메커니즘이다.
| HTTP keep-alive | TCP keepalive | |
|---|---|---|
| 레이어 | L7 | L4 / OS |
| 목적 | 같은 TCP 연결로 여러 HTTP 요청/응답 재사용 | idle TCP 연결이 아직 살아 있는지 확인 |
| 기본 동작 | HTTP/1.1에서는 지속 연결이 기본이고 Connection: close 로 종료 | socket에 SO_KEEPALIVE 가 켜졌을 때만 동작 |
| 누가 담당하나 | 브라우저, Nginx, WAS, 프록시 | 커널 / socket 옵션 |
정리하면:
- HTTP keep-alive 는 “응답 후 연결을 닫지 않는 것”
- TCP keepalive 는 “오래 idle인 연결에 probe를 보내는 것”
이다.
Nginx Keepalive 구조#
Client ──[A]──> Nginx ──[B]──> WAS여기서 A와 B는 서로 다른 keepalive 정책을 가진다.
1) Downstream: Client ↔ Nginx#
이 구간은 보통 HTTP persistent connection 을 뜻한다.
keepalive_timeout 75;
keepalive_requests 1000;keepalive_timeout은 클라이언트와의 idle HTTP 연결을 얼마나 열어 둘지keepalive_requests는 하나의 연결로 몇 개 요청까지 처리할지
를 정한다.
2) Upstream: Nginx ↔ WAS#
이 구간은 Nginx가 백엔드로 가는 연결을 캐시해서 재사용하는 구조다.
여기서 말하는 upstream connection reuse 는 아주 단순하게 말하면:
- 요청 하나를 처리할 때마다
Nginx -> WASTCP connection을 매번 새로 만들고 바로 끊는 대신 - 요청 처리가 끝난 뒤에도 그 connection을 잠시 idle 상태로 남겨 두었다가
- 다음 요청이 같은 upstream 서버로 가야 할 때 그 connection을 다시 쓰는 것
이다.
즉, 핵심은 “살아 있는 TCP connection을 다음 HTTP 요청에도 재사용한다” 는 것이다.
이건 성능 최적화와 연결 재사용 정책에 가깝고, 뒤에서 나오는 TCP keepalive와는 목적이 다르다.
upstream backend {
server 127.0.0.1:8080;
keepalive 32;
}
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}여기서 주의할 점:
keepalive 32에서 32라는 숫자는 “지금 처리 중인 요청 수"를 세는 것도 아니고, “Nginx에서 WAS로 가는 전체 연결 수"를 세는 것도 아니다.- 이 숫자는 각 worker가 요청 처리를 마친 뒤 끊지 않고 idle 상태로 남겨 둘 upstream connection의 최대 개수를 뜻한다.
- 즉, 새 요청이 들어왔을 때 다시 쓸 수 있도록 재사용용으로 보관해 두는 connection 을 worker 하나당 최대 32개까지 들고 있겠다는 의미다.
- 예를 들어 worker가 4개라면, idle upstream connection은 worker마다 최대 32개씩 잡힐 수 있으므로 전체 idle connection 수는 32보다 더 많아질 수 있다.
- 오래된 설명처럼
keepalive 32 = 백엔드 연결 총 32개라고 이해하면 틀리다.
예를 들어 worker 1이 127.0.0.1:8080 으로 열린 connection을 20개 idle 상태로 들고 있다고 해보자. 그 상태에서 새 요청이 들어와 같은 upstream 서버로 보내야 하면, Nginx는 이미 열려 있던 idle connection 중 하나를 꺼내 다시 사용할 수 있다. 반대로 idle connection이 하나도 없으면 그때 새 connection을 만든다.
즉, keepalive 32 는 “worker 하나가 다음 요청에 재사용하려고 안 끊고 들고 있는 idle upstream connection을 최대 32개까지 보관한다” 는 뜻으로 읽으면 된다.
또 하나는 버전 차이다.
- 오랫동안 Nginx는 upstream HTTP 프록시 기본값이 HTTP/1.0 이었기 때문에
proxy_http_version 1.1과proxy_set_header Connection ""가 사실상 필수였다. - NGINX 1.29.7부터는 HTTP upstream keep-alive가 기본 동작으로 바뀌었다.
그래도 설정 파일을 읽는 사람 입장에서는 위 두 줄을 명시적으로 유지하는 편이 여전히 이해하기 쉽다.
3) Nginx의 TCP keepalive는 또 별개#
Nginx의 upstream connection reuse와 OS TCP keepalive는 다른 문제다.
둘의 차이를 먼저 분리해서 보면 이해가 쉽다.
- upstream connection reuse: Nginx가 요청 처리가 끝난 뒤에도
Nginx -> WASconnection을 닫지 않고 남겨 두었다가, 다음 요청에 다시 쓰는 것 - TCP keepalive: 아주 오래 idle 상태인 TCP socket이 아직 살아 있는지 커널이 probe로 확인하는 것
즉, 앞의 것은 “다음 요청에 재사용할까?” 에 대한 정책이고, 뒤의 것은 “이 socket이 아직 살아 있나?” 를 확인하는 OS 기능이다.
필요하면 별도로:
proxy_socket_keepalive on;처럼 socket의 SO_KEEPALIVE 를 켤 수 있다.
여기서 SO_KEEPALIVE 는 이 TCP socket에 대해 OS의 TCP keepalive 기능을 켜라는 socket 옵션이다.
좀 더 풀면:
- Nginx가 백엔드(WAS)와 TCP connection을 하나 연다.
- 그 connection은 운영체제 입장에서는 하나의 socket 이다.
proxy_socket_keepalive on;을 주면 Nginx는 그 socket에SO_KEEPALIVE옵션을 켠다.- 그러면 이후의 keepalive probe 전송 여부와 타이밍은 커널의 TCP keepalive 설정을 따른다.
중요한 점은 proxy_socket_keepalive on; 이라고 해서 Nginx가 애플리케이션 데이터를 주기적으로 보내는 것은 아니라는 점이다. 이 설정은 OS에게 “이 소켓에는 TCP keepalive를 적용해라” 라고 알려 주는 것이다.
즉:
keepalive 32는 worker 하나가 요청이 끝난 뒤에도 안 끊고 남겨 둘 upstream connection을 몇 개까지 보관할지proxy_socket_keepalive on은 그 TCP socket에 OS keepalive 기능을 켤지
를 정한다.
즉,
keepalive,keepalive_timeout은 HTTP 연결 재사용 정책proxy_socket_keepalive는 TCP socket keepalive
다.
“커넥션을 유지하는 기술"의 실제 의미#
특별한 마법이 있는 게 아니라, 각 레이어에서 연결을 닫지 않고 재사용하려는 정책을 두는 것이다.
1. 요청/쿼리 처리 완료
2. 응답 후 즉시 FIN을 보내지 않음
3. 일정 시간 idle 상태로 connection을 보관
4. 다음 요청이 오면 같은 connection 재사용
5. timeout 또는 수명 정책이 지나면 정리다만 실제 생존은 다음이 함께 맞아야 한다.
- 애플리케이션/풀의 수명 정책
- DB 서버의 idle timeout
- 프록시/LB/NAT/방화벽 timeout
- 필요 시 TCP keepalive 또는 application-level ping
Keepalive의 단점#
| 단점 | 설명 |
|---|---|
| 메모리 소비 | idle connection도 socket buffer와 각종 per-connection state를 점유 |
| 파일 디스크립터 고갈 | 유휴 연결이 많아지면 새 연결 수용이 어려워질 수 있음 |
| 오래된 connection 보관 | 상대가 비정상 종료했는데 다음 사용 시점까지 모를 수 있음 |
| 백엔드 연결 쏠림 | upstream keepalive가 특정 인스턴스에 connection을 오래 붙잡을 수 있음 |
| 설정 충돌 | maxLifetime, wait_timeout, LB idle timeout이 서로 안 맞으면 간헐적 오류가 남 |
예를 들어:
동시 유휴 커넥션 100,000개 x 20KB = 약 2GB처럼 idle connection 자체가 리소스 비용이 될 수 있다. 이 수치는 어디까지나 대략적인 예시이며, 실제 메모리 사용량은 커널/애플리케이션/프로토콜 구현에 따라 달라진다.
전 구간 Keepalive 전체 그림#
Client ─── Nginx ─── WAS ─── HikariCP ─── MySQL
│ │ │ │
│ │ │ └─ DB idle timeout (예: wait_timeout)
│ │ └─ pool 재사용 / keepalive / maxLifetime
│ └─ downstream/upstream HTTP keep-alive
└─ client-side persistent connection
추가로 각 TCP socket에는 필요 시 OS TCP keepalive를 켤 수 있음요약하면:
- HTTP keep-alive 는 요청 간 재사용
- HikariCP 는 DB connection 재사용과 교체 정책
- TCP keepalive 는 idle socket 생존 확인
을 담당한다.
이 셋은 협력할 수는 있지만, 서로 같은 기능은 아니다.
참고 자료#
- Linux Kernel Documentation - IP Sysctl (
tcp_keepalive_time,tcp_keepalive_intvl,tcp_keepalive_probes) - HikariCP README - Configuration
- NGINX Official Docs -
keepalive_timeout,keepalive_requests - NGINX Official Docs -
ngx_http_upstream_modulekeepalive - NGINX Official Docs -
proxy_socket_keepalive - NGINX Official Docs - CHANGES (
1.29.7) - MySQL 8.4 Reference Manual -
wait_timeout - AWS Docs - Application Load Balancer connection idle timeout
- AWS Docs - Network Load Balancer TCP idle timeout
- AWS Docs - Troubleshoot NAT gateways (350초 idle timeout)
- AWS Docs - EC2 security group connection tracking
- AWS Docs - Classic Load Balancer idle timeout (
TCP keep-alive probes do not prevent termination)
