회사에서 istio를 도입하면서 tcp connection 관련 트러블 슈팅을 해야하는 일이 많아졌다. 대표적인것이 pod gracefulshutdown에 따라 istio proxy container 종료전에 남아있는 connection을 추적하는 일이었는데, 이 과정에서 CS 적인 지식.. 특히 네트워크 부분에 대한 깊이 있는 이해가 필요했다. 그 와중에 다시 접한 개념이 옛날에 어렴풋이 봤던 TIME_WAIT Socket이라는 개념으로 복습을 위해 정리한다.
TCP는 연결을 종료할 때 4-way handshake를 사용하게 된다. 클라이언트 - 서버 통신모델에서 양쪽 모두에게 통신 종료를 알리고 확인하는 단계를 포함한다.
[클라이언트] [서버]
ESTABLISHED ESTABLISHED
| |
| ----- FIN (seq=x) --------> |
FIN_WAIT_1 |
| |
| <---- ACK (seq=y, ack=x+1) |
FIN_WAIT_2 CLOSE_WAIT
| |
| (남은 데이터 전송 완료)
| |
| <---- FIN (seq=y) ------- |
| LAST_ACK
| |
| ----- ACK (ack=y+1) ------> |
TIME_WAIT CLOSED
| |
(2MSL 타임아웃) |
| |
CLOSED |
TCP 연결 종료할 때는 먼저 종료를 선언하는 쪽이 주체가 되며 클라이언트와 서버 모두가 주체가 될 수 있다. 여기서 TIME_WAIT은 TCP 연결을 종료하는 주체가 일정 시간 동안 그 연결 정보를 유지하는 소켓의 상태를 의미한다.
TIME_WAIT 상태는 기본적으로 2 * MSL(Maximum Segment Lifetime) 동안 유지되며 Linux에서는 /proc/sys/net/ipv4/tcp_fin_timeout 등으로 조절할 수 있다고 한다.
크게 2가지가 있다.
이전 연결에서 네트워크 어딘가에 남아있는 지연된 패킷이 새로운 연결에 영향을 주지 않도록 하기 위함.
한 마디로 늦게 들어오는 패킷이 있을까봐 연결 정보를 널널하게 유지하는 것이다.
위 TCP 연결 그림에서 FIN → ACK 과정에 ACK이 유실되면 FIN은 재전송된다. TCP는 신뢰성있는 프로토콜로 재전송 메커니즘이 있기 때문이다!
대표적으로는 이런 상황이다.
상대방: FIN 보냄
나: ACK 보냄 + TIME_WAIT 진입
이때 ACK이 유실된다면
상대방은 FIN을 재전송한다.
하지만 나는 TIME_WAIT 상태이기 때문에 이전 연결 정보를 유지하고있으므로 다시 ACK 응답이 가능하다.
"소켓은 네트워크 통신을 위한 엔드포인트로, 운영체제에서 IP 주소와 포트 번호의 조합으로 식별됩니다. 하나의 소켓은 특정 프로토콜(TCP/UDP)을 사용하여 특정 IP 주소와 포트 번호의 조합을 통해 통신합니다."
TIME_WAIT 소켓이 생성되면 결국 os 단에서 하나의 포트를 점유하고 있는 상태가 되는 것이다. 그런데 하나의 애플리케이션이 생성할 수 있는 소켓의 양은 한정적이므로, TIME_WAIT 소켓이 많아지면 신규 연결을 할 수가 없다...
HTTP는 TCP 기반이며 HTTP 클라이언트가 매 요청마다 새로운 TCP 연결을 열고 닫게 되며 이러한 문제를 직격으로 맞게 된다.. 특히 API 서버나 프론트엔드 백엔드 간 통신에서 이런 방식이 누적되면 수천 개의 TIME_WAIT 상태가 쌓이는 걸 보게 된다.
이를 줄이는 가장 효과적인 방법 중 하나가 바로 HTTP Keep-Alive이다. HTTP 1.1부터 기본 동작인 Connection 재사용 방식으로, 하나의 TCP 연결 위에서 여러 HTTP 요청을 연달아 보내는 방식이다.
예를 들어 기존에는
요청 1 → TCP 연결 열기 → 요청 → 응답 → 연결 닫기 (TIME_WAIT 생성)
요청 2 → 다시 TCP 연결 열기 → 요청 → 응답 → 연결 닫기 (TIME_WAIT 생성)
...
이랬다면 keep-alive 도입 이후에는
TCP 연결 열기
요청 1 → 응답
요청 2 → 응답
요청 3 → 응답
...
일정 시간 후 연결 닫기 (TIME_WAIT 1개만 생성)
이를 통해
TIME_WAIT 소켓 수 급감
연결 생성/해제 비용 감소로 성능 개선
클라이언트/서버 모두 리소스 절약
를 얻을 수 있다.
Connection Pool(커넥션 풀)이란, 자주 쓰이는 네트워크 연결(TCP, DB 등)을 미리 만들어두고 재사용하는 기법이다
DB 서버와 클라이언트는 TCP/IP 위에서 동작하며, 각 DB는 자체 프로토콜을 정의하여 요청/응답을 주고 받는다.
| DBMS | 프로토콜 종류 | 특징 |
|---|---|---|
| MySQL / MariaDB | MySQL Native Protocol | TCP 기반, 바이너리 프로토콜 |
| PostgreSQL | PostgreSQL Protocol | 자체 TCP 바이너리 프로토콜 |
| MongoDB | MongoDB Wire Protocol | TCP + BSON 기반 |
| Redis | RESP (REdis Serialization Protocol) | 텍스트 기반이지만 매우 간단하고 빠름 |
| Cassandra | CQL Binary Protocol | TCP 기반, CQL 바이너리 |
| Elasticsearch | HTTP + JSON | 예외적으로 HTTP 사용 (RESTful) |
| ClickHouse | Native TCP Protocol, HTTP도 지원 | binary + optional HTTP |
| SQLite | 로컬 파일 기반 | 네트워크 없음 (예외) |
그래서 매 요청마다 tcp 연결을 맺으면 Handshake 시간도 소요될 뿐더러 TIME_WAIT 누적되고...DB 서버 부하도 증가한다.
그래서 다음과 같은 방식으로 connection pool을 관리한다.
서버 시작 시 DB 연결 10개 미리 생성
↓
요청이 오면 → 기존 연결 할당 → 쿼리 실행 → 다시 풀에 반납
↓
요청 끝나도 연결은 살아있음 (Keep-Alive)
TCP 및 DB handshake 비용 제거
재사용으로 TIME_WAIT 줄어듦
최대 동시 연결 수 제어 가능 (풀 크기)