작은 TCP 쓰기가 멈칫하는 이유: Nagle 알고리즘과 지연 ACK, TCP_NODELAY
요청을 작은 write 두 번으로 나눠 보내면 두 번째 조각이 상대의 ACK를 기다릴 수 있습니다. Nagle 알고리즘과 지연 ACK의 규칙을 RFC 896, 1122, 9293으로 나눠 보고, TCP_NODELAY와 리눅스 전용 옵션이 각각 무엇을 바꾸는지 정리했습니다.
클라이언트가 요청 헤더를 write()로 한 번, 본문을 또 한 번 보냅니다. 서버는 요청이 다 도착하면 금방 처리하는데도 응답이 늦게 옵니다. 본문이 서버에 늦게 도착하기 때문입니다. 이럴 때 의심해 볼 만한 것이 송신 쪽의 Nagle 알고리즘과 수신 쪽의 지연 ACK가 맞물리는 상황입니다. 둘 다 따로 보면 패킷 수를 줄이려는 합리적인 규칙인데, 요청-응답 패턴에서 서로를 기다리게 됩니다.
다만 작은 쓰기가 느리다고 해서 언제나 이 상호작용 때문은 아닙니다. 아래에서는 두 규칙이 어디서 왔는지, 표준이 무엇을 요구하는지, 어떤 조건에서 지연이 생기는지를 나눠 봅니다. 표준은 현재 TCP 명세인 RFC 9293(2022년 8월, STD 7)을 기준으로 하고, 이전 요구사항인 RFC 1122와 역사적 배경인 RFC 896을 함께 봅니다. 리눅스 동작은 2026년 10월 18일에 확인한 man-pages 저장소의 tcp(7)(미출시 개발 버전)만 근거로 합니다.
송신 쪽: Nagle 알고리즘
출발점은 1984년 John Nagle의 RFC 896입니다. 이 문서는 첫머리에서 스스로 표준을 정하지 않는 논의용 메모라고 밝힙니다. 다룬 문제는 Telnet처럼 키 입력 한 글자씩 보내는 경우였습니다. 데이터 1바이트에 헤더 40바이트가 붙은 41바이트 패킷이 글자마다 나가고, 부하가 큰 네트워크에서는 이 오버헤드가 혼잡을 키운다는 것입니다.
RFC 896이 제안한 규칙은 단순합니다. 이미 보낸 데이터 중 확인(ACK)받지 못한 것이 남아 있으면, 사용자에게서 새 데이터가 와도 새 세그먼트를 보내지 않습니다. 원문은 이 억제가 타이머나 크기 검사 없이 무조건이라고 적습니다. 연결이 한가할 때는 미확인 데이터가 없으니 쓰는 즉시 나가고, 보류된 데이터는 상대의 ACK가 도착할 때 한꺼번에 나갑니다.
RFC 1122(1989년)는 이것을 요구사항으로 옮기면서 조건을 하나 더했습니다. 미확인 데이터가 있으면 송신 TCP는 사용자 데이터를 PSH 비트와 관계없이 버퍼에 모으되, 미확인 데이터가 확인되거나 최대 크기 세그먼트(Eff.snd.MSS 바이트)를 채울 수 있게 되면 보냅니다. 구현은 Nagle 알고리즘을 구현해야 하고(SHOULD), 애플리케이션이 연결 단위로 이를 끌 방법은 반드시 있어야 합니다(MUST).
RFC 9293은 RFC 1122의 TCP 부분을 대체하며 같은 내용을 3.7.4절에 둡니다. 구현 권고는 SHLD-7, 연결 단위로 끄는 수단은 MUST-17입니다. 이 절은 Nagle 알고리즘이 현재 대부분의 TCP 코드베이스에 구현되어 있고 때로는 약간 변형되어 있다고 적습니다. 어느 경우든 송신은 slow start의 제한도 함께 받습니다.
수신 쪽: 지연 ACK
받는 쪽은 데이터 세그먼트마다 ACK를 하나씩 보내는 대신 그보다 적게 보내 효율을 높일 수 있습니다. 이것이 지연 ACK입니다. RFC 1122 4.2.3.2절과 RFC 9293 3.8.6.3절의 요구사항은 다음과 같습니다.
- 지연 ACK를 구현해야 한다(SHOULD, RFC 9293의 SHLD-18).
- 지연은 0.5초 미만이어야 한다(MUST, MUST-40).
- 최대 크기 세그먼트가 이어지는 흐름에서는 적어도 두 번째 세그먼트마다 ACK를 보내야 한다(SHOULD, SHLD-19). RFC 9293은 여기에 “또는 새 데이터 2×RMSS 바이트마다”를 덧붙입니다.
RFC 1122의 설명에 따르면 ACK를 늦추는 이유 중 하나는 애플리케이션이 창을 갱신하고 곧바로 응답을 보낼 기회를 주는 것입니다. 그러면 ACK, 창 갱신, 응답 데이터가 세그먼트 하나에 함께 실립니다. 문서는 문자 단위 원격 로그인에서 서버가 보내는 세그먼트 수를 3분의 1로 줄일 수 있다는 예를 듭니다.
표준이 정하는 것은 상한(0.5초 미만)이고, 실제 타이머 값은 구현이 고릅니다. 이 글은 특정 운영체제의 값을 다루지 않습니다.
둘이 만나는 순서
요청을 두 번에 나눠 쓰고, 서버는 요청이 다 와야 응답할 수 있다고 해 봅시다. 처음에는 연결에 미확인 데이터가 없습니다. 전제가 하나 더 있습니다. 서버의 지연 ACK 구현이 이 첫 작은 세그먼트의 ACK를 늦추기로 한다는 것입니다. 표준은 이를 허용할 뿐 요구하지는 않으므로, 다른 구현은 이 세그먼트에 곧바로 ACK를 보낼 수도 있습니다.
클라이언트 (Nagle 켜짐) 서버 (지연 ACK)
write(헤더) ── 세그먼트 1 ────────────▶ 받음. 요청이 덜 와서 응답할 수 없음
write(본문) 미확인 데이터가 있어 보류 실어 보낼 응답이 없어 ACK를 늦출 수 있음
◀──────────────────── ACK ── 다른 계기가 없으면 타이머가 보냄 (표준상 0.5초 미만)
── 세그먼트 2 ────────────▶ 요청 완성, 처리 시작
◀───────────────── 응답 ────
- 헤더는 미확인 데이터가 없으니 바로 나갑니다.
- 본문은 작고, 헤더가 아직 확인되지 않았으니 Nagle 규칙에 따라 보류됩니다.
- 서버는 요청 일부만 받아 응답을 만들 수 없으므로 ACK에 실을 데이터가 없습니다. 작은 세그먼트 하나뿐이라 “두 번째 최대 크기 세그먼트마다”라는 조건에도 걸리지 않으니 서버는 ACK를 늦출 수 있고, 이 시나리오에서는 실제로 늦춥니다.
- 다른 이유로 ACK가 먼저 나가지 않으면 결국 서버의 지연 ACK 타이머가 ACK를 보냅니다. 그 ACK가 도착해야 비로소 본문이 나갑니다.
이 순서는 두 규칙의 정의와 위의 전제에서 따라 나오는 설명입니다. RFC 9293 부록 A.3은 흔한 운영체제에서 Nagle 알고리즘과 지연 ACK가 둘 다 기본으로 켜져 있고, 요청-응답 방식에서 이 조합이 애플리케이션 성능을 떨어뜨릴 수 있다고 적습니다.
뒤집어 보면 위 조건 중 하나만 깨져도 이 지연은 생기지 않습니다. 요청을 한 번에 쓰거나, 두 번째 쓰기 전에 첫 세그먼트의 ACK가 이미 왔거나, 서버가 보낼 데이터가 있어 ACK가 바로 실려 가거나, 서버가 첫 세그먼트의 ACK를 늦추지 않고 보내는 경우입니다. 반대로 작은 쓰기가 늦어지는 원인은 이것 말고도 있습니다. 송신은 slow start의 제한과, 상대가 광고한 창에서 미확인 데이터를 뺀 사용 가능한 창(usable window)의 제한도 받습니다. 그러니 옵션을 바꾸기 전에 패킷 캡처로 두 번째 세그먼트가 정말 상대의 ACK 직후에 나가는지부터 확인하는 편이 낫습니다.
어떻게 피하나
메시지를 한 번에 쓴다
애플리케이션에서 헤더와 본문을 버퍼에 모아 한 번에 쓰면, 미확인 데이터가 없는 연결에서는 Nagle 규칙이 전송을 막지 않습니다. 패킷 수도 늘지 않습니다. 단, 메시지가 세그먼트 하나보다 크면 꽉 찬 세그먼트는 나가지만 끝에 남은 작은 조각은 여전히 앞선 데이터의 ACK를 기다릴 수 있습니다.
TCP_NODELAY로 Nagle을 끈다
표준이 반드시 두라고 한 “연결 단위로 끄는 수단”이 리눅스에서는 TCP_NODELAY 소켓 옵션입니다. RFC 9293 부록 A.3도 Nagle을 끄는 것은 일반적으로 소켓 옵션으로 지원되며, 많은 애플리케이션이 그냥 끈다고 적습니다. 리눅스 tcp(7)에 따르면 TCP_NODELAY를 켜면 데이터가 적어도 세그먼트를 가능한 한 빨리 보냅니다. 끈 상태에서는 충분한 양이 모일 때까지 버퍼에 두어 작은 패킷을 자주 보내지 않습니다.
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
int one = 1;
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one) == -1) {
/* errno 처리 */
}
대가도 분명합니다. Nagle을 끈 채 메시지를 잘게 나눠 쓰면, 쓰기마다 작은 패킷이 될 수 있습니다. RFC 896이 풀려던 문제로 돌아가는 셈이라, TCP_NODELAY를 켜더라도 메시지는 한 번에 쓰는 편이 좋습니다.
리눅스 전용 옵션
아래 두 옵션은 tcp(7)이 이식성 있는 코드에서는 쓰지 말라고 적어 둔 리눅스 동작입니다.
TCP_CORK: Nagle과 반대로 더 모으는 쪽입니다. 켜져 있으면 덜 찬 프레임을 보내지 않고, 옵션을 끄면 모인 것을 내보냅니다.sendfile(2)앞에 헤더를 붙일 때 쓸모가 있다고 문서는 설명합니다. 현재 구현에는 200밀리초 상한이 있어서 그 시간이 지나면 자동으로 전송됩니다.TCP_NODELAY보다 우선하지만,TCP_NODELAY를 설정하는 순간 대기 중인 출력은 명시적으로 내보내집니다.TCP_QUICKACK: 받는 쪽에서 ACK를 늦추지 않고 바로 보내는 quickack 모드로 바꿉니다. 영구 설정이 아니라 모드 전환일 뿐이고, 이후에는 커널 내부 처리에 따라 다시 모드가 바뀔 수 있습니다.
표준 밖의 변형
RFC 9293 부록 A.3은 이 문제를 줄이는 Nagle 알고리즘 변형(Minshall의 제안)을 소개합니다. 일부 흔한 운영체제에 구현되어 있고 상호운용성에는 영향이 없지만, TCP 표준에는 들어가지 않았다고 적습니다. 어떤 시스템이 이 변형을 쓰는지는 이 글에서 확인하지 않았습니다.
정리하면
- RFC 896은 Nagle 알고리즘의 출처지만 표준 문서는 아닙니다.
- 요구사항은 RFC 1122에서 정해졌고, 지금은 RFC 9293이 기준입니다. Nagle 구현은 SHOULD, 연결 단위로 끄는 수단은 MUST, 지연 ACK는 SHOULD이면서 0.5초 미만이 MUST입니다.
- 지연은 “작은 두 번째 쓰기 + 미확인 데이터 + 응답할 수 없으면서 실제로 ACK를 늦추기로 한 수신자”가 겹칠 때 생깁니다.
- 먼저 메시지를 한 번에 쓰고, 필요하면
TCP_NODELAY를 씁니다.TCP_CORK와TCP_QUICKACK는 리눅스에서만 통하는 이야기입니다.
참고한 문서
- RFC 896 Congestion Control in IP/TCP Internetworks: 머리말, “The small-packet problem”, “The solution to the small-packet problem”
- RFC 1122 Requirements for Internet Hosts – Communication Layers: 4.2.3.2, 4.2.3.4, 4.2.5절
- RFC 9293 Transmission Control Protocol (TCP): 3.7.4, 3.8.6.2.1, 3.8.6.3, 3.9.1.2절, 부록 A.3
- Linux man-pages
tcp(7): Socket options,TCP_CORK,TCP_NODELAY,TCP_QUICKACK
링크와 내용은 2026년 10월 18일에 확인했습니다.