안동민 개발노트 아이콘

안동민 개발노트

6장 : TCP

TCP 심화와 실무

TCP의 기본 메커니즘을 이해했으니, 이제 실무에서 실제로 마주치는 TCP 관련 문제와 운영 판단을 살펴보겠습니다. 이론적으로 TCP는 이렇게 동작한다를 넘어, 서비스 지연, 커넥션 고갈, 반개방 연결, 공격 대응에서 TCP가 어떤 신호를 남기는지를 읽는 것이 중요합니다.


Nagle 알고리즘과 지연 ACK

작은 데이터를 너무 자주 보내면 헤더 오버헤드와 패킷 처리 비용이 커집니다. IPv4 기준으로 TCP 헤더는 최소 20바이트, IP 헤더도 최소 20바이트이므로, 1바이트 데이터를 보내도 최소 40바이트의 L3/L4 헤더가 붙습니다. 실제 링크에서는 Ethernet, TLS, TCP 옵션 같은 비용도 더해질 수 있습니다.

Nagle 알고리즘은 작은 세그먼트가 연속으로 쏟아지는 상황을 줄이기 위해, 미확인 데이터가 남아 있을 때 작은 데이터를 잠시 모으는 방식입니다. 버퍼가 MSS에 도달하거나, 앞서 보낸 데이터가 확인되면 전송할 수 있습니다.

Nagle 알고리즘 규칙
if (미확인 데이터가 없음) {
    즉시 전송;
} else if (버퍼에 MSS만큼 쌓였음) {
    즉시 전송;  // 꽉 찼으면 기다리지 않음
} else {
    ACK가 올 때까지 버퍼에 모아둠;
    ACK 도착하면 모은 데이터를 한 번에 전송;
}

지연 ACK(Delayed ACK)는 수신 측의 최적화입니다. ACK만을 위한 패킷을 즉시 보내는 대신, 일정 시간(보통 200ms) 동안 기다렸다가 다른 데이터와 함께 ACK를 보냅니다. 지연 ACK(Delayed ACK)는 수신 측의 최적화입니다. ACK만 담은 패킷을 매번 즉시 보내지 않고, 짧은 시간 기다려 데이터와 함께 ACK를 보내거나 여러 ACK를 합칩니다. RFC 기준으로 ACK 지연은 과도하면 안 되며, 구현체마다 기본 지연 시간은 다를 수 있습니다.

문제는 Nagle과 지연 ACK가 작은 요청/응답 패턴에서 동시에 나타날 때입니다. 송신자는 ACK를 기다리며 다음 작은 전송을 묶고, 수신자는 ACK를 조금 늦추면서 응답 데이터를 기다릴 수 있습니다. 영구 교착은 아니지만, 타이머가 풀릴 때까지 불필요한 지연이 보일 수 있습니다.

tcp_nodelay_example.py
import socket

# Nagle 알고리즘 비활성화 (TCP_NODELAY)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.connect(("server.example.com", 8080))

# 이제 작은 데이터도 즉시 전송됨
sock.sendall(b"small data")

# TCP_NODELAY를 사용해야 하는 경우:
# - 게임 서버 (실시간 입력 전달)
# - 대화형 프로토콜 (SSH, Telnet)
# - 작은 RPC/프레임을 지연 없이 보내야 하는 서비스
# - Redis/memcached 같은 캐시 서버

# TCP_NODELAY가 항상 이득은 아님:
# - 대량 파일 전송이나 큰 스트리밍은 작은 쓰기를 줄이는 쪽이 더 중요
# - 애플리케이션 버퍼링, write coalescing, corking과 함께 판단해야 함

TCP Keep-Alive

TCP 연결은 한 번 수립되면, 양쪽에서 데이터를 보내지 않아도 연결 상태가 유지됩니다. 하지만 상대방이 전원 장애, 네트워크 단절, NAT 상태 만료처럼 조용히 사라지면, 애플리케이션은 다음 I/O 전까지 이를 모를 수 있습니다. 이런 상태를 반개방(Half-Open) 연결이라고 부릅니다.

TCP Keep-Alive는 일정 시간 동안 데이터가 오가지 않는 연결에 probe를 보내 상대방이 아직 응답 가능한지 확인하는 메커니즘입니다. TCP 표준에서는 keep-alive를 선택 기능으로 다루며, Linux에서는 SO_KEEPALIVE를 켜야 소켓에 적용됩니다.

파라미터Linux 기본값설명조정 관점
tcp_keepalive_time7200초 (2시간)첫 probe까지 대기 시간장애 감지 시간을 줄이고 싶으면 단축
tcp_keepalive_intvl75초probe 재전송 간격너무 짧으면 불필요한 트래픽 증가
tcp_keepalive_probes9회실패 판단 전 probe 횟수오탐과 장애 감지 시간 사이 절충
keepalive_config.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# TCP Keep-Alive 활성화
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

# Linux에서 Keep-Alive 파라미터 조정
import platform
if platform.system() == "Linux":
    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 300)   # 5분
    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30)   # 30초
    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)      # 5회

# 대략적인 실패 감지 시간: 300 + 30 × 5 = 450초 (7.5분)
# 운영 환경에서는 LB/NAT idle timeout, 애플리케이션 timeout과 함께 맞춰야 함

HTTP의 Keep-Alive와 TCP Keep-Alive는 다른 개념입니다. HTTP Keep-Alive는 같은 TCP 연결을 재사용하여 여러 HTTP 요청을 처리하겠다는 것이고, TCP Keep-Alive는 연결이 살아있는지 주기적으로 확인하겠다는 것입니다.


TCP Fast Open

일반적인 TCP 연결에서는 3-way handshake가 완료된 후에야 데이터를 보낼 수 있습니다. 1 RTT가 소비되는 것입니다.

TCP Fast Open(TFO)는 최초 연결에서 서버가 쿠키를 발급하고, 이후 같은 서버로 재연결할 때 클라이언트가 SYN 패킷에 쿠키와 데이터를 함께 실어 보내는 기술입니다. 서버가 쿠키를 검증하면 3-way handshake가 끝나기 전에도 데이터를 애플리케이션에 전달할 수 있어, 재연결의 첫 요청에서 최대 1 RTT를 줄일 수 있습니다.

TFO는 모든 클라이언트, 서버, 중간 장비에서 안정적으로 동작한다고 가정하면 안 됩니다. SYN 데이터는 재전송될 수 있으므로, 서버는 중복 실행되면 안 되는 요청을 조심스럽게 다뤄야 합니다. Linux는 TFO 기능을 제공하지만, 실제 사용 여부는 커널 설정, 소켓 옵션, 서비스 정책에 따라 달라집니다.


실무에서 자주 만나는 TCP 문제

실제 서비스를 운영하면서 자주 만나는 TCP 관련 문제를 정리해 보겠습니다.

TIME_WAIT 누적: 짧은 연결을 매우 자주 만들고 닫으면 TIME_WAIT 상태가 많이 보일 수 있습니다. TIME_WAIT 자체는 늦게 도착한 세그먼트가 새 연결을 오염시키지 않도록 남는 정상 상태입니다. 문제는 주로 클라이언트/프록시처럼 많은 outbound 연결을 만들 때 로컬 포트가 고갈되는 경우입니다. 해결은 커넥션 재사용, keep-alive, 커넥션 풀, 포트 범위 조정, 부하 분산을 먼저 고려해야 합니다.

CLOSE_WAIT 누적: CLOSE_WAIT은 상대방이 FIN을 보냈고, 우리 애플리케이션이 아직 close()를 호출하지 않은 상태입니다. 이 값이 지속적으로 증가한다면 대개 애플리케이션의 소켓 정리 누락을 의심해야 합니다.

close_wait_prevention.py
import socket

# 나쁜 예: close() 호출 누락 가능
def bad_example():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(("server.example.com", 80))
    sock.sendall(b"GET / HTTP/1.0\r\n\r\n")
    data = sock.recv(4096)
    # 예외 발생 시 close()가 호출되지 않음!
    # → CLOSE_WAIT 상태로 남을 수 있음
    sock.close()

# 좋은 예: with문으로 자동 정리
def good_example():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect(("server.example.com", 80))
        sock.sendall(b"GET / HTTP/1.0\r\n\r\n")
        data = sock.recv(4096)
    # with 블록을 벗어나면 자동으로 close() 호출
    # 예외가 발생해도 반드시 정리됨

커넥션 풀 고갈: 데이터베이스나 외부 API 연결을 풀로 관리할 때, 풀의 모든 연결이 사용 중이면 새 요청이 대기합니다. 풀 크기만 키우면 하위 시스템을 더 압박할 수 있으므로, 타임아웃, 최대 대기 시간, 백프레셔, 재시도 정책을 함께 봐야 합니다.

연결 타임아웃 vs 읽기 타임아웃: HTTP 클라이언트에서 두 가지 타임아웃을 구분해야 합니다.

타임아웃 유형구간적정값초과 시 의미
Connection TimeoutSYN → SYN-ACK1~5초서버 도달 불가 또는 포트 미응답
Read Timeout연결 후 → 응답 데이터5~30초서버 처리 지연
Write Timeout데이터 전송 완료5~15초네트워크 혼잡 또는 상대방 수신 불가
Idle Timeout마지막 데이터 이후30~300초유휴 연결 정리

SYN Flood 공격과 방어

SYN Flood 공격: 공격자가 대량의 SYN 패킷을 보내고 최종 ACK를 보내지 않으면, 서버는 반쯤 열린 연결을 위한 SYN backlog와 관련 자원을 소모합니다. 공격 트래픽이 충분히 크면 정상 사용자의 연결 수립도 밀릴 수 있습니다.

syn_flood_defense.sh
#!/bin/bash
# === SYN Flood 방어 설정 ===

# SYN Cookie 활성화
echo "SYN Cookie: $(cat /proc/sys/net/ipv4/tcp_syncookies)"
# sudo sysctl -w net.ipv4.tcp_syncookies=1

# SYN Backlog 크기 증가
echo "SYN Backlog: $(cat /proc/sys/net/ipv4/tcp_max_syn_backlog)"
# sudo sysctl -w net.ipv4.tcp_max_syn_backlog=65535

# SYN-ACK 재전송 횟수 감소 (기본 5 → 2)
echo "SYN-ACK retries: $(cat /proc/sys/net/ipv4/tcp_synack_retries)"
# sudo sysctl -w net.ipv4.tcp_synack_retries=2

# 현재 SYN_RECV 상태 확인
echo ""
echo "=== SYN_RECV 연결 수 ==="
ss -tan state syn-recv | wc -l

TCP 성능 튜닝 체크리스트

항목확인 명령관찰 신호우선 확인할 조치
TIME_WAITss -s짧은 연결 종료가 급증연결 재사용, 커넥션 풀, 포트 범위 확인
CLOSE_WAITss -tan state close-wait지속 증가애플리케이션 close 누락 확인
Listen backlogss -tnlpaccept queue 포화애플리케이션 accept 속도, somaxconn 조정
SYN backlogss -tan state syn-recvSYN_RECV 급증SYN cookies, rate limit, 방화벽 정책
혼잡 제어sysctl net.ipv4.tcp_congestion_control환경과 알고리즘 불일치CUBIC/BBR 등 환경별 비교 검증
버퍼 크기sysctl net.ipv4.tcp_rmem처리량 제한 의심BDP 기준으로 수신/송신 버퍼 검토
RTTss -ti지연 증가서버 위치, CDN, 라우팅, 재시도 정책 확인
Retransmitnetstat -s | grep -i retrans재전송 증가패킷 손실, 혼잡, 무선/터널 구간 확인
작은 쓰기 지연애플리케이션 로그, 패킷 캡처일정한 짧은 지연 반복TCP_NODELAY 또는 애플리케이션 버퍼링 검토

다음 장에서는 TCP와는 정반대의 철학을 가진 UDP를 살펴보고, 언제 어떤 프로토콜을 선택해야 하는지를 판단하는 기준을 세워 보겠습니다.