좀비 커넥션은 어떻게 생기는가
TCP 연결은 명시적으로 닫히지 않는 한 커널 입장에서 계속 "살아있는" 상태로 유지된다. 문제는 상대방이 정상 종료(FIN) 없이 사라지는 경우다. 서버 프로세스 강제 종료, NAT 게이트웨이의 세션 만료, 무선 네트워크 단절, 클라우드 로드밸런서의 유휴 커넥션 정리 등이 대표적이다. 이런 상황에서는 한쪽만 연결이 끊겼다고 알고, 다른 쪽은 여전히 소켓을 열어둔 채 응답을 기다린다. 이것이 좀비 커넥션이다.
좀비 커넥션이 위험한 이유는 조용하기 때문이다. 애플리케이션은 아무 에러도 받지 못하고 recv()에서 무한정 블로킹되거나, 커넥션 풀에서 죽은 커넥션을 정상으로 착각하고 계속 빌려준다. 커넥션 풀이 죽은 소켓으로 가득 차면 신규 요청은 전부 타임아웃되고, 겉으로는 "DB는 멀쩡한데 앱만 느려지는" 장애로 나타난다.
TCP keepalive의 동작 원리
TCP keepalive는 유휴 상태의 연결에 대해 주기적으로 빈 ACK 프로브를 보내 상대가 살아있는지 확인하는 커널 기능이다. 세 가지 파라미터로 제어한다.
tcp_keepalive_time: 마지막 데이터 이후 첫 프로브까지 대기 시간(기본 7200초 = 2시간)tcp_keepalive_intvl: 프로브 재전송 간격(기본 75초)tcp_keepalive_probes: 응답 없을 때 연결을 끊기까지 시도 횟수(기본 9회)
기본값의 문제는 명확하다. 첫 프로브까지 2시간을 기다린다. 좀비 커넥션이 두 시간 동안 방치된다는 뜻이다. 실무에서는 이 값을 대폭 낮춰야 한다.
시스템 레벨 설정
리눅스 커널 전역 설정은 sysctl로 조정한다. 아래는 유휴 60초 후 첫 프로브, 10초 간격으로 6회 재시도하는 설정이다. 최악의 경우 약 120초 안에 죽은 연결을 감지한다.
# /etc/sysctl.d/99-keepalive.conf
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 6
# 적용
sudo sysctl -p /etc/sysctl.d/99-keepalive.conf
# 현재 값 확인
sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes
다만 전역 설정은 모든 소켓에 영향을 주고, keepalive는 소켓별로 SO_KEEPALIVE 옵션을 켜야만 실제로 동작한다. 옵션을 켜지 않은 소켓에는 아무 효과가 없다.
애플리케이션 레벨 설정
더 정밀한 제어는 소켓 옵션으로 개별 지정한다. 파이썬 예시로, 전역 sysctl을 건드리지 않고 특정 소켓에만 keepalive를 적용한다.
import socket
def enable_keepalive(sock, idle=60, interval=10, count=6):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# 리눅스 전용 옵션 (macOS는 TCP_KEEPALIVE 사용)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, idle)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, interval)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, count)
return sock
s = socket.create_connection(("db.internal", 5432))
enable_keepalive(s)
PostgreSQL 같은 서버는 접속 문자열에서 직접 keepalive를 지정할 수 있다. 커넥션 풀이 죽은 커넥션을 오래 물고 있는 문제를 크게 줄여준다.
postgresql://user:[email protected]:5432/app
?keepalives=1
&keepalives_idle=60
&keepalives_interval=10
&keepalives_count=6
keepalive와 애플리케이션 타임아웃은 다르다
흔한 오해는 keepalive만 켜면 모든 무응답을 막는다는 생각이다. keepalive는 "연결 자체가 살아있는가"만 확인한다. 상대 서버가 TCP 스택은 살아있는데 애플리케이션이 응답을 못 하는 경우(GC 스톱, 데드락, 느린 쿼리)에는 프로브에 정상 응답하므로 keepalive가 개입하지 않는다. 이 계층은 애플리케이션 타임아웃으로 막아야 한다.
| 구분 | TCP keepalive | 애플리케이션 타임아웃 |
|---|---|---|
| 감지 대상 | 연결 단절, 상대 소멸 | 느린 응답, 무응답 |
| 계층 | 커널(L4) | 애플리케이션(L7) |
| 못 잡는 것 | 살아있지만 느린 서버 | 이미 끊긴 커넥션(감지 지연) |
둘은 경쟁이 아니라 보완 관계다. keepalive로 죽은 연결을 정리하고, read/write 타임아웃으로 느린 응답을 끊는다.
주의점과 함정
첫째, 중간 장비를 고려해야 한다. NAT나 클라우드 로드밸런서는 유휴 세션을 자체적으로 만료시킨다(AWS NLB는 기본 350초). keepalive 프로브 간격이 이 값보다 길면, 장비가 먼저 세션을 끊어 오히려 keepalive가 무의미해진다. 반드시 인프라의 유휴 타임아웃보다 keepalive_time을 짧게 잡아야 한다.
둘째, 프로브를 너무 공격적으로 설정하면 유휴 연결이 많은 환경에서 불필요한 트래픽과 배터리 소모(모바일)를 유발한다. 내부 서비스 간 커넥션은 짧게, 모바일 클라이언트 대상은 다소 여유 있게 차등 적용하는 편이 좋다.
셋째, keepalive 감지에는 여전히 수십 초에서 수 분이 걸린다. 즉시성이 필요한 요청 경로에서는 애플리케이션 레벨 타임아웃과 재시도, 서킷 브레이커를 함께 두는 것이 안전하다. keepalive는 방치된 유휴 커넥션 정리를 위한 안전망이지, 실시간 장애 감지 수단은 아니다.