Suốt các bài trước ta đo bắt tay TCP, TLS, keep-alive — nhưng tất cả bắt đầu từ một địa chỉ IP. Câu hỏi ít ai để ý: làm sao net.Dial("tcp", "example.com:443") biết IP của example.com? Câu trả lời là một bước xảy ra trước cả bắt tay TCP: DNS resolution — phân giải tên miền thành IP. Nó là một chuyến đi mạng riêng, thường vô hình, và khi nó chậm hoặc timeout, ứng dụng "chậm bí ẩn" mà ta hay đổ lỗi nhầm cho TCP hay server. Bài này (phần 8 loạt Mạng) chạy thật để đo DNS tốn bao nhiêu và cache giúp gì.

Cơ chế: phân giải đệ quy và cache nhiều tầng

Khi chưa có trong cache, phân giải một tên miền là một quá trình đệ quy qua nhiều máy chủ:

resolver -> root (.) -> TLD (.com) -> authoritative (example.com)

Mỗi bước là một chuyến đi mạng, nên lần đầu (cold) chậm. May thay có cache nhiều tầng, mỗi bản ghi giữ theo TTL của nó: cache trong ứng dụng → cache của OS (nscd/systemd-resolved) → cache của resolver. Lần sau trúng cache, kết quả trả về ngay mà không đi tới authoritative.

t0 := time.Now()
ips, _ := net.LookupHost("example.com")   // đo thời gian phân giải
d := time.Since(t0)
// lần đầu = cold (đi mạng); lặp lại = cached (resolver nhớ)
// "localhost" nằm trong /etc/hosts -> phân giải tức thì, không đi mạng

Muốn biết DNS chiếm bao nhiêu trong một request, dùng httptrace với DNSStart/DNSDone:

trace := &httptrace.ClientTrace{
    DNSStart: func(...) { dnsStart = time.Now() },
    DNSDone:  func(...) { dnsDone  = time.Now() },
    GotConn:  func(...) { connDone = time.Now() },  // sau TCP+TLS
}

Ảnh chụp đoạn mã Go nền tối minh hoạ DNS resolution, cơ chế phân giải đệ quy cộng cache nhiều tầng trước khi net Dial tcp example.com 443 bắt tay phải đổi example.com sang IP đường đi khi chưa cache resolver root chấm TLD .com authoritative example.com nhiều bước đi mạng lần đầu cold chậm cache nhiều tầng giữ theo TTL của bản ghi cache ứng dụng cache OS nscd systemd-resolved cache resolver lần sau trúng cache nhanh không đi tới authoritative, đo bằng Go t0 time Now ips net LookupHost example.com đo thời gian phân giải d time Since t0 lần đầu cold đi mạng lặp lại cached resolver nhớ localhost nằm trong etc hosts phân giải tức thì không đi mạng, httptrace tách phần DNS trong một request trace httptrace ClientTrace DNSStart dnsStart DNSDone dnsDone GotConn connDone sau TCP TLS biết DNS chiếm bao nhiêu trong tổng thời gian request

Hình 1: DNS phân giải đệ quy qua root → TLD → authoritative (cold chậm), có cache nhiều tầng theo TTL (cached nhanh); net.LookupHost đo thời gian, /etc/hosts tức thì, httptrace tách phần DNS trong request.

Đo thật: cold, cached, /etc/hosts

Mình phân giải các tên miền thật trong go-lab (có mạng) và đo:

Ảnh chụp bảng kết quả chạy thật DNS output thật, một cùng tên miền cold vs cached example.com lần đầu cold 6.967 ms đến 172.66.147.243 trung bình 50 lần sau cached resolver 794 µs cache nhanh hơn 9 lần không phải đi tới authoritative nữa, hai vài tên miền khác mỗi cái lần đầu cold biến thiên lớn golang.org cold 140.325 ms cold có thể rất chậm thất thường cloudflare.com cold 6.404 ms wikipedia.org cold 6.405 ms, ba etc hosts localhost không đi mạng localhost từ etc hosts 1 µs tức thì không truy vấn DNS, bốn httptrace DNS chiếm bao nhiêu trong 1 request HTTPS DNS 1.015 ms đã cached ở lần này kết nối TCP TLS 58 ms chờ response TTFB 28 ms tổng 86 ms DNS chiếm 1.2 phần trăm khi DNS đã cache thì nhỏ nhưng DNS cold 6.9ms hay 140ms sẽ là phần đáng kể và DNS timeout là nguyên nhân app treo hay bị bỏ sót, kết luận DNS là bước ẩn trước bắt tay TCP cold đi mạng cached nhanh 9x cold biến thiên lớn 6ms tới 140ms tùy resolver authoritative etc hosts tức thì cache cộng TTL quyết định nhanh chậm

Hình 2: Chạy thật — example.com cold 6,967ms vs cached 794µs (~9 lần nhanh hơn); golang.org cold vọt lên 140ms (biến thiên lớn); localhost qua /etc/hosts 1µs (không đi mạng); httptrace cho thấy DNS 1,015ms (đã cache) chiếm 1,2% của request 86ms.

Đọc kết quả đo được:

  • Cold vs cached chênh ~9 lần: example.com lần đầu tốn 6,967ms (đi mạng qua resolver), nhưng 50 lần sau trung bình chỉ 794µs (trúng cache resolver). Cache DNS là thứ khiến bạn hầu như không cảm nhận DNS trong ngày thường — nhưng lần đầu tiên tới một host mới luôn trả giá cold.
  • Cold biến thiên rất lớn: cùng là cold, cloudflare.com và wikipedia.org tốn ~6,4ms, nhưng golang.org vọt lên 140ms. DNS cold phụ thuộc resolver, đường tới authoritative, và tình trạng mạng lúc đó — nó thất thường, và một tên miền có authoritative chậm có thể làm request đầu tiên chậm đột biến.
  • /etc/hosts tức thì: localhost (có trong /etc/hosts) phân giải trong 1µs — không truy vấn DNS gì cả. Đây là lý do dùng /etc/hosts (hoặc IP trực tiếp) loại bỏ hoàn toàn chi phí DNS cho các host cố định.
  • DNS trong request thật: khi đã cache, DNS chỉ chiếm 1,2% (1ms trong tổng 86ms). Nhưng nếu là cold (6,9ms hay 140ms), nó sẽ là phần đáng kể — và tệ hơn, một DNS timeout (vài giây) làm cả request treo, một nguyên nhân "app chậm" rất hay bị bỏ sót vì người ta nhìn vào server thay vì bước phân giải.

Đánh đổi cần cân nhắc

Cache DNS đổi độ tươi lấy tốc độ — TTL là nút điều chỉnh. TTL cao nghĩa là cache lâu, ít truy vấn DNS (nhanh) nhưng chậm cập nhật khi IP đổi (ví dụ khi failover sang server khác). TTL thấp cho phản ứng nhanh với thay đổi nhưng tăng số truy vấn DNS. Đây là đánh đổi thật khi vận hành: dịch vụ hay đổi IP (blue-green, autoscaling) cần TTL thấp; dịch vụ ổn định thì TTL cao tiết kiệm.

Ứng dụng chạy lâu nên cân nhắc cache DNS trong tiến trình — nhưng cẩn thận TTL. Nhiều runtime (kể cả Go mặc định) không cache DNS trong tiến trình — mỗi lần dial tới host mới lại hỏi resolver OS. Với dịch vụ gọi cùng host liên tục, thêm một lớp cache DNS trong ứng dụng giảm chi phí. Nhưng cache quá lâu mà bỏ qua TTL sẽ giữ IP cũ sau khi server đã đổi — một lỗi kinh điển khiến traffic vẫn đâm vào IP đã chết. Tôn trọng TTL.

DNS timeout phải có giới hạn và cần được theo dõi. Vì DNS đi trước mọi thứ, một resolver chậm hay chết làm mọi request tới host mới treo. Đặt timeout cho phân giải (Go: net.Resolver với context có deadline), và đưa thời gian DNS vào giám sát (httptrace DNSStart/DNSDone). Khi "app chậm" mà server và mạng đều ổn, DNS là nơi cần nhìn tới — nó vô hình nên hay bị quên.

Ba ý mang về

  1. DNS là bước ẩn trước bắt tay TCP: đo thật cold 6,97ms (đi mạng qua resolver đệ quy), cached 794µs (~9 lần nhanh hơn nhờ cache theo TTL); mỗi kết nối tới host mới đều bắt đầu bằng bước này.
  2. Cold biến thiên lớn và /etc/hosts thì tức thì: đo thật cold từ 6ms tới 140ms tùy resolver/authoritative, còn localhost qua /etc/hosts chỉ 1µs không đi mạng — cache + TTL quyết định nhanh/chậm.
  3. DNS chậm/timeout là nguyên nhân "app chậm" hay bị bỏ sót: đo thật khi cached DNS chỉ chiếm 1,2% request, nhưng cold hay timeout sẽ là phần lớn; đặt timeout, tôn trọng TTL, và đưa thời gian DNS vào giám sát.

Nguồn

Phần sau ta quay lại đọc trạng thái kết nối: các trạng thái socket qua /proc/net/tcp và bẫy TIME_WAIT — vì sao đóng nhiều kết nối để lại hàng nghìn socket TIME_WAIT (như đã thấy thoáng qua ở bài 1) và khi nào điều đó thành vấn đề.