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
}

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:

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.comlầ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.comvàwikipedia.orgtốn ~6,4ms, nhưnggolang.orgvọ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ề
- 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.
- 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
localhostqua/etc/hostschỉ 1µs không đi mạng — cache + TTL quyết định nhanh/chậm. - 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
- RFC 1034/1035 — Domain Names (Concepts và Implementation): https://www.rfc-editor.org/rfc/rfc1034
- Go docs — net.Resolver và net/http/httptrace: https://pkg.go.dev/net#Resolver
- Cloudflare — What is DNS? / DNS cache: https://www.cloudflare.com/learning/dns/what-is-dns/
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 đề.