항상 문제는 TCP_NODELAY다
(brooker.co.za)- 분산 시스템의 지연(latency) 문제를 디버깅할 때 가장 먼저 확인해야 하는 항목이 TCP_NODELAY 설정임
- Nagle 알고리듬은 1984년 RFC896에서 제안된 방식으로, 작은 패킷 전송 시 TCP 헤더 오버헤드를 줄이기 위해 설계됨
- 그러나 지연된 ACK(delayed ACK) 메커니즘과 결합될 때, 데이터 전송이 ACK 수신까지 지연되어 지연 민감형 애플리케이션의 성능을 악화시킴
- 현대 데이터센터 환경에서는 RTT가 매우 짧고, 대부분의 시스템이 이미 큰 메시지를 전송하므로 Nagle 알고리듬의 이점이 거의 사라짐
- 따라서 현대 분산 시스템에서는 TCP_NODELAY를 기본으로 활성화해야 하며, Nagle 알고리듬은 더 이상 필요하지 않음
Nagle 알고리듬의 배경
- 1984년 John Nagle의 RFC896은 키보드 입력처럼 작은 데이터 전송 시 발생하는 40바이트 헤더 대비 1바이트 데이터의 4000% 오버헤드 문제를 해결하기 위해 제안됨
- 당시 문제는 사용자가 한 글자씩 입력할 때마다 작은 패킷이 전송되어 네트워크 효율이 낮아지는 현상
- 해결책은 이전 데이터가 ACK되지 않은 상태에서는 새로운 세그먼트를 전송하지 않도록 제한하는 방식
- 이 접근은 당시 네트워크 환경에서는 효과적이었으나, 지연 시간(latency) 이 중요한 현대 시스템에는 부적합
Nagle 알고리듬과 Delayed ACK의 상호작용
- Delayed ACK(RFC813, RFC1122)는 수신 측이 즉시 ACK를 보내지 않고, 응답 데이터가 생기거나 타이머가 만료될 때까지 ACK를 지연시키는 방식
- Nagle 알고리듬은 ACK를 기다리며 전송을 멈추고, delayed ACK는 ACK를 늦추므로 양쪽이 서로 기다리는 교착 상태가 발생
- John Nagle 자신도 이 조합을 “끔찍한 조합”이라 표현하며, 두 기능이 독립적으로 도입되었지만 함께 사용될 때 지연을 유발한다고 지적
현대 환경에서의 문제점
- 데이터센터 내 RTT는 약 500μs, 동일 리전 내에서도 수 밀리초 수준으로 매우 짧음
- 이런 환경에서 한 RTT만큼 전송을 지연하는 것은 성능 손실로 이어짐
- 또한 현대 분산 시스템은 TLS, 직렬화, 프로토콜 오버헤드 등으로 인해 이미 충분히 큰 메시지를 전송하므로, 단일 바이트 패킷 문제는 거의 존재하지 않음
- 작은 메시지 최적화는 이제 애플리케이션 계층에서 처리되고 있음
TCP_NODELAY의 필요성
-
지연에 민감한 분산 시스템에서는 TCP_NODELAY를 활성화해 Nagle 알고리듬을 비활성화하는 것이 권장됨
- 이는 “비효율적”이거나 “잘못된 설정”이 아니라, 현대 하드웨어와 트래픽 특성에 맞는 선택
- 저자는 TCP_NODELAY가 기본값이 되어야 한다고 주장
- 일부 “write() 호출마다 전송”하는 코드가 느려질 수 있으나, 그런 코드는 근본적으로 수정되어야 함
기타 관련 옵션
- TCP_QUICKACK 옵션은 ACK 지연을 줄이지만, 이식성 문제와 비일관적 동작으로 인해 근본 해결책이 아님
- 핵심 문제는 커널이 애플리케이션이 의도한 시점보다 데이터를 오래 보유하는 것이며, write() 호출 시 즉시 전송되어야 함
결론
- Nagle 알고리듬은 과거 네트워크 효율을 높이기 위한 훌륭한 발명이었으나,
현대의 고속 네트워크와 분산 시스템 환경에서는 오히려 지연을 초래하는 구시대적 기능 - 따라서 TCP_NODELAY를 항상 활성화하는 것이 현대 시스템 설계의 기본 원칙으로 제시됨
Hacker News 의견들
- 예전 멀티포인트 네트워킹 시절에 만들어진 Nagle 알고리즘의 배경을 설명함
당시 여러 호스트가 하나의 이더넷 채널을 공유했기 때문에 충돌을 피하기 위해 CSMA/CD를 사용했음
하지만 오늘날 대부분의 이더넷은 포인트 투 포인트 구조로, 송수신이 동시에 가능한 풀 듀플렉스 환경임
따라서 CSMA는 더 이상 필요하지 않으며, TCP_NODELAY를 설정해 Nagle 알고리즘을 비활성화하는 것이 대부분의 경우 합리적이라 생각함- CSMA 관련 동기가 Nagle 알고리즘 설계에 실제로 있었던 건지, 아니면 단순히 시대적 배경을 언급한 것인지 궁금함
- 사실 Nagle 알고리즘은 단순히 패킷 병합(coalescing) 목적이었음
기본값으로 설정된 것은 네트워킹 역사상 큰 실수 중 하나라고 생각함 - 참고로 이더넷은 CSMA/CD, WiFi는 CSMA/CA를 사용함
2014년쯤 데이터센터 스위치를 교체할 때, 10Mbit 하프 듀플렉스를 지원하지 않아 일부 구형 장비를 유지해야 했던 경험이 있음 - 애플리케이션이 패킷 크기를 신경 쓰지 않거나 지연에 민감하지 않을 때는 Nagle이 꽤 합리적임
너무 작은 패킷 생성을 방지해줌 - 네트워크 레이어 혼동이 있는 것 같음
Nagle은 TCP 계층의 최적화로, 작은 패킷을 묶어 효율을 높이는 역할임
CSMA는 물리/데이터링크 계층의 문제로, Nagle과는 별개임
- 게임 개발 중 네트워크 지연 문제를 디버깅하다가 이 글을 발견했음
Go로 작성된 백엔드는 기본적으로 TCP_NODELAY가 설정되어 있어서 원인은 아니었지만, Nagle의 문제 인식에 대한 부분이 흥미로웠음
예전 토론도 있었는데 이 스레드 참고 가능함- Julia Evans의 좋은 글도 추천함
DICOM 프로토콜처럼 채팅형 통신에서는 TCP_NODELAY=1로 설정 시 처리량이 크게 향상됨 - 어떤 게임을 개발 중인지 궁금함. 나도 Ebitengine과 Golang으로 게임 개발을 즐기고 있어서 관심이 많음
- Julia Evans의 좋은 글도 추천함
- Nagle 본인이 10년 전쯤 말하길, 진짜 문제는 delayed ACK라고 함
관련 링크 참고
요즘 워크로드에서는 delayed ACK이 큰 이점을 주지 않는다고 생각함
HTTP 중심의 현대 환경에서는 Nagle과 delayed ACK 둘 다 끄는 게 낫다고 봄- 원문에서도 이를 다룸
데이터센터 간 RTT가 수백 마이크로초 수준이라, 한 RTT라도 지연시키는 건 오히려 손해일 수 있음
- 원문에서도 이를 다룸
- 폴란드어로 “nagle”은 “갑자기”라는 뜻인데, 알고리즘 이름과 너무 잘 어울려서 놀라웠음
- Nagle 알고리즘이 커널 기본값으로 설정된 건 이상하다고 생각함
언제 보낼지, 언제 버퍼링할지는 애플리케이션이 결정해야 함 - 글에서 MSG_MORE를 언급하지 않은 게 의외였음
Linux에서는 추가 데이터가 곧 전송될 것임을 커널에 알려주는 힌트로, 헤더와 데이터를 따로 보낼 때 유용함
io_uring과 함께 쓰면 더 효율적임- 사실 하나의 시스템 콜로 여러 조각 데이터를 복사 없이 보낼 수도 있음
- Nagle 알고리즘의 문제는 소켓 API에 즉시 전송(flush) 기능이 없다는 점이라 생각함
즉시 응답이 필요한 메시지 이후에 버퍼를 비워 보내는 기능이 있으면 좋겠음
요즘 TCP 채널은 동기·비동기 메시지가 섞여 있어서 더 복잡함
SCTP 같은 프로토콜이 더 널리 쓰였으면 함- 스트림 API에 flush 기능이 없다는 점에 동의함. 명백한 설계 누락이라 생각함
- 네트워크 I/O를 파일처럼 다루려는 UNIX 철학은 이해하지만, 메시지 지향 API가 처음부터 있었으면 이런 문제는 없었을 것임
TLS 같은 래핑에서도 메시지 경계를 찾는 게 번거로움 - 모든 send에 MSG_MORE를 붙이고 마지막에만 빼면 간접적으로 flush 효과를 낼 수 있을 듯함
- 스트림 API는 여러모로 불편함
이상적으로는 “버퍼링 허용” 비트를 설정해 큰 전송을 나누고 마지막에 “즉시 전송”을 지정할 수 있어야 함
TCP_CORK는 그나마 비슷한 대안이지만 좀 조잡함
파일 I/O도 비슷한 문제를 겪음 - TCP_CORK가 뭔지 궁금함
- (2024) 이전 토론은 이 링크에서 있었음
-
Oxide and Friends 팟캐스트 에피소드에서 이 주제를 다룸
꽤 흥미로운 내용임- Oxide는 서버 OS와 하드웨어를 새로 설계하는 회사라, 전통적인 프로토콜을 재검토하는 접근이 브랜드 철학과 잘 맞음
- Nagle 알고리즘은 정책을 커널에 넣은 것 같아 어색함
애플리케이션이 지연과 처리량의 균형을 직접 조절할 수 있어야 함- delayed ack이 없을 때는 합리적인 알고리즘이며, TCP 스택의 일부로 존재하는 이유는 그 계층에서 문제를 해결하려 했기 때문임
하지만 애플리케이션 수준에서 구현하려면 unacked data를 알아야 해서 비효율적임 - 이론적으로는 맞지만, 현실적으로는 대부분의 유저스페이스 코드가 네트워크 하부를 신경 쓰지 않음
단순한 20ms flush 타이머만 있었어도 훨씬 나았을 것임 - 사실 TCP_NODELAY는 소켓 단위로 설정되므로, 애플리케이션이 직접 선택하는 유저스페이스 결정에 가깝다고 봄
- 한 프로그램의 트레이드오프가 다른 프로그램에 영향을 줄 수 있으므로, 커널이 전체 시스템 관점에서 중재자 역할을 하는 게 필요하다고 생각함
- delayed ack이 없을 때는 합리적인 알고리즘이며, TCP 스택의 일부로 존재하는 이유는 그 계층에서 문제를 해결하려 했기 때문임