응답이 느려요 — 병목 찾기
증상
서비스는 작동합니다. 연결도 됩니다. 그런데 응답이 느립니다. 브라우저 개발자 도구에서 보면 API 호출 하나에 2~3초가 걸립니다. 가끔은 괜찮고, 가끔은 또 느립니다. "서버가 느린 것 같다"는 제보는 들어오는데, 원인이 어디인지 알 수가 없습니다.
가설
응답 지연은 원인이 여러 계층에 분산되어 있습니다. DNS 조회가 느릴 수 있고, TCP 핸드셰이크에서 RTT가 클 수도 있고, 서버 처리 자체가 오래 걸릴 수도 있습니다. 이 세 구간을 구분하지 않으면 엉뚱한 곳을 최적화하는 실수를 합니다.
도구로 확인하기
curl에는 각 단계의 시간을 측정할 수 있는 -w 옵션이 있습니다.
# 파일: 터미널curl -w "\n\ntime_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n" -o /dev/null -s https://api.example.com/health
출력 예시입니다.
time_namelookup: 0.004
time_connect: 0.082
time_appconnect: 0.150
time_starttransfer: 1.842
time_total: 1.843
각 항목이 무엇을 의미하는지 보면 됩니다. time_namelookup은 DNS 조회에 걸린 시간입니다. time_connect는 DNS 완료부터 TCP 핸드셰이크 완료까지 걸린 누적 시간으로, TLS는 포함하지 않습니다. HTTPS의 TLS 핸드셰이크가 끝나는 시점은 별도 항목인 time_appconnect로 측정됩니다. time_starttransfer는 첫 번째 바이트가 도착하기까지 걸린 누적 시간입니다. 서버가 요청을 받아서 응답을 시작하기까지의 처리 시간이 여기에 포함됩니다.
각 구간은 인접한 두 값의 차로 구합니다. 위 예시에서 DNS 조회는 4ms, TCP 핸드셰이크는 time_connect에서 time_namelookup을 빼서 78ms, TLS 핸드셰이크는 time_appconnect에서 time_connect를 빼서 68ms, 서버 처리는 time_starttransfer에서 time_appconnect를 빼서 1692ms가 걸렸습니다. 원인은 서버 처리 단계에 있습니다.
반대로 time_namelookup이 1초 이상이라면 DNS가 병목입니다. DNS 서버를 8.8.8.8이나 1.1.1.1로 바꾸거나, 로컬 캐싱 리졸버를 설정하는 것을 고려합니다.
time_connect가 비정상적으로 크다면 네트워크 경로에 지연이 있는 것입니다. traceroute로 경로를 확인합니다.
# 파일: 터미널traceroute api.example.com
각 홉의 응답 시간을 보면 어느 구간에서 지연이 생기는지 추적할 수 있습니다. 특정 홉에서 갑자기 수백 ms로 뛴다면 그 구간의 회선이나 라우터가 문제입니다. 지리적으로 멀리 떨어진 서버를 쓰고 있다면 CDN 도입이나 서버 리전 변경을 고려해야 합니다.
원인과 해결
세 가지 가능성을 순서대로 확인합니다.
첫째, DNS가 느리다면 DNS 서버를 변경하거나 결과를 캐싱하는 리졸버를 추가합니다. 둘째, RTT가 크다면 사용자와 서버 사이의 물리적 거리가 원인인 경우가 많습니다. 서버를 사용자에게 가까운 리전으로 이동하거나 CDN을 앞에 붙입니다. 셋째, 서버 처리 시간이 길다면 네트워크는 정상이고 코드나 데이터베이스 쿼리를 최적화해야 합니다.
이 PART를 마치며
이 장에서 다룬 도구들 — ping, traceroute, dig, curl, netstat, ss, tcpdump, Wireshark — 은 모두 이미 알고 있는 프로토콜의 실제 동작을 눈으로 보여주는 창입니다. 책에서 배운 계층, 패킷, 핸드셰이크, TTL, TIME_WAIT가 더 이상 추상적인 개념이 아니라 터미널 출력에서 직접 읽히는 것들이 됩니다. 원리를 이해하는 사람은 도구 하나로 훨씬 많은 것을 봅니다.