Service mới deploy lên staging, health check fail liên tục. Log ghi ENOTFOUND db-primary.internal. Database chạy bình thường, port mở, telnet từ pod khác kết nối được. Nguyên nhân: DNS record cho db-primary.internal chưa được tạo ở namespace mới — service resolve ra NXDOMAIN. Lỗi không phải network, không phải database — lỗi ở DNS, thứ ít ai nghĩ tới đầu tiên nhưng là nguyên nhân gốc của rất nhiều “connection timeout” và “host not found” trên production.

Bài này đi qua cách DNS resolve hoạt động, các lỗi phổ biến, cách debug bằng dignslookup, và đặc biệt là DNS trong Kubernetes — nơi thêm nhiều lớp phức tạp với CoreDNS, search domain, và ndots.


DNS resolve hoạt động thế nào

Khi ứng dụng gọi fetch("https://api.example.com/users"), bước đầu tiên không phải là mở TCP connection — mà là hỏi DNS “api.example.com có IP nào?”. Nếu bước này chậm hoặc sai, mọi thứ phía sau đều chậm hoặc lỗi theo.

Quá trình resolve diễn ra qua nhiều tầng cache, và hiểu các tầng này là chìa khoá để debug nhanh.

Tầng 1 — cache local trên máy (hoặc pod). OS giữ cache DNS trong bộ nhớ. Trên Linux, nscd hoặc systemd-resolved quản lý cache này. Trên macOS, mDNSResponder làm việc tương tự. Nếu domain đã được resolve gần đây và TTL chưa hết, OS trả kết quả ngay — không gọi ra ngoài. Đây là tầng nhanh nhất nhưng cũng là nơi stale cache hay gây rắc rối.

Tầng 2 — recursive resolver. Nếu cache local không có, OS gửi query tới recursive resolver — thường là DNS server của ISP, hoặc public resolver như 8.8.8.8 (Google), 1.1.1.1 (Cloudflare), hoặc DNS server nội bộ công ty. Recursive resolver cũng có cache riêng: nếu ai đó đã hỏi cùng domain trước đó và TTL chưa hết, resolver trả kết quả từ cache mà không hỏi tiếp.

Tầng 3 — authoritative nameserver. Nếu recursive resolver cũng không có cache, nó bắt đầu quá trình resolution đầy đủ: hỏi root nameserver (.com, .org, .vn) lấy nameserver cho TLD, rồi hỏi TLD nameserver lấy authoritative nameserver cho domain cụ thể, rồi hỏi authoritative nameserver lấy IP. Quá trình này có thể tốn 50-200ms tuỳ vị trí và latency mạng.

Điểm quan trọng mà nhiều dev bỏ qua: mỗi DNS record có TTL (Time To Live) — con số tính bằng giây, nói cho cache giữ kết quả bao lâu. TTL 300 nghĩa là sau 5 phút, cache hết hạn, lần query tiếp phải hỏi lại upstream. TTL thấp (30-60 giây) cho phép thay đổi nhanh nhưng tốn query. TTL cao (3600+ giây) giảm query nhưng khi bạn đổi IP, phải chờ lâu mới propagate hết.


Các loại record cần biết

Không cần nhớ tất cả loại DNS record — chỉ cần nắm vài loại mà dev hay gặp.

A record map domain sang IPv4 address. Đây là loại phổ biến nhất — api.example.com → 203.0.113.42. AAAA record tương tự nhưng cho IPv6.

CNAME record là alias — www.example.com → example.com. CNAME nói “đừng hỏi tôi IP, hãy đi hỏi domain kia”. Lưu ý quan trọng: CNAME không được đặt ở apex domain (root domain, ví dụ example.com), chỉ dùng cho subdomain. Nếu bạn cần alias ở apex domain, dùng ALIAS hoặc ANAME record (tuỳ DNS provider hỗ trợ).

SRV record chứa host + port, hay dùng trong service discovery nội bộ — Kubernetes dùng SRV record để client biết service chạy ở port nào.

TXT record chứa text tuỳ ý — hay dùng cho SPF/DKIM (email authentication), domain verification (Google Search Console, Let’s Encrypt DNS-01 challenge). Mình từng mất 2 giờ debug vì TXT record cho Let’s Encrypt DNS-01 challenge bị cắt ngắn do vượt 255 ký tự — phải chia thành nhiều string nối lại.

NS record chỉ ra nameserver nào authoritative cho domain. Khi bạn chuyển domain từ provider này sang provider khác (ví dụ Route53 sang Cloudflare), bạn đổi NS record ở registrar.


Lỗi DNS phổ biến và cách debug

NXDOMAIN — domain không tồn tại

NXDOMAIN nghĩa là authoritative nameserver trả lời rằng domain không có record nào. Nguyên nhân thường gặp: gõ sai tên domain, DNS record chưa được tạo, hoặc domain hết hạn.

Debug bước đầu tiên luôn là dig:

dig api.example.com +short
# Nếu không có output → không có A record
dig api.example.com ANY +noall +answer
# Xem tất cả record của domain

Nếu dig cũng trả NXDOMAIN, kiểm tra ở authoritative nameserver trực tiếp để loại trừ cache:

dig api.example.com @ns1.example.com
# Hỏi trực tiếp authoritative NS, bypass mọi cache

Mình hay gặp NXDOMAIN trong Kubernetes khi service name bị sai namespace — db-primary resolve được trong cùng namespace, nhưng từ namespace khác phải dùng db-primary.other-namespace.svc.cluster.local.

Stale cache — record cũ vẫn sống

Bạn đổi IP của server, cập nhật A record, nhưng 30 phút sau một số user vẫn trỏ vào IP cũ. Đây là stale cache — cache ở resolver hoặc ở client vẫn giữ giá trị cũ vì TTL chưa hết.

Cách kiểm tra TTL còn lại:

dig api.example.com +noall +answer
# api.example.com.    247    IN    A    203.0.113.42
# 247 là số giây TTL còn lại

Nếu TTL ban đầu là 3600 (1 giờ) mà bạn cần đổi IP nhanh, bạn phải chờ tối đa 1 giờ. Chiến lược tốt hơn: hạ TTL trước khi migration. Một ngày trước khi đổi IP, hạ TTL xuống 60 giây. Chờ TTL cũ hết (1 giờ), lúc này mọi cache đã refresh với TTL mới 60 giây. Đổi IP — trong vòng 60 giây mọi người sẽ thấy IP mới. Sau khi migration ổn định, tăng TTL trở lại.

Trên máy local, flush DNS cache nếu cần test nhanh:

# macOS
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
# Linux (systemd-resolved)
sudo systemd-resolve --flush-caches
# Windows
ipconfig /flushdns

Split-horizon DNS — cùng domain khác IP tuỳ nơi hỏi

Split-horizon (hay split-brain DNS) là khi cùng một domain trả về IP khác nhau tuỳ vào ai đang hỏi. Ví dụ: api.example.com trả 10.0.0.5 (IP nội bộ) khi hỏi từ trong VPN, nhưng trả 203.0.113.42 (IP public) khi hỏi từ internet.

Đây là thiết kế có chủ đích — traffic nội bộ đi thẳng qua private network, nhanh hơn và an toàn hơn. Nhưng nó gây nhầm lẫn khi debug: bạn thử từ laptop (qua VPN) thì OK, nhưng service trên server (không qua VPN) lại resolve ra IP khác.

Debug bằng cách chỉ định resolver cụ thể:

dig api.example.com @8.8.8.8       # Hỏi Google DNS (public view)
dig api.example.com @10.0.0.2      # Hỏi internal DNS (private view)

Nếu kết quả khác nhau, bạn đang ở môi trường split-horizon. Tôi từng mất 1 giờ debug vì service trong Kubernetes resolve ra IP public (đi vòng qua internet rồi về) thay vì IP private — chỉ vì CoreDNS forward query ra public resolver cho domain nội bộ. Fix bằng cách thêm stub domain trong CoreDNS config.

Timeout DNS — chậm nhưng không lỗi

DNS query thường mất 1-50ms. Nếu mất hơn 500ms, có vấn đề. DNS timeout thường do: resolver không phản hồi (network issue), firewall block port 53 UDP/TCP, hoặc resolver quá tải.

dig api.example.com +stats
# ;; Query time: 2345 msec  ← quá chậm

Thử đổi resolver để isolate:

dig api.example.com @1.1.1.1 +stats
# Nếu Cloudflare DNS nhanh nhưng internal DNS chậm → vấn đề ở internal resolver

Mình từng gặp trường hợp DNS timeout liên tục trong Kubernetes — hoá ra CoreDNS pod bị OOM kill vì resource limit quá thấp, restart liên tục, trong khoảng restart thì DNS query timeout. Tăng memory limit cho CoreDNS deployment fix ngay.


DNS trong Kubernetes — đặc biệt phức tạp

DNS trong Kubernetes là nguồn gốc của rất nhiều bug khó debug vì nó có thêm nhiều lớp so với DNS thông thường. Hiểu cách Kubernetes resolve DNS sẽ giúp bạn tiết kiệm hàng giờ debug.

CoreDNS và service discovery

Mỗi Kubernetes cluster chạy CoreDNS (hoặc kube-dns cũ) như DNS server cho cluster. Khi pod cần resolve tên service, query đi tới CoreDNS. CoreDNS biết tất cả Service và Endpoint trong cluster, trả IP ClusterIP hoặc pod IP tuỳ loại Service.

Tên đầy đủ (FQDN) của một Service trong Kubernetes: <service>.<namespace>.svc.cluster.local. Ví dụ: postgres.database.svc.cluster.local.

Nhưng bạn không cần dùng FQDN mọi lúc. Kubernetes inject search domain vào /etc/resolv.conf của mỗi pod:

search default.svc.cluster.local svc.cluster.local cluster.local
ndots: 5

Dòng search nói rằng khi bạn query postgres, Kubernetes sẽ thử lần lượt: postgres.default.svc.cluster.local, postgres.svc.cluster.local, postgres.cluster.local, rồi cuối cùng postgres (query gốc). Dừng ở kết quả đầu tiên tìm được.

ndots — cạm bẫy performance

ndots: 5 là cài đặt mặc định trong Kubernetes, và nó có ảnh hưởng lớn đến hiệu năng DNS mà ít ai để ý. Quy tắc: nếu domain có ít hơn 5 dấu chấm, Kubernetes sẽ thử tất cả search domain trước khi thử domain gốc.

api.example.com có 2 dấu chấm — ít hơn 5 — nên Kubernetes sẽ thử: api.example.com.default.svc.cluster.local (NXDOMAIN), api.example.com.svc.cluster.local (NXDOMAIN), api.example.com.cluster.local (NXDOMAIN), rồi cuối cùng api.example.com (thành công). Mỗi domain bên ngoài cluster tốn 4 DNS query thay vì 1. Nhân lên cho mọi HTTP request ra ngoài, DNS query tăng gấp 4 lần.

Hai cách giải quyết. Cách thứ nhất: thêm dấu chấm cuối (trailing dot) — api.example.com. — nói cho resolver rằng đây là FQDN, không cần thử search domain. Cách thứ hai: giảm ndots xuống 2 hoặc 3 trong pod spec:

spec:
  dnsConfig:
    options:
      - name: ndots
        value: "2"

Mình đổi ndots: 2 cho tất cả pod gọi external API, DNS query giảm 60%, CoreDNS load giảm đáng kể. Nhưng cẩn thận: nếu giảm ndots quá thấp mà service name nội bộ có dấu chấm (ví dụ headless service), có thể resolve sai.

Headless Service vs ClusterIP Service

ClusterIP Service có một virtual IP duy nhất — DNS trả về IP đó, kube-proxy route traffic đến pod phía sau. Headless Service (clusterIP: None) thì khác — DNS trả về tất cả pod IP trực tiếp. Client tự chọn pod nào kết nối.

Headless Service hữu ích khi client cần biết tất cả pod — ví dụ StatefulSet (Kafka broker, PostgreSQL replica), hoặc khi client muốn tự implement load balancing. Nhưng nó cũng nghĩa là khi pod thay đổi (scale up/down, restart), DNS response thay đổi theo — client cần handle DNS change, không cache IP cứng.

ExternalName Service — CNAME trong Kubernetes

ExternalName Service tạo CNAME record trong CoreDNS — my-db.default.svc.cluster.local → rds-instance.abc123.us-east-1.rds.amazonaws.com. Pod gọi my-db thì DNS resolve sang RDS instance bên ngoài cluster.

Tiện cho việc abstract external dependency — đổi RDS instance chỉ cần đổi Service manifest, không sửa code. Nhưng cẩn thận: ExternalName Service không proxy traffic, nó chỉ trả CNAME. Nếu external endpoint cần TLS với hostname cụ thể, certificate phải match domain gốc chứ không phải tên Service.


Debug DNS bằng dig và nslookup

dig là công cụ mình dùng nhiều nhất khi debug DNS. nslookup đơn giản hơn nhưng output ít chi tiết hơn. Trong container Kubernetes, cả hai có thể không có sẵn — cài dnsutils hoặc dùng busybox:latest image.

Các lệnh dig hay dùng

# Resolve cơ bản
dig api.example.com +short

# Xem đầy đủ response (TTL, authority, additional section)
dig api.example.com

# Hỏi resolver cụ thể
dig api.example.com @8.8.8.8

# Trace toàn bộ resolution path (từ root → TLD → authoritative)
dig api.example.com +trace

# Xem record cụ thể
dig api.example.com MX
dig api.example.com TXT
dig api.example.com NS

# Reverse DNS (IP → domain)
dig -x 203.0.113.42

+trace đặc biệt hữu ích khi bạn nghi resolver đang cache sai — nó đi từ root nameserver xuống, bypass hoàn toàn cache.

Debug DNS trong Kubernetes pod

Nếu pod không resolve được service, chạy debug pod:

kubectl run dns-debug --image=busybox:1.36 --rm -it -- sh
# Trong pod:
nslookup postgres.database.svc.cluster.local
nslookup kubernetes.default
cat /etc/resolv.conf

Kiểm tra /etc/resolv.conf trong pod — đây là nơi search domain và nameserver được cấu hình. Nếu nameserver trỏ sai hoặc search domain thiếu namespace, resolve sẽ thất bại.

Kiểm tra CoreDNS có đang chạy không:

kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50

Mình từng debug 30 phút vì CoreDNS log ghi SERVFAIL cho domain nội bộ — hoá ra ConfigMap coredns bị sửa sai syntax ở forward plugin, CoreDNS không forward được query ra upstream resolver.


DNS và incident response

Khi incident xảy ra và bạn nghi liên quan DNS, checklist debug nhanh mà mình theo:

Bước 1: từ máy hoặc pod bị lỗi, chạy dig <domain> +short. Có IP không? IP đúng không? Bước 2: nếu không có IP hoặc IP sai, thử hỏi resolver khác — dig <domain> @8.8.8.8. Nếu resolver khác cho kết quả đúng, vấn đề ở resolver hiện tại (cache stale, config sai). Bước 3: kiểm tra TTL — dig <domain> +noall +answer. TTL còn bao lâu? Có đang cache kết quả cũ không? Bước 4: nếu dùng Kubernetes, kiểm tra /etc/resolv.conf trong pod, xác nhận nameserver và search domain đúng. Bước 5: kiểm tra CoreDNS pod (hoặc kube-dns) có healthy không, xem log có error gì.

Hầu hết incident DNS mình gặp đều rơi vào ba nhóm: stale cache (đợi TTL hết hoặc flush cache), config sai (resolver address, search domain, ndots), hoặc DNS server down (CoreDNS OOM, external resolver unreachable). Hiểu ba nhóm này đủ để xử lý 90% trường hợp.


TTL strategy cho production

Chọn TTL không phải “set rồi quên” — nó ảnh hưởng trực tiếp đến tốc độ failover và lượng DNS query.

TTL thấp (30-60 giây) cho phép failover nhanh — đổi IP thì tối đa 1 phút mọi người thấy IP mới. Nhưng DNS query tăng đáng kể vì cache hết hạn liên tục. Với service có SLO failover chặt (< 5 phút), TTL thấp là cần thiết.

TTL cao (1-24 giờ) giảm DNS query, giảm load lên resolver và authoritative server. Nhưng failover chậm — đổi IP rồi phải chờ hàng giờ. Hợp cho service ổn định, ít thay đổi IP (CDN endpoint, email MX record).

TTL trung bình (300-600 giây, tức 5-10 phút) là lựa chọn cân bằng mà mình dùng cho hầu hết service. Đủ nhanh để failover trong khoảng chấp nhận được, không quá nhiều DNS query.

Một pattern mà mình hay dùng cho migration: giảm TTL 24-48 giờ trước migration → thực hiện migration → verify → tăng TTL trở lại. Đơn giản nhưng nhiều team quên bước “giảm TTL trước”, rồi đổi IP xong phải chờ hàng giờ mới propagate hết.


Anti-pattern DNS hay gặp

Cache DNS kết quả ở application layer. Một số HTTP client hoặc connection pool cache IP đã resolve và không bao giờ re-resolve. Khi IP thay đổi (failover, scaling), app vẫn gọi IP cũ. Java InetAddress mặc định cache DNS vĩnh viễn trong JVM — phải set networkaddress.cache.ttl để có TTL hợp lý. Node.js http.Agent cũng giữ socket mở dùng IP cũ — cần handle DNS change khi dùng long-lived connection.

Hardcode IP thay vì dùng DNS. “DNS chậm nên tôi hardcode IP cho nhanh” — rồi khi IP đổi phải redeploy. DNS có cache, overhead không đáng kể so với lợi ích linh hoạt. Chỉ hardcode IP khi có lý do kỹ thuật rất cụ thể (bootstrap DNS server chính nó).

Quên trailing dot trong FQDN. Trong Kubernetes, api.example.com bị thử với search domain trước (4 query thừa). Thêm dấu chấm cuối api.example.com. hoặc giảm ndots để tránh query thừa.

Không monitor DNS. DNS timeout hay resolution failure ít khi có metric riêng — nó thường hiện dưới dạng “connection timeout” hoặc “host not found” trong application log. Thêm metric cho DNS resolution time nếu app phụ thuộc nhiều external service — phát hiện sớm khi DNS chậm trước khi thành incident.


Tóm tắt

DNS resolve qua ba tầng cache: local → recursive resolver → authoritative nameserver. Mỗi tầng có TTL riêng, và stale cache ở bất kỳ tầng nào đều gây rắc rối khi IP thay đổi. Giảm TTL trước migration, flush cache khi cần test nhanh.

Trong Kubernetes, CoreDNS thêm lớp phức tạp: search domain, ndots, headless service, ExternalName. ndots: 5 mặc định gây 4 query thừa cho mỗi external domain — giảm ndots hoặc dùng trailing dot cho domain bên ngoài cluster.

Debug DNS bằng dig — kiểm tra IP, TTL, hỏi resolver khác để isolate, dùng +trace để bypass cache. Trong Kubernetes, kiểm tra /etc/resolv.conf trong pod và CoreDNS log là hai bước đầu tiên.

DNS ít khi là thứ bạn nghĩ tới đầu tiên khi incident xảy ra — nhưng nó thường là nguyên nhân gốc của “connection timeout”, “host not found”, và “intermittent failure”. Hiểu DNS đủ sâu để debug trong 5 phút thay vì 45 phút — đó là kỹ năng mà mọi backend engineer cần.