Reddit

How a Missing TCP_NODELAY Flag Silently Ate 80% of Our Database Throughput

빠진 TCP_NODELAY 설정이 데이터베이스 처리량을 조용히 갉아먹은 과정

Go 데이터베이스 드라이버 업데이트 뒤 TCP_NODELAY 설정이 빠지면서 작은 TCP 쓰기마다 지연이 발생했고, PostgreSQL 연결과 API 처리량이 함께 악화됐습니다. 글은 ClientWrite 대기 이벤트와 소켓 시스템 호출로 원인을 추적하고, 수정 후 처리량이 초당 1,100건에서 8,400건으로 늘어난 결과를 공유합니다.

AI 요약

결제 API 서버와 PostgreSQL의 CPU 사용률은 낮았지만, 배포 직후 핵심 거래 API의 응답 시간이 35ms에서 4.2초 이상으로 뛰었습니다. 글은 데이터베이스 쿼리 문제가 아니라 Go 서비스와 데이터베이스 사이의 TCP 소켓 동작이 병목이었다고 설명합니다.

장애 징후와 추적

v2.14.0 배포는 12개 API 노드에 적용됐습니다. 4분 뒤 /v1/checkout의 P99 지연 시간이 4.2초로 치솟았고, HTTP 504 오류가 발생했습니다. 팀은 먼저 새 버전의 느린 쿼리를 의심해 PostgreSQL의 pg_stat_activity와 pg_stat_statements를 확인했습니다. 그러나 활성 연결 대부분에서 wait_event_type은 Client, wait_event는 ClientWrite였습니다. PostgreSQL이 쿼리를 처리한 뒤 결과를 보내고 있었지만, 클라이언트가 소켓에서 데이터를 받아 가지 않아 기다리는 상태였습니다.

스테이징 환경에 실제 트래픽 일부를 재현하고 API 프로세스에 strace를 붙이자, 데이터베이스 연결로 작은 패킷을 보낸 뒤 다음 읽기 작업이 거의 매번 40ms 동안 지연되는 현상이 보였습니다. 글은 이 시간을 Nagle 알고리즘과 TCP Delayed ACK가 맞물릴 때 생기는 대기로 설명합니다.

작은 쓰기와 40ms 지연

Nagle 알고리즘은 작은 패킷을 잇달아 보내는 일을 줄이려고 이전 패킷의 ACK를 받거나 전송할 데이터가 충분히 쌓일 때까지 작은 쓰기를 모읍니다. 반면 Linux TCP 수신 측은 응답에 ACK를 함께 실을 수 있도록 ACK 전송을 최대 40ms 미루기도 합니다. 이 둘이 함께 작동하면 송신 측은 ACK를 기다리며 다음 데이터를 보류하고, 수신 측은 추가 데이터가 오기를 기다리며 ACK를 미룹니다.

이 사례에서는 데이터베이스 요청 헤더나 매개변수처럼 여러 부분으로 나뉜 작은 쓰기마다 이런 대기가 생겼습니다. v2.14.0에서 Go 데이터베이스 드라이버를 올리면서 연결 소켓에 TCP_NODELAY를 설정하던 초기화 코드가 사라진 탓입니다. TCP_NODELAY는 Nagle 알고리즘을 끄고 작은 패킷도 바로 보내도록 합니다. 그 결과 동시 네트워크 I/O 대기가 쌓였고, 데이터베이스 활성 연결 200개가 모두 소진됐습니다.

수정과 부하 테스트

팀은 드라이버의 연결 초기화 과정에 소켓 제어 콜백을 다시 추가해 TCP_NODELAY를 명시적으로 설정했습니다. 초당 5,000건 요청을 재현한 스테이징 부하 테스트에서 P50 지연 시간은 42.1ms에서 2.4ms로, P99는 4,120ms에서 18.2ms로 줄었습니다. 최대 활성 DB 연결 수는 200개에서 14개로 감소했고, 처리량은 초당 1,100건에서 8,400건으로 증가했습니다.

글은 PostgreSQL의 ClientWrite 대기 이벤트가 보이면 데이터베이스 계산보다 클라이언트 애플리케이션이나 네트워크 버퍼를 살펴야 한다고 정리합니다. 드라이버를 업그레이드할 때는 공개 API 변화만 확인하지 말고 저수준 연결 초기화 코드도 비교해야 합니다. 또한 로컬호스트에서 수행한 부하 테스트는 네트워크 지연이 거의 없어 이 사례의 40ms 효과를 드러내지 못했습니다. 실제 네트워크 지연 특성을 반영한 테스트가 필요하다고 덧붙입니다.

Reddit 반응

  • @u/SuicidalTree — kylesinvestigation[.]com의 자기 홍보 스팸 계정입니다.
  • @u/Suitable-Ad5348 — 연결 풀링이 부하를 견디지 못할 때까지, 빠진 nodelay 설정이 얼마나 큰 피해를 주는지 다들 잊곤 합니다.

원문: Reddit / 번역·요약: Trawling