TCP/UDP Internals
TCP/UDP Internals: 신뢰성과 지연을 고르는 전송 계층
섹션 제목: “TCP/UDP Internals: 신뢰성과 지연을 고르는 전송 계층”1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”TCP(Transmission Control Protocol)는 IP 위에서 연결, 순서, 재전송, 흐름 제어, 혼잡 제어를 제공하는 신뢰성 우선 전송 프로토콜이다. UDP(User Datagram Protocol)는 IP 위에서 포트와 체크섬만 얇게 얹고 나머지 책임을 애플리케이션에 넘기는 지연 최소화 전송 프로토콜이다.
둘 다 OSI 모델의 L4(Transport Layer)에 속한다.
앞 문서인 OSI 모델 & Web Server에서 L7은 HTTP 같은 응용 규약, L4는 포트와 연결 생명주기를 다루는 계층이라고 봤다.
이 문서는 그 L4 안에서 “왜 TCP는 느려 보이지만 안전하고, 왜 UDP는 빠르지만 조심해야 하는가”를 설명한다.
첫 회독에서는 Linux 커널 파라미터나 패킷 캡처 도구를 몰라도 된다.
중요한 것은 SYN, ACK, seq, rwnd, cwnd, MSS, TIME_WAIT 같은 단어가 어떤 문제를 해결하기 위해 생겼는지 이해하는 것이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”HTTP, WebSocket, gRPC, DB 연결, Redis 연결, SSH는 대부분 TCP 위에서 동작한다. DNS, 실시간 미디어, 일부 게임 프로토콜, QUIC/HTTP/3는 UDP를 선택하거나 UDP 위에 자체 신뢰성 계층을 얹는다. 따라서 API 장애를 볼 때도 “HTTP 상태 코드 문제”와 “TCP 연결 자체가 성립하지 않는 문제”를 분리해야 한다.
예를 들어 502 Bad Gateway는 L7 프록시가 타겟으로부터 올바른 응답을 받지 못했다는 신호다.
그 안에는 애플리케이션 crash, 포트 미리스닝, ALB와 Node.js keep-alive timeout 불일치, TCP reset이 모두 숨어 있을 수 있다.
반대로 Connection refused는 서버 프로세스가 해당 포트를 듣지 않거나 중간 장비가 즉시 거절했다는 L4 신호에 가깝다.
timeout은 상대가 거절조차 하지 않고 응답이 돌아오지 않는 상태라 라우팅, 방화벽, NACL, 패킷 손실까지 의심 범위가 넓어진다.
브라우저의 fetch()나 Node.js의 HTTP client는 이 과정을 자동으로 처리한다.
하지만 지연 시간을 분해하면 DNS 조회, TCP 3-way handshake, TLS handshake, HTTP 요청/응답이 따로 비용을 만든다.
이 중 TCP 연결 재사용만 잘해도 매 요청마다 handshake를 반복하는 비용을 줄일 수 있다.
2.5 선행 기술의 한계 — TCP와 UDP가 등장한 이유
섹션 제목: “2.5 선행 기술의 한계 — TCP와 UDP가 등장한 이유”IP(Internet Protocol)는 기본적으로 best-effort delivery다. best-effort는 “가능한 한 전달해 보지만, 손실·중복·순서 뒤바뀜을 상위 계층에 알려주지 않을 수 있다”는 뜻이다. 라우터는 패킷을 다음 홉으로 넘기는 데 집중하고, 최종 애플리케이션이 원하는 순서와 의미까지 보장하지 않는다.
이 상태에서 파일 다운로드나 HTTP 응답을 그대로 IP에 맡기면 세 가지 문제가 생긴다.
- 중간 패킷 하나가 사라져도 수신자는 어떤 바이트가 빠졌는지 모른다.
- 뒤 패킷이 먼저 도착하면 애플리케이션은 원래 순서를 재구성해야 한다.
- 수신자나 네트워크가 버티지 못하는 속도로 보내면 버퍼와 라우터 큐가 넘친다.
TCP는 이 문제를 전송 계층에서 해결한다. 시퀀스 번호로 바이트 위치를 붙이고, ACK로 받은 범위를 확인하고, 손실이 의심되면 재전송하며, 수신자와 네트워크 상태에 맞춰 전송 속도를 조절한다. 즉 TCP의 철학은 애플리케이션이 바이트 스트림을 믿고 읽을 수 있게 하자다.
UDP는 반대 선택을 한다. 전송 계층에서 순서와 재전송을 약속하지 않는 대신, 첫 패킷부터 바로 보내고 작은 헤더만 붙인다. DNS처럼 한 번의 질의/응답이 작거나, 영상통화처럼 늦게 도착한 프레임보다 최신 프레임이 중요한 경우에는 이 선택이 더 낫다. 즉 UDP의 철학은 공통 신뢰성 비용을 빼고 애플리케이션별로 필요한 만큼만 구현하게 하자다.
QUIC은 두 선택지의 경계를 다시 섞은 사례다. QUIC은 UDP 위에서 동작하지만 내부적으로 연결 ID, 재전송, 혼잡 제어, TLS 1.3, 독립 스트림을 구현한다. “UDP를 쓰면 무조건 신뢰성이 없다”가 아니라, “UDP 자체는 신뢰성을 주지 않으므로 QUIC 같은 상위 프로토콜이 직접 책임진다”가 정확하다.
3. 읽기 전 짧은 계층 리마인더
섹션 제목: “3. 읽기 전 짧은 계층 리마인더”이 문서에서 말하는 연결은 대부분 L4 연결이다. HTTP 요청 1개가 실패했다고 해서 항상 L4 연결이 실패한 것은 아니다. 반대로 TCP 연결이 성립하지 않으면 그 위의 HTTP, WebSocket, gRPC는 시작도 못 한다.
L7 Application HTTP, WebSocket, gRPC, DNS messageL6 Presentation TLS certificate, encryptionL4 Transport TCP, UDP, port, handshake, reset, timeoutL3 Network IP address, routing, Security Group, NACLL2/L1 Ethernet, Wi-Fi, physical linkOSI 모델 & Web Server에서 배운 진단 순서를 그대로 가져오면 된다.
404,401,403은 대부분 L7 판단에서 시작한다.502,504는 L7 프록시가 보고하지만 원인은 L4/L7에 걸칠 수 있다.Connection refused는 L4에서 즉시 거절된 신호에 가깝다.timeout은 L3/L4/L7 어디서 멈췄는지 더 좁혀야 한다.certificate오류는 TLS가 있는 L6 문제로 본다.
TCP/UDP를 공부하는 이유는 이 중 L4 신호를 읽기 위해서다. 어느 계층이 문제인지 확정하면, 확인해야 할 로그와 설정의 범위가 급격히 줄어든다.
4. TCP의 핵심 모델
섹션 제목: “4. TCP의 핵심 모델”TCP는 메시지 단위 프로토콜이 아니라 바이트 스트림 프로토콜이다.
애플리케이션이 write("hello")와 write("world")를 두 번 호출해도 수신자는 helloworld가 한 번에 보이거나 hel과 loworld처럼 나뉘어 보일 수 있다.
TCP가 보장하는 것은 “write 단위 보존”이 아니라 “바이트 순서와 손실 복구”다.
이 차이를 모르면 네트워크 코드를 잘못 짜기 쉽다.
예를 들어 직접 TCP 소켓 위에 프로토콜을 만들 때는 메시지 길이 prefix나 delimiter를 둬야 한다.
HTTP는 이 역할을 헤더와 Content-Length, chunked encoding으로 해결한다.
WebSocket은 TCP 위에서 별도의 frame 경계를 만든다.
4-1. TCP 연결은 4-tuple로 식별된다
섹션 제목: “4-1. TCP 연결은 4-tuple로 식별된다”TCP 연결 하나는 보통 아래 네 값으로 식별된다.
(source IP, source port, destination IP, destination port)예를 들어 클라이언트가 10.0.1.10:53124에서 api.example.com:443으로 접속하면 연결 식별자는 다음과 같다.
(10.0.1.10, 53124, 203.0.113.20, 443)서버의 443 포트 하나에 수천 클라이언트가 붙을 수 있는 이유는 source IP와 source port가 다르기 때문이다. 클라이언트 OS는 outgoing connection마다 ephemeral port(임시 포트)를 할당한다. 짧은 연결을 너무 많이 만들면 이 임시 포트가 TIME_WAIT 상태로 묶여 새 연결에 쓸 포트가 부족해질 수 있다.
수치 감각
| 항목 | 흔한 값 | 의미 |
|---|---|---|
| 서버 listen port | 80, 443, 3000 | 서버가 외부 요청을 받는 고정 포트 |
| 클라이언트 ephemeral port | 대략 1024~65535 범위 | OS가 새 outgoing connection에 임시 배정 |
| 연결 식별자 | 4-tuple | 같은 서버 포트라도 클라이언트가 다르면 별도 연결 |
| TIME_WAIT 유지 시간 | 구현마다 다르지만 2MSL 개념 | 같은 4-tuple의 옛 패킷과 새 연결을 섞지 않기 위한 대기 |
TCP lifecycle은 처음부터 모든 상태를 외울 필요가 없다. 서버는 LISTEN에서 연결을 기다리고, 양쪽은 SYN_SENT와 SYN_RECEIVED를 거쳐 ESTABLISHED로 들어간다. 종료할 때 능동적으로 닫은 쪽은 FIN_WAIT 계열과 TIME_WAIT을 거칠 수 있고, 상대가 먼저 닫았는데 내 애플리케이션이 socket을 닫지 않으면 CLOSE_WAIT이 쌓인다. 이 미니맵만 있어도 “연결을 못 맺는 문제”와 “맺은 뒤 닫지 못하는 문제”를 분리할 수 있다.
4-2. 3-way handshake: 양방향 통신 준비를 확인한다
섹션 제목: “4-2. 3-way handshake: 양방향 통신 준비를 확인한다”TCP는 데이터를 보내기 전에 양쪽이 서로의 초기 시퀀스 번호를 교환한다. 이 과정이 3-way handshake다.
sequenceDiagram participant C as Client participant S as Server C->>S: SYN, seq = x S-->>C: SYN-ACK, seq = y, ack = x + 1 C->>S: ACK, ack = y + 1 C->>S: Application data can now flow
여기서 SYN은 synchronize의 약자다.
서로 사용할 초기 sequence number를 맞춘다는 뜻이다.
ACK는 acknowledgment의 약자다.
“내가 여기까지 받았다”는 확인이다.
손으로 따라가는 예시
1. 클라이언트가 SYN을 보낸다. seq = 1000
2. 서버가 SYN-ACK를 보낸다. seq = 7000 ack = 1001 의미: "네 SYN 1바이트를 받았고, 내 시작 번호는 7000이다."
3. 클라이언트가 ACK를 보낸다. ack = 7001 의미: "네 SYN 1바이트를 받았다."SYN 자체도 시퀀스 번호 공간을 1만큼 소비한다.
그래서 클라이언트의 다음 데이터 바이트는 seq = 1001부터 시작한다.
이 규칙 덕분에 양쪽은 연결 시작 직후부터 어느 바이트가 어디에 놓이는지 같은 좌표계를 갖는다.
왜 2-way가 아니라 3-way인가
2-way라면 클라이언트가 서버의 응답을 받았다는 사실을 서버가 알 수 없다. 서버 입장에서는 자신의 SYN-ACK가 클라이언트에게 도달했는지 불확실하다. 세 번째 ACK가 도착해야 서버도 “양방향 경로가 살아 있고, 서로의 초기 번호를 확인했다”고 판단할 수 있다.
실패 신호
| 관측 | 의미 | 먼저 볼 개념 |
|---|---|---|
| SYN만 반복되고 SYN-ACK가 없음 | 서버까지 도달하지 않거나 응답 경로가 막힘 | 라우팅, Security Group, NACL, 방화벽 |
| SYN-ACK 뒤 ACK가 없음 | 클라이언트 응답 경로 또는 중간 장비 문제 | 비대칭 라우팅, NAT, 방화벽 |
| 즉시 RST | 포트가 닫혔거나 중간 장비가 거절 | Connection refused, listener 상태 |
| 연결 후 바로 RST | 앱/프록시가 연결을 강제 종료 | keep-alive mismatch, 프로세스 재시작 |
4-3. Sequence number와 ACK: TCP 신뢰성의 좌표계
섹션 제목: “4-3. Sequence number와 ACK: TCP 신뢰성의 좌표계”TCP의 재전송은 “패킷 번호”가 아니라 “바이트 번호”를 기준으로 동작한다. 각 TCP 세그먼트에는 시작 sequence number가 붙고, ACK에는 다음으로 기대하는 바이트 번호가 들어간다. ACK가 누적(cumulative)이라는 점이 중요하다.
worked example: 3개 세그먼트 전송
MSS(Maximum Segment Size)가 1000 bytes라고 가정하자. 클라이언트가 총 3000 bytes를 보낸다.
Segment A: seq = 1001, length = 1000 -> bytes 1001~2000Segment B: seq = 2001, length = 1000 -> bytes 2001~3000Segment C: seq = 3001, length = 1000 -> bytes 3001~4000수신자가 A와 B를 잘 받으면 ACK = 3001을 보낸다.
이 ACK는 “3000까지 받았고, 다음은 3001을 기대한다”는 뜻이다.
수신자가 C까지 받으면 ACK = 4001이 된다.
손실이 끼어드는 경우
Segment A 도착 -> ACK 2001Segment B 손실Segment C 도착 -> 여전히 ACK 2001Segment C 재도착/추가 데이터 도착 -> 계속 ACK 2001 반복수신자는 B가 빠졌으므로 C를 먼저 받았더라도 누적 ACK를 3001로 올릴 수 없다. 송신자는 같은 ACK가 여러 번 반복되는 것을 보고 “2001부터 시작하는 세그먼트가 손실됐을 수 있다”고 추론한다. 3번의 중복 ACK가 오면 timeout을 기다리지 않고 빠른 재전송(Fast Retransmit)을 시도한다.
반례: ACK가 왔다고 모든 데이터가 애플리케이션 처리된 것은 아니다
TCP ACK는 커널 수신 버퍼까지 도착했다는 신호다. 서버 애플리케이션이 그 데이터를 읽어서 비즈니스 로직을 완료했다는 뜻은 아니다. 따라서 “TCP ACK를 받았으니 주문 처리도 성공했다”처럼 해석하면 안 된다. 애플리케이션 성공은 HTTP 2xx, gRPC status, 도메인 이벤트 같은 더 위 계층의 확인으로 판단해야 한다.
4-4. 재전송: 손실을 복구하지만 지연을 만든다
섹션 제목: “4-4. 재전송: 손실을 복구하지만 지연을 만든다”TCP는 손실을 감지하면 데이터를 다시 보낸다. 이 덕분에 애플리케이션은 대체로 안정적인 바이트 스트림을 읽을 수 있다. 하지만 재전송은 공짜가 아니다. 손실을 감지하려면 시간이 필요하고, 재전송된 세그먼트가 다시 왕복해야 한다.
손실 감지 방식은 크게 두 가지다.
| 방식 | 언제 발생하는가 | 지연 특성 |
|---|---|---|
| Retransmission Timeout(RTO) | ACK가 정해진 시간 안에 오지 않음 | 보수적이라 지연이 크다 |
| Fast Retransmit | 같은 ACK가 3번 이상 중복됨 | 네트워크가 아직 흐르고 있다는 신호라 더 빠르다 |
수치 감각
RTT가 30ms인 네트워크에서 세그먼트 하나가 손실됐다고 하자. Fast Retransmit이 가능하면 다음 ACK 흐름 안에서 손실을 빨리 감지할 수 있다. 반대로 전송량이 작아 중복 ACK가 충분히 나오지 않으면 RTO까지 기다릴 수 있다. 이때 사용자는 “서버 처리는 빠른데 응답이 가끔 튄다”는 p95/p99 지연으로 체감한다.
실패 신호
- 평균 지연은 낮은데 p99만 튄다.
- 작은 응답은 정상인데 큰 다운로드에서만 멈칫한다.
- 같은 endpoint가 Wi-Fi에서는 괜찮고 VPN 또는 모바일망에서만 느리다.
- 애플리케이션 로그의 처리 시간은 짧은데 클라이언트 total time이 길다.
이런 경우 “서버 코드가 느리다”만 보지 말고 TCP 재전송, MTU, 중간 네트워크 손실을 후보에 넣어야 한다.
4-5. 요청 하나의 시간 예산: handshake는 어디에 숨어 있는가
섹션 제목: “4-5. 요청 하나의 시간 예산: handshake는 어디에 숨어 있는가”프론트엔드 코드에서는 fetch() 한 줄로 보이지만, 네트워크 시간 예산은 여러 구간으로 나뉜다.
이 절의 목적은 브라우저 API를 설명하는 것이 아니라 TCP 연결 비용이 전체 지연에 어디서 들어가는지 보는 것이다.
첫 요청DNS lookup -> TCP 3-way handshake -> TLS handshake -> HTTP request -> server processing -> response download
연결 재사용 요청HTTP request -> server processing -> response download수치 예시: RTT 30ms인 외부 API
| 단계 | 새 TCP+TLS 연결 | 기존 연결 재사용 |
|---|---|---|
| DNS cache miss | 20ms | 0ms |
| TCP handshake | 30ms | 0ms |
| TLS handshake | 30ms 이상 | 0ms 또는 재개 |
| 서버 처리 | 40ms | 40ms |
| 응답 다운로드 | 10ms | 10ms |
| 총합 감각 | 약 130ms+ | 약 50ms |
이 표는 정확한 성능 예측식이 아니다. 핵심은 같은 서버 처리 시간이라도 새 연결을 계속 만들면 handshake 비용이 매 요청에 붙는다는 점이다. HTTP Keep-Alive와 connection pool은 서버 코드를 빠르게 만드는 기능이 아니라, 이미 만든 TCP/TLS 연결을 재사용해 반복 비용을 줄이는 기능이다.
반례: 연결 재사용이 항상 좋은 것은 아니다
너무 오래 열린 idle connection은 파일 디스크립터와 메모리를 점유한다. NAT나 방화벽이 중간에서 조용히 끊은 연결을 서버나 클라이언트가 살아 있다고 착각할 수도 있다. 따라서 keep-alive timeout은 “길수록 좋다”가 아니라 “프록시, 서버, 클라이언트의 timeout 순서가 맞고 리소스 한도 안에 있다”가 기준이다.
실무 판단
- 같은 origin으로 짧은 요청을 자주 보낸다 → connection pool과 keep-alive 이점이 크다.
- 요청 간격이 몇 분 이상 벌어진다 → idle timeout과 죽은 연결 감지 비용을 같이 본다.
- ALB나 Nginx가 중간에 있다 → 프록시 timeout과 backend timeout 순서를 맞춘다.
- 모바일/사내망처럼 연결이 자주 바뀐다 → 재사용률보다 reconnect, retry, fallback 경험이 더 중요할 수 있다.
5. Flow Control: 수신자를 보호한다
섹션 제목: “5. Flow Control: 수신자를 보호한다”흐름 제어(Flow Control)는 수신자가 감당할 수 있는 만큼만 보내게 하는 장치다.
TCP 헤더의 receive window, 보통 rwnd라고 부르는 값이 핵심이다.
수신자는 자신의 커널 버퍼 여유 공간을 ACK에 실어 보낸다.
송신자는 rwnd를 넘는 데이터를 outstanding 상태로 둘 수 없다.
송신 버퍼
[ 이미 ACK됨 ][ 보냈지만 ACK 대기 ][ 지금 보낼 수 있음 ][ 아직 못 보냄 ] ^ ^ send base send base + rwndworked example: 수신 버퍼가 작은 경우
수신자가 rwnd = 16KB라고 알렸다고 하자.
송신자는 ACK 없이 최대 16KB까지만 보낼 수 있다.
수신 애플리케이션이 데이터를 빨리 읽으면 커널 버퍼가 비고, 다음 ACK에 rwnd = 32KB처럼 더 큰 값이 실릴 수 있다.
반대로 애플리케이션이 느리게 읽으면 rwnd가 줄어든다.
rwnd = 0이 되면 zero window 상태다.
송신자는 더 보내지 않고, 수신 버퍼가 다시 비었는지 확인하기 위해 작은 probe를 주기적으로 보낸다.
흐름 제어가 깨졌을 때의 모양
| 증상 | 의미 |
|---|---|
| 수신 앱 CPU가 바쁘고 socket receive buffer가 찬다 | 앱이 네트워크보다 느리다 |
| 송신 측 throughput이 낮은데 네트워크 사용률은 낮다 | 수신자 window에 막혔을 수 있다 |
| DB client pool에서 응답을 못 읽고 연결이 쌓인다 | TCP 문제가 아니라 앱/DB 처리 지연이 flow control로 드러날 수 있다 |
오해 방지
Flow Control은 네트워크 전체를 보호하지 않는다. 수신자 하나의 버퍼를 보호한다. 네트워크 중간의 라우터와 링크가 혼잡한지는 Congestion Control이 다룬다.
퀴즈
rwnd가 작아져 전송이 느려졌다면 네트워크가 혼잡하다는 뜻일까?
정답 보기
아니다. rwnd는 수신자 버퍼 여유 공간을 뜻한다. 네트워크 혼잡 여부는 cwnd, 손실, RTT 변화 같은 혼잡 제어 신호와 구분해야 한다.
6. Congestion Control: 네트워크를 보호한다
섹션 제목: “6. Congestion Control: 네트워크를 보호한다”혼잡 제어(Congestion Control)는 송신자가 네트워크를 막히게 만들지 않도록 스스로 속도를 조절하는 장치다.
TCP는 수신자가 많이 받을 수 있다고 해도 네트워크 경로가 감당하지 못하면 속도를 줄여야 한다.
이때 쓰는 핵심 변수가 congestion window, 즉 cwnd다.
실제로 송신자가 ACK 없이 보낼 수 있는 양은 아래처럼 결정된다.
실제 전송 가능량 = min(rwnd, cwnd)rwnd는 수신자 보호 장치이고, cwnd는 네트워크 보호 장치다.
둘 중 더 작은 값이 전송 속도를 제한한다.
6-1. Slow Start: 모르는 길에서는 조심스럽게 늘린다
섹션 제목: “6-1. Slow Start: 모르는 길에서는 조심스럽게 늘린다”새 연결은 네트워크 경로의 용량을 모른다.
처음부터 큰 데이터를 쏟아내면 라우터 큐가 넘치고 손실이 발생할 수 있다.
그래서 TCP는 작은 cwnd에서 시작해 ACK가 돌아오는 속도를 보며 전송량을 늘린다.
RTT 0: cwnd = 1 MSSRTT 1: cwnd = 2 MSSRTT 2: cwnd = 4 MSSRTT 3: cwnd = 8 MSSRTT 4: cwnd = 16 MSS최근 구현은 초기 window를 10 MSS 정도로 두는 경우가 많다. 중요한 원리는 “처음에는 보수적으로 시작하고, ACK가 잘 돌아오면 빠르게 늘린다”는 점이다.
6-2. Congestion Avoidance: 손실 전까지 선형으로 탐색한다
섹션 제목: “6-2. Congestion Avoidance: 손실 전까지 선형으로 탐색한다”cwnd가 임계값인 ssthresh에 도달하면 TCP는 증가 속도를 늦춘다.
이 구간에서는 RTT마다 대략 1 MSS씩 늘리며 네트워크 여유를 천천히 탐색한다.
손실이 발생하면 혼잡 신호로 보고 cwnd를 줄인다.
CWND││ Congestion Avoidance│ ╭────────────────────│ Slow ││ Start ╭╯│ ╭───────╯│ ╭───╯└─────────────────────────────── 시간6-3. CUBIC과 BBR: 혼잡을 무엇으로 판단하는가
섹션 제목: “6-3. CUBIC과 BBR: 혼잡을 무엇으로 판단하는가”Linux에서 흔히 만나는 CUBIC은 손실 기반(loss-based) 혼잡 제어다. 패킷 손실을 네트워크 포화 신호로 보고 window를 줄인다. 단순하고 널리 쓰이지만, 손실이 이미 발생한 뒤에야 줄어드는 경향이 있다.
BBR(Bottleneck Bandwidth and Round-trip propagation time)은 병목 대역폭과 RTT를 추정해 전송률을 조절한다. 핵심은 “패킷을 잃어봐야 줄인다”보다 “경로가 낼 수 있는 처리량과 지연을 계속 측정한다”에 가깝다. 고지연 장거리 경로나 CDN origin 통신에서 유리할 수 있지만, 모든 환경에서 무조건 더 좋은 스위치는 아니다.
첫 회독 기준
- CUBIC과 BBR의 내부 수식은 나중에 봐도 된다.
- 지금은 손실 기반과 대역폭/RTT 기반이라는 판단 신호 차이만 잡는다.
- 운영 설정 변경은 선택 심화다. 본문 이해에는 필요하지 않다.
반례
“TCP는 손실이 생기면 항상 느려진다”는 말은 대체로 맞지만 충분하지 않다.
작은 손실 하나가 대용량 스트림 전체를 느리게 만들 수도 있고, QUIC처럼 독립 스트림을 가진 프로토콜은 한 스트림의 손실 영향을 다른 스트림으로 덜 퍼뜨릴 수도 있다.
또한 손실이 없어도 수신자 rwnd가 작거나 애플리케이션이 느리면 throughput은 낮다.
6-4. BDP: window가 throughput을 제한하는 이유
섹션 제목: “6-4. BDP: window가 throughput을 제한하는 이유”BDP(Bandwidth-Delay Product)는 링크를 꽉 채우려면 네트워크 안에 동시에 떠 있어야 하는 데이터량이다. 식은 단순하다.
첫 회독에서는 BDP를 튜닝 공식으로 외우지 않아도 된다. “RTT가 길고 대역폭이 큰 경로에서는 window가 작으면 링크를 다 쓰지 못한다”는 감각만 잡으면 충분하다. 실제 window scaling, kernel buffer, congestion control 설정은 운영 심화로 돌려도 된다.
BDP = bandwidth * RTT100Mbps 링크와 RTT 50ms를 가진 경로를 생각해 보자.
100Mbps = 12.5MB/s12.5MB/s * 0.05s = 0.625MB이 경로를 꽉 채우려면 대략 625KB 정도의 데이터가 ACK를 기다리며 네트워크 안에 떠 있어야 한다.
그런데 cwnd나 rwnd가 64KB에 묶이면 어떻게 될까?
가능 throughput ≈ window / RTT ≈ 64KB / 0.05s ≈ 1.28MB/s ≈ 10.2Mbps물리 링크는 100Mbps인데 실제 전송은 10Mbps 근처에서 막힐 수 있다. 이때 “서버 CPU가 낮은데 다운로드가 느리다”는 현상이 생긴다. 원인은 애플리케이션 처리량이 아니라 window 크기, RTT, 손실, congestion control의 조합일 수 있다.
왜 이 예시가 중요한가
- 같은 서버라도 가까운 리전에서는 빠르고 먼 리전에서는 느릴 수 있다.
- RTT가 커질수록 같은 window로 낼 수 있는 throughput이 줄어든다.
- 대용량 전송에서는 p50 API latency보다 BDP와 손실률이 더 중요해질 수 있다.
- CDN을 쓰는 이유 중 하나는 RTT를 줄여 같은 window에서도 더 높은 체감 throughput을 내기 위해서다.
오해 방지
BDP를 이해했다고 바로 커널 window 값을 키우라는 뜻은 아니다. 현대 OS는 window scaling과 autotuning을 제공한다. 첫 회독에서는 “RTT가 큰 경로는 작은 window로 링크를 못 채운다”는 감각만 가져가면 충분하다.
6-5. 손실, 지연, 처리량을 따로 읽기
섹션 제목: “6-5. 손실, 지연, 처리량을 따로 읽기”네트워크 성능을 볼 때 latency와 throughput을 섞으면 판단이 흐려진다. 짧은 API 요청은 latency가 중요하고, 큰 파일 전송은 throughput이 중요하다. TCP 재전송은 둘 다에 영향을 주지만 증상은 다르게 보인다.
| 상황 | 사용자가 보는 증상 | TCP 관점 |
|---|---|---|
| 작은 API 요청에서 세그먼트 하나 손실 | 응답이 가끔 100ms 이상 튐 | RTO 또는 Fast Retransmit 지연 |
| 큰 파일 전송에서 1% 손실 | 평균 다운로드 속도 저하 | cwnd 감소와 재전송 반복 |
| RTT가 큰 해외 리전 호출 | 첫 응답과 전체 처리 모두 느림 | handshake와 BDP 비용 증가 |
| 수신 앱이 느림 | 네트워크 사용률 낮고 전송 정체 | rwnd 축소 또는 zero window |
따라서 “느리다”는 말만으로는 부족하다. 첫 바이트가 늦은지, 다운로드가 느린지, p99만 튀는지, 큰 payload에서만 재현되는지를 나눠야 한다.
7. MTU, MSS, PMTUD: 큰 패킷이 항상 좋은 것은 아니다
섹션 제목: “7. MTU, MSS, PMTUD: 큰 패킷이 항상 좋은 것은 아니다”MTU(Maximum Transmission Unit)는 한 링크에서 한 번에 실을 수 있는 최대 frame 크기다. 일반 Ethernet에서는 1500 bytes가 흔하다. MSS(Maximum Segment Size)는 TCP payload 한 조각의 최대 크기다. IPv4에서 IP 헤더 20 bytes와 TCP 헤더 20 bytes를 빼면 흔히 1460 bytes가 된다.
첫 회독에서는 PMTUD(Path MTU Discovery)의 모든 ICMP 동작을 외우기보다, “작은 응답은 되는데 큰 응답만 멈춘다”는 실패 신호를 기억하면 된다. 터널, VPN, 사내 방화벽이 들어오면 실제 경로 MTU가 줄 수 있고, ICMP가 막히면 그 사실을 알리는 신호도 사라질 수 있다.
Ethernet MTU 1500 bytes - IP header 20 bytes - TCP header 20 bytes= TCP MSS 1460 bytesworked example: 10KB HTTP 응답
10KB 응답 body를 TCP로 보낸다고 하자. MSS가 1460 bytes라면 payload는 대략 7개 세그먼트로 나뉜다.
10,240 bytes / 1,460 bytes ≈ 7.0 segments세그먼트가 많을수록 ACK, 재전송, 혼잡 제어의 관측 단위도 많아진다. 큰 응답 하나는 작은 응답보다 패킷 손실과 MTU 문제를 더 잘 드러낸다.
7-1. 터널링과 VPN은 실제 MTU를 줄인다
섹션 제목: “7-1. 터널링과 VPN은 실제 MTU를 줄인다”VPN, IPSec, GRE, VXLAN 같은 터널링은 원래 패킷 바깥에 새 헤더를 덧붙인다. 물리 링크 MTU가 1500이어도 터널 헤더가 50 bytes를 차지하면 내부 payload가 쓸 수 있는 공간은 줄어든다.
기본 Ethernet MTU: 1500터널 오버헤드: 50실제 내부 MTU: 1450TCP MSS 근사: 1410이때 송신자가 여전히 1460 MSS로 보낸다면 중간 장비가 패킷을 나누거나 버려야 한다. DF(Don’t Fragment) bit가 설정되어 있고 ICMP “Fragmentation Needed” 메시지가 차단되면 Path MTU Discovery(PMTUD)가 실패한다.
7-2. PMTUD 실패 신호
섹션 제목: “7-2. PMTUD 실패 신호”| 증상 | 왜 MTU를 의심하는가 |
|---|---|
| 작은 API 응답은 성공하지만 큰 파일 다운로드만 멈춘다 | 작은 세그먼트는 통과하고 큰 세그먼트만 경로 MTU를 넘을 수 있다 |
| VPN 안에서만 특정 크기 이상 요청이 실패한다 | 터널 헤더 때문에 실제 MTU가 줄었을 수 있다 |
| TLS handshake나 HTTP/2 큰 header에서만 끊긴다 | 초기 작은 패킷은 통과하지만 큰 record가 막힐 수 있다 |
| 재시도하면 가끔 성공한다 | 경로나 MSS 협상이 일관되지 않을 수 있다 |
오해 방지
MTU 문제는 “서버가 느리다”와 다르다.
서버 처리 시간 로그는 짧은데 클라이언트가 큰 응답을 못 받는다면, L7 코드보다 L3/L4 경로를 봐야 한다.
처음 읽을 때는 ip link, iptables, tcpdump 명령을 외우기보다 “큰 패킷만 실패하면 MTU 후보”라는 실패 신호를 잡는다.
7-3. MSS Clamping은 왜 SYN에서 보이는가
섹션 제목: “7-3. MSS Clamping은 왜 SYN에서 보이는가”MSS는 TCP 연결 시작 시 SYN option에 실린다. 양쪽은 “내가 한 번에 받을 수 있는 TCP payload 최대 크기”를 handshake 단계에서 알린다. 중간 장비가 터널 경로의 실제 MTU를 알고 있다면 SYN의 MSS 값을 더 작게 고쳐서 큰 세그먼트가 생기지 않게 만들 수 있다. 이를 MSS clamping이라고 부른다.
개념 예시
클라이언트 원래 MSS: 1460VPN 경로 실제 가능 MSS: 1360중간 장비가 SYN option을 1360으로 낮춤서버는 이후 payload를 1360 이하로 분할이 방식은 큰 패킷을 보낸 뒤 실패를 겪는 대신, 처음부터 안전한 크기로 보내게 만든다. 다만 이것도 운영 설정 영역이다. 첫 회독에서는 “MSS는 handshake 때 협상되고, 터널 환경에서는 낮아질 수 있다”까지만 잡으면 된다.
반례
UDP에는 TCP SYN과 MSS option이 없다. UDP 애플리케이션이 큰 datagram을 보내면 IP fragmentation이나 애플리케이션 레벨 분할을 직접 고려해야 한다. 그래서 UDP 기반 프로토콜은 경로 MTU를 더 조심스럽게 다루거나, 애초에 작은 packet size를 선택하는 경우가 많다.
8. 연결 종료: TIME_WAIT과 CLOSE_WAIT을 구분한다
섹션 제목: “8. 연결 종료: TIME_WAIT과 CLOSE_WAIT을 구분한다”TCP 연결은 양방향이므로 닫을 때도 양쪽의 종료 신호가 필요하다.
한쪽이 FIN을 보내면 “나는 더 보낼 데이터가 없다”는 뜻이다.
상대방은 ACK로 받았다고 답하고, 자신도 보낼 데이터가 끝나면 FIN을 보낸다.
Active close side Passive close side──────────────── ──────────────────FIN_WAIT_1 ───── FIN ─────────> CLOSE_WAITFIN_WAIT_2 <──── ACK ───────── <──── FIN ───────── LAST_ACKTIME_WAIT ───── ACK ─────────> CLOSED2MSL 대기CLOSED8-1. TIME_WAIT은 대부분 정상 안전장치다
섹션 제목: “8-1. TIME_WAIT은 대부분 정상 안전장치다”TIME_WAIT은 먼저 닫은 쪽이 마지막 ACK 이후 잠시 기다리는 상태다. 두 가지 이유가 있다.
- 마지막 ACK가 유실됐을 때 상대가 FIN을 재전송하면 다시 ACK하기 위해서다.
- 이전 연결의 오래된 세그먼트가 새 연결에 섞이지 않도록 기다리기 위해서다.
worked example: 마지막 ACK 유실
서버 -> FIN클라이언트 -> ACK # 이 ACK가 중간에서 유실서버 -> FIN 재전송 # 서버는 ACK를 못 받았으므로 다시 보냄클라이언트 -> ACK # TIME_WAIT에 남아 있으므로 다시 답할 수 있음클라이언트가 마지막 ACK 직후 바로 CLOSED로 사라졌다면 재전송된 FIN에 응답하지 못한다. TIME_WAIT은 지저분한 네트워크 현실에서 종료를 안전하게 만드는 대기실이다.
8-2. CLOSE_WAIT은 애플리케이션이 닫지 않은 신호다
섹션 제목: “8-2. CLOSE_WAIT은 애플리케이션이 닫지 않은 신호다”CLOSE_WAIT은 상대가 FIN을 보냈고, 내 커널은 그 사실을 애플리케이션에 알렸지만, 내 애플리케이션이 아직 close()를 호출하지 않은 상태다.
잠깐 보일 수는 있지만 계속 쌓이면 소켓 누수 가능성이 높다.
| 상태 | 누가 먼저 닫았는가 | 정상성 |
|---|---|---|
| TIME_WAIT | 내가 먼저 닫음 | 대개 정상. 시간이 지나면 사라짐 |
| CLOSE_WAIT | 상대가 먼저 닫음 | 계속 쌓이면 앱이 close를 안 한 것 |
| FIN_WAIT_2 | 내가 FIN을 보냈고 상대 FIN을 기다림 | 오래 지속되면 상대 종료 지연 |
| LAST_ACK | 내가 FIN을 보냈고 마지막 ACK 대기 | 오래 지속되면 ACK 경로 문제 가능 |
퀴즈
TIME_WAIT 수천 개와 CLOSE_WAIT 수천 개 중 코드 버그를 먼저 의심해야 하는 쪽은?
정답 보기
CLOSE_WAIT이다. 상대가 FIN을 보낸 뒤 내 애플리케이션이 close()를 호출하지 않아 소켓이 남아 있을 가능성이 높다. TIME_WAIT은 높은 트래픽 서버에서 정상적으로 많이 보일 수 있다.
8-3. TIME_WAIT 폭증의 진짜 질문
섹션 제목: “8-3. TIME_WAIT 폭증의 진짜 질문”TIME_WAIT이 많다는 사실만으로 장애는 아니다. 질문은 “왜 이렇게 많은 짧은 연결을 만들고 닫는가”다.
흔한 원인은 다음과 같다.
- HTTP keep-alive가 꺼져 매 요청마다 새 TCP 연결을 만든다.
- 외부 API client가 connection pool을 쓰지 않는다.
- 프록시나 서버가
Connection: close를 강제한다. - 서버가 먼저 닫는 구조라 서버 쪽에 TIME_WAIT이 집중된다.
- 배포나 health check가 짧은 연결을 대량으로 만든다.
근본 해결은 보통 커널 값을 먼저 바꾸는 것이 아니라 연결 재사용을 늘리는 것이다. 커널 튜닝은 선택 심화이며, 잘못 적용하면 NAT, old segment, 보안 장비와 충돌할 수 있다.
9. UDP: 빠른 이유와 지는 책임
섹션 제목: “9. UDP: 빠른 이유와 지는 책임”UDP는 연결을 만들지 않는다. 3-way handshake도 없고, 순서 보장도 없고, 재전송도 없다. 헤더는 8 bytes로 작다.
UDP header┌───────────┬───────────┬───────────┬───────────┐│ Src Port │ Dst Port │ Length │ Checksum ││ 2 bytes │ 2 bytes │ 2 bytes │ 2 bytes │└───────────┴───────────┴───────────┴───────────┘이 단순함 때문에 UDP는 첫 패킷부터 데이터를 보낼 수 있다. DNS 질의처럼 작은 요청과 작은 응답이 오가는 경우에는 연결 수립 비용을 없애는 이점이 크다. 실시간 미디어처럼 늦게 도착한 옛 데이터가 쓸모없는 경우에도 TCP의 재전송이 오히려 방해가 된다.
9-1. UDP가 빠르다는 말의 정확한 의미
섹션 제목: “9-1. UDP가 빠르다는 말의 정확한 의미”“UDP가 TCP보다 빠르다”는 말은 세 가지로 나눠야 한다.
| 의미 | UDP가 유리한 이유 | 반례 |
|---|---|---|
| 첫 요청 지연 | handshake 없이 바로 전송 | 애플리케이션이 자체 handshake를 만들면 이점 감소 |
| 헤더 오버헤드 | 8 bytes 고정 헤더 | payload가 크면 헤더 차이는 작아짐 |
| 손실 처리 | 늦은 데이터를 버리고 최신 데이터 우선 가능 | 파일 전송처럼 모든 바이트가 필요하면 앱이 재전송을 다시 구현해야 함 |
UDP는 마법처럼 네트워크 대역폭을 늘리지 않는다. 신뢰성을 빼서 지연을 줄이는 선택이다. 애플리케이션이 결국 순서, 재전송, 혼잡 제어를 다시 구현해야 한다면 TCP보다 복잡해질 수 있다.
9-2. UDP 사용 사례의 공통점
섹션 제목: “9-2. UDP 사용 사례의 공통점”| 사용 사례 | 왜 UDP인가 |
|---|---|
| DNS | 질의/응답이 작고 짧다. 손실되면 클라이언트가 다시 질의할 수 있다 |
| 실시간 음성/영상 | 늦은 프레임보다 최신 프레임이 중요하다 |
| 온라인 게임 | 위치 업데이트는 오래된 값을 재전송하는 것보다 최신 값을 보내는 편이 낫다 |
| DHCP | 네트워크 설정 전 단계라 연결 지향 모델이 맞지 않는다 |
| QUIC/HTTP/3 | UDP 위에서 독립 스트림과 자체 신뢰성 모델을 구현한다 |
반례: 결제 요청을 UDP로 보내면 안 되는 이유
결제 요청은 “언젠가 한 번 도착하면 된다”가 아니다. 중복 처리 방지, 순서, 실패 확인, 재시도, 감사 로그가 필요하다. UDP를 쓰면 이 모든 것을 애플리케이션이 직접 설계해야 한다. 대부분의 백엔드 API가 TCP 기반 HTTP를 쓰는 이유가 여기에 있다.
9-3. TCP stream과 UDP datagram의 메시지 경계
섹션 제목: “9-3. TCP stream과 UDP datagram의 메시지 경계”TCP는 stream이라 애플리케이션 메시지 경계를 보존하지 않는다. UDP는 datagram이라 한 번 보낸 단위가 수신에서도 하나의 datagram으로 보인다. 이 차이는 직접 프로토콜을 만들 때 매우 중요하다.
TCP에서 깨지는 직관
송신 코드write("A")write("B")
수신 결과 가능성read() -> "AB"read() -> "A", 다음 read() -> "B"read() -> "A"의 일부, 다음 read() -> 나머지TCP는 순서를 보장하지만 write() 호출 단위를 보장하지 않는다.
그래서 TCP 위의 애플리케이션 프로토콜은 메시지 길이를 앞에 붙이거나 delimiter를 둔다.
HTTP, Redis RESP, PostgreSQL wire protocol이 모두 자체 framing을 갖는 이유다.
UDP에서 생기는 다른 문제
sendto("A")sendto("B")
수신 결과 가능성recvfrom() -> "A"recvfrom() -> "B"또는 B가 먼저 오거나, A가 사라질 수 있음UDP는 datagram 단위를 보존하지만 순서와 도착을 보장하지 않는다. 즉 TCP는 “경계는 없지만 순서는 있다”에 가깝고, UDP는 “경계는 있지만 순서와 도착은 없다”에 가깝다.
선택 기준
- 긴 바이트 스트림을 정확히 전달해야 한다 → TCP가 자연스럽다.
- 작은 메시지 하나하나가 독립이고, 일부 손실을 허용한다 → UDP가 자연스럽다.
- UDP를 쓰되 순서와 재전송이 필요하다 → QUIC처럼 별도 전송 계층을 설계해야 한다.
10. QUIC과 HTTP/3: UDP 위에 새 전송 계층을 만든다
섹션 제목: “10. QUIC과 HTTP/3: UDP 위에 새 전송 계층을 만든다”HTTP/2는 하나의 TCP 연결 안에서 여러 HTTP stream을 multiplexing한다. 하지만 TCP 자체는 바이트 스트림 하나다. TCP 레벨에서 패킷 하나가 손실되면, 그 뒤 바이트를 애플리케이션에 순서대로 넘길 수 없어 모든 HTTP/2 stream이 같이 기다릴 수 있다. 이를 TCP head-of-line blocking이라고 부른다.
첫 회독에서는 QUIC의 패킷 포맷보다 경계만 잡는다. QUIC은 UDP 위에 새 전송 계층을 얹어 손실 복구, 혼잡 제어, TLS 1.3, stream 관리를 직접 수행한다. 그래서 “UDP라서 신뢰성이 없다”가 아니라 “UDP 위에서 애플리케이션/프로토콜이 어떤 신뢰성을 다시 만들었는가”가 핵심 질문이다.
HTTP/2 over TCP
Stream A: A1 A2(손실) A3Stream B: B1 B2 B3 ^ TCP는 빠진 바이트를 기다리므로 B도 지연될 수 있음QUIC은 UDP 위에서 동작하면서 여러 stream을 QUIC 내부에서 독립적으로 다룬다. 한 stream의 손실이 다른 stream의 전달을 덜 막도록 설계되어 있다.
HTTP/3 over QUIC
Stream A: A1 A2(손실) A3 -> A stream만 복구 대기Stream B: B1 B2 B3 -> B stream은 계속 진행 가능QUIC의 핵심 특징은 다음과 같다.
- UDP 위에서 동작한다.
- TLS 1.3을 기본으로 포함한다.
- 연결을 IP:Port 4-tuple이 아니라 connection ID로 추적할 수 있다.
- 네트워크가 Wi-Fi에서 LTE로 바뀌어도 연결 migration이 가능하다.
- 손실 복구와 혼잡 제어를 QUIC 계층이 직접 구현한다.
10-1. QUIC의 경계
섹션 제목: “10-1. QUIC의 경계”QUIC은 UDP의 단점을 없애는 단순한 스위치가 아니다. 기업 네트워크나 방화벽이 UDP 443을 막으면 클라이언트는 보통 HTTP/2로 fallback한다. 서비스가 끊기지 않을 수는 있지만, QUIC의 지연 개선과 stream 독립성은 얻지 못한다.
또한 백엔드 애플리케이션 서버가 직접 HTTP/3를 처리하지 않는 경우가 많다.
CloudFront나 CDN/edge가 HTTP/3를 종단하고, origin과는 HTTP/1.1 또는 HTTP/2로 통신하는 구조도 흔하다.
따라서 “우리 NestJS가 HTTP/3를 직접 켜야 하나”보다 “클라이언트와 edge 사이에서 HTTP/3 이점이 필요한가”를 먼저 묻는 편이 실무적으로 안전하다.
브라우저가 HTTP/3를 시도하는 경로에는 Alt-Svc 같은 힌트도 관여한다. UDP 443이 막혀 있으면 이 힌트가 있어도 최종적으로 HTTP/2로 돌아갈 수 있으므로, 도입 효과는 “설정했다”가 아니라 실제 협상 비율과 fallback 비율로 봐야 한다.
11. Keepalive: 같은 이름의 두 계층을 구분한다
섹션 제목: “11. Keepalive: 같은 이름의 두 계층을 구분한다”TCP Keepalive와 HTTP Keep-Alive는 이름이 비슷하지만 다른 계층의 기능이다.
| 항목 | TCP Keepalive | HTTP Keep-Alive |
|---|---|---|
| 계층 | L4 Transport | L7 Application |
| 목적 | 유휴 TCP 연결이 아직 살아 있는지 probe | TCP 연결을 여러 HTTP 요청에 재사용 |
| 주체 | OS socket 옵션과 커널 | HTTP client/server, 프록시 |
| 흔한 실패 | 죽은 연결을 오래 붙잡음 | 프록시와 서버 timeout 불일치로 reset/502 |
| 첫 회독 기준 | 이름과 목적만 이해 | ALB 502 원리까지 이해 |
11-1. HTTP Keep-Alive가 줄이는 비용
섹션 제목: “11-1. HTTP Keep-Alive가 줄이는 비용”한 요청마다 새 TCP 연결을 만들면 최소한 TCP handshake와 TLS handshake 비용이 들어간다. 같은 origin에 여러 요청을 보낼 때 HTTP Keep-Alive로 연결을 재사용하면 이 비용을 줄인다.
수치 예시
| 단계 | 새 연결 | 기존 연결 재사용 |
|---|---|---|
| DNS cache hit | 0ms | 0ms |
| TCP handshake | 1 RTT | 0ms |
| TLS handshake | 1 RTT 이상 | 0ms 또는 재개 |
| 서버 처리 | 20ms | 20ms |
RTT가 30ms라면 새 연결은 handshake만으로 60ms 이상이 추가될 수 있다. 반면 기존 연결을 재사용하면 같은 서버 처리 시간에서도 사용자 체감 지연이 줄어든다.
11-2. ALB 502: timeout 순서가 뒤집히면 닫힌 소켓에 쓴다
섹션 제목: “11-2. ALB 502: timeout 순서가 뒤집히면 닫힌 소켓에 쓴다”AWS ALB의 기본 idle timeout은 흔히 60초로 이해한다. Node.js HTTP server의 keepAliveTimeout 기본값은 더 짧을 수 있다. 서버가 먼저 연결을 닫았는데 ALB가 아직 그 연결을 재사용 가능하다고 생각하면, ALB가 닫힌 socket에 요청을 보내다가 reset을 만나 502를 반환할 수 있다.
시간 흐름
0s ALB -> Node.js TCP 연결 생성5s Node.js가 유휴 연결을 닫음55s ALB는 연결을 재사용하려고 요청 전송55s 이미 닫힌 socket이므로 reset/오류결과 ALB 502, target_status_code = -해결 방향은 단순하다. 백엔드 서버의 keep-alive timeout을 ALB idle timeout보다 길게 두고, headers timeout은 keep-alive timeout보다 길게 둔다.
const server = app.getHttpServer();
server.keepAliveTimeout = 65000; // ALB idle timeout 60s보다 길게server.headersTimeout = 66000; // keepAliveTimeout보다 길게이 코드는 원리를 보여주는 최소 예시다. 실제 운영에서는 프레임워크 버전, Node.js 버전, ALB 설정값, 배포 환경의 proxy chain을 함께 확인한다.
12. Nagle, Delayed ACK, 작은 패킷 지연
섹션 제목: “12. Nagle, Delayed ACK, 작은 패킷 지연”Nagle 알고리즘은 작은 패킷(tinygram)이 네트워크에 너무 많이 흐르지 않게, ACK가 오기 전에는 작은 write를 모아 보내는 송신 측 최적화다.
Delayed ACK는 수신 측이 ACK만 담긴 작은 패킷을 줄이기 위해 잠깐 기다리는 최적화다.
둘 다 효율을 위한 장치지만, 함께 만나면 작은 요청/응답에서 지연이 튈 수 있다.
첫 회독에서는 세부 알고리즘보다 “작은 메시지에서 40ms 안팎의 지연이 반복될 수 있고, TCP_NODELAY는 지연과 패킷 수의 trade-off를 바꾸는 선택지”라는 정도만 잡으면 된다.
클라이언트 서버write(header 100B) ───────────> 수신, ACK 지연 시작write(body 50B) -- Nagle 보류 Delayed ACK도 잠깐 기다림~40ms 대기ACK <───────────body 50B ───────────>언제 의심하는가
- payload가 작은 요청에서만 p95/p99가 40ms 단위로 튄다.
- 대용량 전송은 정상인데 작은 메시지를 여러 번
write()하는 프로토콜만 느리다. - WebSocket, 게임 서버, RPC처럼 작은 메시지가 많은 경로다.
판단 기준
HTTP API 서버나 실시간 소켓 서버에서는 보통 TCP_NODELAY를 켜 Nagle을 비활성화하는 편이 안전하다.
반대로 저대역폭 환경에서 작은 패킷 폭증을 줄이는 것이 더 중요하면 Nagle이 도움이 될 수 있다.
중요한 것은 “무조건 끄기”보다 작은 메시지 지연과 패킷 수 사이의 trade-off를 이해하는 것이다.
그리고 직접 socket 옵션을 바꾸기 전에는 런타임과 프레임워크의 기본값을 먼저 확인한다. Node.js, HTTP 라이브러리, gRPC 런타임, 프록시가 이미 작은 메시지 지연을 줄이는 설정을 갖고 있을 수 있다.
13. ALB와 NLB: L7을 볼 것인가, L4를 그대로 넘길 것인가
섹션 제목: “13. ALB와 NLB: L7을 볼 것인가, L4를 그대로 넘길 것인가”ALB(Application Load Balancer)는 L7 로드밸런서다. HTTP 요청의 path, host, header, cookie, gRPC 같은 응용 계층 정보를 보고 라우팅할 수 있다. NLB(Network Load Balancer)는 L4 로드밸런서다. TCP, UDP, TLS 연결을 더 낮은 계층에서 전달하며, 정적 IP와 낮은 지연이 필요할 때 적합하다.
첫 회독에서는 AWS 세부 설정보다 질문을 구분하는 것이 먼저다. HTTP 헤더와 path를 보고 라우팅해야 하면 ALB 쪽으로, TCP/UDP 연결 자체를 낮은 지연으로 넘기고 싶으면 NLB 쪽으로 생각한다. listener, target group, health check 세부 튜닝은 나중에 운영 문서로 돌아와도 된다.
| 기준 | ALB | NLB |
|---|---|---|
| 계층 | L7 Application | L4 Transport |
| 주 대상 | HTTP, HTTPS, gRPC | TCP, UDP, TLS |
| 라우팅 | host/path/header 기반 | IP/port 기반 |
| UDP | 지원하지 않음 | 지원 |
| 정적 IP | 기본적으로 부적합 | 적합 |
| WebSocket | 지원 | 지원 |
| 관찰 신호 | HTTP status, target status, access log | connection, flow, target health |
13-1. 선택 기준
섹션 제목: “13-1. 선택 기준”flowchart TD
A["외부 트래픽 진입점을 고른다"] --> B{"HTTP path/header 라우팅이 필요한가?"}
B -->|예| C["ALB - L7 라우팅과 HTTP 관측"]
B -->|아니오| D{"UDP 또는 정적 IP가 필요한가?"}
D -->|예| E["NLB - L4 TCP/UDP 전달"]
D -->|아니오| F{"gRPC/WebSocket처럼 장기 연결인가?"}
F -->|HTTP/gRPC 관측이 중요| C
F -->|초저지연/패스스루가 중요| E ALB를 기본값으로 두기 좋은 경우
- 일반적인 HTTP API 서버다.
- path 기반 라우팅과 host 기반 라우팅이 필요하다.
- ECS/EKS/Lambda 연동과 health check를 관리형으로 쓰고 싶다.
- HTTP status와 target response time으로 관측하고 싶다.
- 팀이 L4 로드밸런서와 TLS passthrough를 직접 운영할 이유가 없다.
NLB를 선택해야 하는 경우
- UDP 프로토콜을 받아야 한다.
- 정적 IP가 필요하다.
- TLS를 백엔드에서 직접 처리해야 한다.
- L7 파싱 오버헤드보다 낮은 지연이 중요하다.
- HTTP가 아닌 TCP 프로토콜을 그대로 전달해야 한다.
반례
“WebSocket이면 무조건 NLB”는 아니다. ALB도 WebSocket을 지원한다. WebSocket이지만 HTTP Upgrade, 인증, host/path 라우팅, ALB access log가 중요하면 ALB가 더 단순하다. 반대로 UDP 기반 게임 서버나 커스텀 TCP 프로토콜이라면 ALB가 애초에 맞지 않는다.
14. TCP/UDP 선택 기준
섹션 제목: “14. TCP/UDP 선택 기준”TCP와 UDP 선택은 “빠른가 느린가” 하나로 끝나지 않는다. 실무에서는 신뢰성 책임을 어느 계층에 둘 것인지 결정하는 문제다.
| 질문 | TCP 쪽으로 기우는 답 | UDP 쪽으로 기우는 답 |
|---|---|---|
| 모든 바이트가 반드시 필요한가 | 예. 파일, 결제, DB query | 아니오. 최신 상태가 더 중요 |
| 순서가 중요한가 | 예. HTTP response body, stream parsing | 일부 메시지는 건너뛰어도 됨 |
| 재전송 정책을 직접 만들 수 있는가 | 굳이 만들 필요 없음 | 앱이 직접 설계할 이유가 있음 |
| 첫 패킷 지연이 중요한가 | 연결 재사용으로 줄일 수 있음 | handshake 자체를 피하고 싶음 |
| 방화벽/NAT 통과성이 중요한가 | TCP 443이 가장 안전한 편 | UDP 차단 환경을 감수해야 함 |
| 관측과 운영 도구가 익숙한가 | HTTP/TCP 생태계가 풍부 | 앱별 메트릭을 더 잘 만들어야 함 |
선택 예시
- 일반 REST API: TCP/HTTP가 기본이다. 요청과 응답의 완전성이 중요하다.
- WebSocket 채팅: TCP 위에서 장기 연결을 쓴다. 메시지 순서와 연결 상태가 중요하다.
- DNS lookup: UDP가 기본이다. 작고 짧으며 실패 시 재질의가 가능하다.
- 실시간 위치 업데이트: UDP가 적합할 수 있다. 옛 위치를 재전송하는 것보다 최신 위치가 중요하다.
- HTTP/3: UDP를 쓰지만 QUIC이 자체 신뢰성을 제공한다. “UDP를 직접 쓰는 앱”과 다르게 봐야 한다.
14-1. 선택 드릴 1: 채팅 읽음 상태 API
섹션 제목: “14-1. 선택 드릴 1: 채팅 읽음 상태 API”요구사항:
- 사용자가 메시지를 읽으면 서버에 읽음 상태를 기록해야 한다.
- 중복 호출이 생겨도 최종 상태는 같아야 한다.
- 실패하면 재시도해야 한다.
판단:
- 일반 HTTP/TCP가 적합하다.
- UDP로 보내면 손실, 중복, 재전송, 순서 문제를 직접 해결해야 한다.
- 성능 병목은 전송 프로토콜보다 idempotency key, DB write path, batch 처리에서 볼 가능성이 높다.
학습 포인트:
TCP는 결제나 상태 변경 같은 “정확히 반영되어야 하는 요청”의 기본값이다. UDP를 쓰려면 전송 계층이 빼놓은 보장을 애플리케이션이 다시 만들어야 한다.
14-2. 선택 드릴 2: 실시간 커서 위치 공유
섹션 제목: “14-2. 선택 드릴 2: 실시간 커서 위치 공유”요구사항:
- 협업 문서에서 다른 사용자의 커서 위치를 거의 실시간으로 보여준다.
- 1초 전 위치는 최신 위치가 오면 의미가 없다.
- 일부 업데이트가 손실되어도 다음 업데이트로 덮어쓸 수 있다.
판단:
- WebSocket/TCP로도 충분히 구현 가능하다.
- 손실 허용과 초저지연이 매우 중요하고 자체 프로토콜을 운영할 수 있다면 UDP 계열도 후보가 된다.
- 브라우저 환경에서는 직접 UDP를 쓰기 어렵기 때문에 WebRTC data channel이나 WebSocket 같은 현실 제약도 함께 본다.
학습 포인트:
프로토콜 선택은 이론적 성능만으로 하지 않는다. 클라이언트 플랫폼, 방화벽 통과성, 운영 도구, fallback 전략까지 포함해야 한다.
14-3. 선택 드릴 3: 사내 DNS-like service
섹션 제목: “14-3. 선택 드릴 3: 사내 DNS-like service”요구사항:
- 질의는 작고 응답도 작다.
- 실패하면 클라이언트가 빠르게 다시 물어볼 수 있다.
- p99 지연이 중요하다.
판단:
- UDP가 자연스럽다.
- 단, 응답이 커지거나 zone transfer처럼 큰 데이터가 필요하면 TCP fallback 또는 별도 경로가 필요하다.
- UDP는 혼잡 제어가 없으므로 대량 질의가 생기면 rate limit과 backoff를 애플리케이션/인프라에서 둬야 한다.
학습 포인트:
UDP가 적합한 문제는 대체로 “작고 독립적인 메시지, 손실 시 단순 재질의, 낮은 지연”이라는 공통점을 가진다.
15. 실패 신호로 읽는 TCP/UDP
섹션 제목: “15. 실패 신호로 읽는 TCP/UDP”이 절은 긴 런북이 아니라 증상과 원리를 연결하는 지도다. 명령어는 선택 부록에 짧게 모았다.
15-1. Connection refused
섹션 제목: “15-1. Connection refused”Connection refused는 보통 SYN에 대해 즉시 RST가 돌아온 상황이다.
상대 host까지는 도달했지만 해당 port를 듣는 프로세스가 없거나, 중간 장비가 명시적으로 거절했을 가능성이 높다.
먼저 떠올릴 질문은 다음과 같다.
- 서버 프로세스가 떠 있는가?
- 해당 port에서 listen 중인가?
- 컨테이너 포트와 service/target group 포트가 맞는가?
- 로드밸런서 target이 healthy인가?
timeout보다 범위가 좁다.
거절 응답이 돌아왔다는 것은 적어도 어떤 장비가 명시적으로 답했다는 뜻이기 때문이다.
15-2. Connection reset by peer
섹션 제목: “15-2. Connection reset by peer”Connection reset by peer는 상대가 RST를 보내 연결을 강제로 끊었다는 뜻이다.
이미 연결됐던 socket이 더 이상 유효하지 않거나, 애플리케이션/프록시가 해당 연결을 사용할 수 없다고 판단한 경우에 나타난다.
흔한 원인은 다음과 같다.
- 서버 프로세스가 재시작되어 기존 연결이 사라졌다.
- ALB가 재사용하려던 연결을 Node.js 서버가 먼저 닫았다.
- 백엔드가 요청을 처리하다가 예외로 socket을 닫았다.
- 방화벽이나 프록시가 정책상 연결을 reset했다.
핵심은 reset이 “느린 응답”이 아니라 “강제 종료”라는 점이다. 서버 처리 시간만 볼 것이 아니라 연결 재사용과 timeout 순서를 같이 봐야 한다.
15-3. Timeout
섹션 제목: “15-3. Timeout”timeout은 정해진 시간 안에 기대한 응답이 오지 않았다는 뜻이다.
refused나 reset보다 더 넓은 신호다.
요청이 서버에 도착하지 못했을 수도 있고, 서버는 처리 중인데 응답이 늦을 수도 있고, 응답 경로가 막혔을 수도 있다.
먼저 나눌 기준은 다음과 같다.
| 관측 | 의심 범위 |
|---|---|
| TCP 연결 자체가 timeout | 라우팅, Security Group, NACL, 방화벽, 서버 listen 여부 |
| 연결은 됐지만 HTTP 응답 timeout | 앱 처리 지연, DB 지연, upstream timeout |
| 작은 응답은 되는데 큰 응답만 timeout | MTU/MSS, PMTUD, 큰 payload 처리 |
| 특정 네트워크에서만 timeout | NAT, VPN, 사내 방화벽, UDP/TCP 정책 |
AWS VPC에서는 Security Group과 NACL을 구분해야 한다. Security Group은 stateful이라 허용된 요청의 응답 트래픽을 별도 outbound rule 없이도 돌려보낼 수 있지만, NACL은 stateless라 양방향 규칙이 맞아야 한다. 특히 클라이언트의 ephemeral port 범위로 돌아가는 outbound/return path가 막히면 앱 로그에는 흔적이 없고, 클라이언트에는 timeout처럼 보일 수 있다.
15-4. 502 Bad Gateway
섹션 제목: “15-4. 502 Bad Gateway”502는 L7 프록시가 “타겟에게서 정상 응답을 받지 못했다”고 말하는 상태 코드다. 하지만 원인은 L4와 L7에 걸쳐 있다.
| 패턴 | 의미 |
|---|---|
target_status_code = - | 타겟 HTTP 응답을 받기 전에 연결 문제가 났을 수 있음 |
| 배포 직후 502 | 앱 기동 전, health check, 포트 mismatch |
| 트래픽 적을 때 간헐적 502 | keep-alive timeout mismatch 가능 |
| 특정 큰 header에서 502 | 프록시 buffer/header 제한 가능 |
| upstream connect failed | 프로세스 down, port 미리스닝, listener 설정 오류 |
이 문서에서 특히 중요한 패턴은 keep-alive timeout mismatch다. ALB는 연결을 살아 있다고 보고 재사용했는데 backend가 이미 닫았다면 reset이 발생하고 502로 보일 수 있다.
15-5. TIME_WAIT 증가
섹션 제목: “15-5. TIME_WAIT 증가”TIME_WAIT 증가는 “짧은 연결이 많다”는 신호다. 정상 트래픽이 많아도 늘 수 있다. 문제는 TIME_WAIT 자체가 아니라 포트 고갈, 파일 디스크립터 고갈, 새 연결 실패로 이어지는지다.
확인 질문은 다음 순서가 좋다.
- HTTP keep-alive가 켜져 있는가?
- 외부 API client가 connection pool을 쓰는가?
- 서버가 먼저 닫는 구조인가?
- 배포/health check가 과도하게 짧은 연결을 만들고 있는가?
- 실제로 새 연결 실패나 latency 증가가 동반되는가?
15-6. CLOSE_WAIT 증가
섹션 제목: “15-6. CLOSE_WAIT 증가”CLOSE_WAIT은 “상대가 닫았는데 내 앱이 아직 닫지 않았다”는 신호다. 계속 증가하면 애플리케이션 소켓 누수를 의심한다.
흔한 코드 경로는 다음과 같다.
- HTTP response stream을 끝까지 읽지 않았다.
- DB client connection이 예외 경로에서 pool로 반환되지 않았다.
- WebSocket disconnect 이벤트에서 정리 로직이 누락됐다.
- 장시간 요청이 취소됐는데 finally 블록에서 자원을 닫지 않았다.
TIME_WAIT과 달리 시간이 지나도 자동으로 사라지지 않는다면 코드 수정이 필요하다.
15-7. 큰 응답만 실패
섹션 제목: “15-7. 큰 응답만 실패”작은 요청은 성공하지만 큰 다운로드, 큰 JSON, 큰 TLS record에서만 실패하면 MTU/MSS 문제를 후보에 둔다. 특히 VPN, 사내망, VPC peering, 터널링 경로에서 자주 드러난다.
이때 중요한 질문은 “서버가 응답을 만들었는가”와 “응답 패킷이 클라이언트까지 도착했는가”를 분리하는 것이다.
애플리케이션 로그에 200 OK generated in 20ms가 있어도 클라이언트가 큰 응답을 받지 못하면 네트워크 경로 문제일 수 있다.
15-8. UDP/QUIC fallback
섹션 제목: “15-8. UDP/QUIC fallback”HTTP/3를 켰는데 일부 사용자가 계속 HTTP/2로 보인다면 장애가 아닐 수 있다. 클라이언트 네트워크가 UDP 443을 막거나 중간 장비가 QUIC을 허용하지 않으면 브라우저가 TCP 기반 HTTP/2로 fallback한다.
사용자 관점에서는 서비스가 동작하지만, HTTP/3의 지연 이점은 사라진다.
따라서 QUIC 도입 효과를 볼 때는 전체 평균만 보지 말고 실제 HTTP/3 협상 비율과 fallback 비율을 같이 봐야 한다.
Alt-Svc가 내려가는데도 협상 비율이 낮으면, 클라이언트 지원 여부와 UDP 443 차단을 먼저 의심한다. 반대로 edge에서는 HTTP/3로 받았지만 origin은 HTTP/2로 통신할 수 있으므로, 어느 구간의 프로토콜을 보고 있는지도 분리해야 한다.
15-9. 증상 조합으로 후보 좁히기
섹션 제목: “15-9. 증상 조합으로 후보 좁히기”실제 장애는 신호 하나만 오지 않는다. 대개 여러 관측이 함께 온다. 아래 표는 첫 가설을 세우는 데 쓰는 학습용 매트릭스다.
| 관측 조합 | 가장 먼저 세울 가설 | 왜 그런가 |
|---|---|---|
Connection refused + 서버 로그 없음 | 포트 미리스닝 또는 target mismatch | 요청이 앱까지 오기 전에 거절됐다 |
timeout + VPC Flow Logs REJECT | NACL/Security Group/routing | 응답 경로가 차단됐을 가능성이 높다 |
ALB 502 + target_status_code=- | backend 연결 재사용/Reset/port 문제 | HTTP 응답 전 연결 단계에서 실패했다 |
| 앱 처리 시간 20ms + 클라이언트 2s | 네트워크 전송/재전송/다운로드 지연 | 서버는 빨리 만들었지만 전달이 느리다 |
| 큰 응답만 실패 + VPN 경유 | MTU/PMTUD | 작은 packet은 통과하고 큰 packet만 막힌다 |
| CLOSE_WAIT 증가 + open files 증가 | 앱 소켓 누수 | 상대 종료 후 내 앱이 닫지 않는다 |
| TIME_WAIT 증가 + RPS 증가 | short-lived connection 증가 | 많은 연결 생성/종료가 정상적으로 쌓인 것일 수 있다 |
| HTTP/3 enable 후 일부만 h3 | UDP 차단 또는 fallback | QUIC 협상이 네트워크별로 다를 수 있다 |
15-10. 먼저 바꾸지 말아야 할 것
섹션 제목: “15-10. 먼저 바꾸지 말아야 할 것”장애를 보면 커널 파라미터나 로드밸런서 설정을 바로 바꾸고 싶어진다. 하지만 TCP/UDP 문제는 원인 범위를 잘못 잡으면 임시 완화가 더 큰 장애를 만든다.
처음에는 다음을 피한다.
- TIME_WAIT이 많다는 이유만으로 무작정 timeout을 줄이기
- MTU 문제를 확인하지 않고 모든 인터페이스 MTU를 낮추기
- ALB 502를 보고 서버 timeout만 크게 늘리기
- UDP 손실을 retry storm으로 덮기
- BBR/CUBIC을 측정 없이 바꾸기
- CLOSE_WAIT을 OS 튜닝으로 해결하려 하기
대신 다음 순서로 본다.
- 증상이 L7인지 L4인지 먼저 나눈다.
refused,reset,timeout중 어떤 실패인지 구분한다.- 작은 payload와 큰 payload의 차이를 본다.
- 새 연결과 재사용 연결의 차이를 본다.
- 상태 소켓이 자연 소멸하는지 계속 쌓이는지 본다.
- 설정 변경은 재현 조건과 되돌림 계획이 있을 때만 한다.
16. TCP 원리의 전이 모델
섹션 제목: “16. TCP 원리의 전이 모델”TCP에서 배운 원리는 다른 시스템에서도 반복된다. 이 절은 비유가 아니라 구조적 대응 관계다.
| TCP 원리 | TCP 구현 | 다른 시스템의 대응 |
|---|---|---|
| 흐름 제어 | rwnd로 수신자 버퍼 보호 | DB connection pool size, queue prefetch |
| 혼잡 제어 | cwnd로 네트워크 과부하 방지 | rate limit, backpressure, adaptive concurrency |
| 재전송 | sequence/ACK/RTO | retry + idempotency key |
| 안전 종료 | FIN, TIME_WAIT | graceful shutdown, drain, circuit breaker HALF_OPEN |
| 연결 재사용 | HTTP Keep-Alive | DB pool, Redis pool, worker pool |
새 시스템을 볼 때 다음 네 질문을 던질 수 있다.
- 수신자를 보호하는 window나 queue limit은 무엇인가?
- 과부하 시 송신자가 감속하는 신호는 무엇인가?
- 실패한 작업을 재시도할 때 중복 처리를 막는 키는 무엇인가?
- 종료 중인 작업을 안전하게 drain하는 상태는 무엇인가?
이 질문은 TCP를 외운 지식이 아니라 분산 시스템 판단 도구로 바꿔 준다.
17. 핵심 비교표
섹션 제목: “17. 핵심 비교표”17-1. TCP vs UDP
섹션 제목: “17-1. TCP vs UDP”| 항목 | TCP | UDP |
|---|---|---|
| 연결 | 3-way handshake로 연결 수립 | 연결 없음 |
| 데이터 모델 | 바이트 스트림 | datagram |
| 순서 보장 | 보장 | 보장하지 않음 |
| 손실 복구 | ACK와 재전송 | 애플리케이션 책임 |
| 흐름 제어 | 있음 | 없음 |
| 혼잡 제어 | 있음 | 없음 또는 앱/상위 프로토콜 책임 |
| 헤더 | 20~60 bytes | 8 bytes |
| 장점 | 신뢰성, 생태계, 운영 도구 | 낮은 지연, 단순함, 앱별 제어 |
| 대표 사용 | HTTP/1.1, HTTP/2, DB, SSH | DNS, 실시간 미디어, 게임, QUIC |
17-2. Flow Control vs Congestion Control
섹션 제목: “17-2. Flow Control vs Congestion Control”| 항목 | Flow Control | Congestion Control |
|---|---|---|
| 보호 대상 | 수신자 | 네트워크 경로 |
| 핵심 변수 | rwnd | cwnd, ssthresh |
| 신호 출처 | 수신 버퍼 여유 | 손실, RTT, ACK 패턴 |
| 실패 모양 | 수신 앱이 못 읽어 전송 지연 | 패킷 손실, p99 지연, throughput 출렁임 |
| 비슷한 시스템 | queue capacity, pool size | backpressure, adaptive rate limit |
17-3. TIME_WAIT vs CLOSE_WAIT
섹션 제목: “17-3. TIME_WAIT vs CLOSE_WAIT”| 항목 | TIME_WAIT | CLOSE_WAIT |
|---|---|---|
| 의미 | 내가 먼저 닫고 마지막 ACK 후 대기 | 상대가 닫았는데 내 앱이 아직 close 안 함 |
| 정상성 | 대개 정상 | 지속 증가하면 비정상 |
| 자연 소멸 | 일정 시간 후 소멸 | 앱이 닫아야 소멸 |
| 먼저 볼 것 | 연결 재사용, short-lived connection | 소켓/stream close 누락, pool 반환 |
17-4. TCP Keepalive vs HTTP Keep-Alive
섹션 제목: “17-4. TCP Keepalive vs HTTP Keep-Alive”| 항목 | TCP Keepalive | HTTP Keep-Alive |
|---|---|---|
| 계층 | L4 | L7 |
| 목적 | 유휴 연결 생존 확인 | HTTP 요청 간 TCP 연결 재사용 |
| 주 실패 | 죽은 연결 감지 지연 | timeout mismatch, 502 |
| 학습 포인트 | probe라는 생존 확인 | handshake 비용 절약과 timeout 정렬 |
18. 선택 부록: 짧은 관찰 스니펫
섹션 제목: “18. 선택 부록: 짧은 관찰 스니펫”본문 이해에는 아래 명령이 필수는 아니다. 운영 중 증상을 좁힐 때 “이런 식으로 관찰한다” 정도만 잡으면 된다.
브라우저 timing으로 DNS/TCP/TLS/TTFB 나누기
DNS lookup -> 이름 해석 지연Initial connection -> TCP handshake 지연SSL -> TLS handshake 지연Waiting (TTFB) -> 서버 처리 + upstream 대기 + 첫 바이트 도착Content Download -> 응답 본문 전송 시간관찰 포인트:
- DNS만 길면 resolver, TTL, public/private DNS를 의심한다.
- Initial connection만 길면 TCP 경로, 방화벽, SYN 재전송을 의심한다.
- TTFB가 길면 앱 처리, DB, upstream timeout, 프록시 대기를 같이 본다.
- Download만 길면 응답 크기, 대역폭, 재전송, MTU/PMTUD를 본다.
UDP datagram 경계를 보는 최소 예시
UDP send #1: "hello"UDP send #2: "world"
receiver가 보는 단위:datagram #1 = "hello"datagram #2 = "world"TCP라면 두 번의 write가 하나의 byte stream으로 합쳐지거나 쪼개질 수 있다. UDP는 datagram 단위를 보존하지만, 순서·재전송·혼잡 제어는 애플리케이션이나 상위 프로토콜이 책임져야 한다.
3-way handshake와 reset 신호 보기
sudo tcpdump -i any -nn 'tcp port 443 and (tcp[tcpflags] & (tcp-syn|tcp-ack|tcp-rst) != 0)'관찰 포인트:
S는 SYN이다.S.는 SYN-ACK이다.R은 reset이다.- SYN만 반복되면 응답 경로를 의심한다.
- RST가 즉시 오면 listener 부재나 명시적 거절을 의심한다.
연결 상태 분포 확인
ss -sss -tan state time-wait | headss -tan state close-wait | head관찰 포인트:
- TIME_WAIT은 많은 트래픽에서 정상적으로 많을 수 있다.
- CLOSE_WAIT이 계속 증가하면 앱이 socket을 닫지 않는 경로를 찾는다.
- ESTABLISHED가 급증하면 장기 연결, pool, WebSocket, slow client를 함께 본다.
MTU/PMTUD 의심 시 작은 확인
ping -s 1472 -M do example.com관찰 포인트:
- IPv4에서
1472 + ICMP header 8 + IP header 20 = 1500이다. - “Frag needed”류 메시지가 나오면 경로 MTU보다 큰 패킷을 보낸 것이다.
- ICMP가 막혀 있으면 PMTUD가 실패해 큰 응답만 멈출 수 있다.
ALB 502와 keep-alive timeout 확인 방향
확인 순서:1. ALB access log에서 elb_status_code와 target_status_code를 분리한다.2. target_status_code가 "-"이면 타겟 HTTP 응답 전 연결 문제를 의심한다.3. backend keepAliveTimeout이 ALB idle timeout보다 긴지 확인한다.4. headersTimeout이 keepAliveTimeout보다 긴지 확인한다.핵심 관계:
headersTimeout > keepAliveTimeout > ALB idle timeoutNagle/Delayed ACK 의심 시 관찰 포인트
의심 조건:- 작은 메시지에서만 40ms 안팎 지연이 반복된다.- write가 여러 번 쪼개져 있다.- 실시간 소켓 또는 RPC처럼 작은 payload가 많다.운영 판단:
- 대부분의 HTTP API와 WebSocket 서버는 TCP_NODELAY가 더 안전하다.
- 단, 패킷 수 증가가 문제인 환경에서는 측정 후 결정한다.
19. 복습 체크리스트
섹션 제목: “19. 복습 체크리스트”TCP/UDP Internals 복습 체크
- TCP가 바이트 스트림 프로토콜이라는 뜻을 설명할 수 있다
- 3-way handshake에서 SYN, SYN-ACK, ACK가 각각 확인하는 것을 말할 수 있다
- sequence number와 ACK가 손실 복구의 좌표계라는 점을 설명할 수 있다
- Fast Retransmit이 timeout보다 빠르게 손실을 감지하는 조건을 설명할 수 있다
- Flow Control의 rwnd와 Congestion Control의 cwnd를 구분할 수 있다
- MSS가 MTU에서 IP/TCP 헤더를 뺀 값이라는 점을 계산할 수 있다
- PMTUD 실패가 왜 큰 응답에서만 드러날 수 있는지 설명할 수 있다
- TIME_WAIT과 CLOSE_WAIT 중 어느 쪽이 소켓 누수 신호인지 구분할 수 있다
- UDP가 빠른 이유와 그 대가를 함께 설명할 수 있다
- QUIC이 UDP 위에서 신뢰성을 직접 구현한다는 경계를 설명할 수 있다
- TCP Keepalive와 HTTP Keep-Alive의 계층과 목적을 구분할 수 있다
- ALB와 NLB 선택 기준을 L7/L4 관점에서 말할 수 있다
- Connection refused, reset, timeout, 502의 첫 의심 범위를 나눌 수 있다
20. 핵심 키워드
섹션 제목: “20. 핵심 키워드”| 키워드 | 설명 |
|---|---|
| TCP | 연결 지향, 순서 보장, 재전송, 흐름/혼잡 제어를 제공하는 L4 프로토콜 |
| UDP | 연결 없이 datagram을 보내는 얇은 L4 프로토콜 |
| 4-tuple | source IP/port와 destination IP/port로 TCP 연결을 식별하는 값 |
| SYN | TCP 연결 시작 시 sequence number 동기화를 요청하는 flag |
| ACK | 받은 바이트 범위를 확인하는 acknowledgment |
| Sequence Number | TCP 바이트 스트림의 위치를 나타내는 번호 |
| RTO | ACK가 오지 않을 때 재전송을 시작하는 timeout |
| Fast Retransmit | 중복 ACK를 보고 timeout 전에 손실 세그먼트를 재전송하는 방식 |
| rwnd | receive window. 수신자 버퍼 여유를 나타내는 흐름 제어 값 |
| cwnd | congestion window. 네트워크 혼잡을 피하기 위한 송신 측 window |
| ssthresh | Slow Start에서 Congestion Avoidance로 넘어가는 임계값 |
| MSS | TCP payload 한 조각의 최대 크기 |
| MTU | 링크 하나에서 한 번에 보낼 수 있는 최대 frame 크기 |
| PMTUD | 경로상 가장 작은 MTU를 찾아 fragmentation을 피하려는 절차 |
| TIME_WAIT | 먼저 닫은 쪽이 마지막 ACK 이후 옛 세그먼트 소멸을 기다리는 상태 |
| CLOSE_WAIT | 상대가 닫았지만 내 애플리케이션이 아직 socket을 닫지 않은 상태 |
| TCP Keepalive | 유휴 TCP 연결이 살아 있는지 L4 probe로 확인하는 기능 |
| HTTP Keep-Alive | HTTP 요청 사이에서 TCP 연결을 재사용하는 L7 기능 |
| Nagle Algorithm | 작은 패킷을 모아 보내려는 송신 측 최적화 |
| Delayed ACK | ACK 전송을 잠깐 늦춰 빈 ACK 패킷을 줄이는 수신 측 최적화 |
| QUIC | UDP 위에서 TLS, stream, 손실 복구, 혼잡 제어를 구현하는 전송 프로토콜 |
| ALB | L7 HTTP/gRPC 라우팅과 관측을 제공하는 AWS Application Load Balancer |
| NLB | L4 TCP/UDP/TLS 전달과 정적 IP에 적합한 AWS Network Load Balancer |
21. 출처와 추천 리소스
섹션 제목: “21. 출처와 추천 리소스”- RFC 9293: Transmission Control Protocol — TCP 표준. 처음에는 상태 전환과 sequence/ACK 개념 위주로 읽는다.
- RFC 768: User Datagram Protocol — UDP 표준. 헤더가 왜 얇은지 확인하기 좋다.
- RFC 9000: QUIC — QUIC의 connection ID, stream, loss recovery 경계를 확인할 때 본다.
- MDN: TCP slow start — Slow Start를 짧게 복습하기 좋다.
- AWS re:Post: Troubleshoot Application Load Balancer HTTP 502 errors — ALB 502 원인 분류를 공식 관점으로 확인할 수 있다.
- NestJS: Keep-Alive connections — NestJS와 Node.js keep-alive timeout 설정을 확인할 때 본다.
22. 한 줄 요약
섹션 제목: “22. 한 줄 요약”TCP는 불안정한 IP 위에서 애플리케이션이 믿고 읽을 수 있는 바이트 스트림을 만들기 위해 handshake, sequence/ACK, 재전송, 흐름 제어, 혼잡 제어를 제공한다.
UDP는 그 공통 비용을 빼고 애플리케이션이 필요한 신뢰성만 직접 선택하게 한다.
장애를 볼 때는 refused, reset, timeout, 502, TIME_WAIT, CLOSE_WAIT 같은 증상을 먼저 L4/L7 경계에 배치하면 원인 후보를 훨씬 빠르게 줄일 수 있다.