Một mặc định luôn được chỉnh cho một kiểu dùng nào đó — và cái kiểu dùng ngược lại thì âm thầm trả giá. Cấu hình DNS mặc định của Kubernetes được tối ưu cho tên nội bộ cụm, nên mỗi lần gọi ra một dịch vụ ngoài cụm, bộ phân giải lại đoán trượt ba lần trước khi đúng. Phần trước đo được ClusterIP chỉ là luật iptables; bài này đo bước trước đó: cái tên web-svc biến thành địa chỉ IP bằng cách nào, và vì sao nó tốn hơn bạn nghĩ.
resolv.conf trong mọi pod
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
nameserver trỏ tới CoreDNS. search là ba hậu tố sẽ được thử. Và ndots:5 là dòng gây tốn kém.
ndots:5 nghĩa là: tên có ít hơn 5 dấu chấm thì thử lần lượt cả ba hậu tố trước, rồi mới thử chính cái tên đó. Với web-svc, đó là điều bạn muốn — web-svc.default.svc.cluster.local khớp ngay ở lần thử đầu. Với api.doitac.com — hai dấu chấm, ít hơn 5 — bộ phân giải thử:
api.doitac.com.default.svc.cluster.local NXDOMAIN
api.doitac.com.svc.cluster.local NXDOMAIN
api.doitac.com.cluster.local NXDOMAIN
api.doitac.com tìm thấy
Ba lần hỏng trước một lần đúng.
Đếm số truy vấn thật
Đọc coredns_dns_requests_total trước và sau 20 lần phân giải:
| Truy vấn | Mỗi lần phân giải | |
|---|---|---|
google.com |
74 | 3,7 |
google.com. |
26 | 1,3 |
Khác biệt duy nhất: một dấu chấm ở cuối. Dấu chấm cuối nói với bộ phân giải rằng đây là tên tuyệt đối — đừng thử hậu tố nào cả. Kết quả là 2,8 lần ít truy vấn hơn, cho mọi tên miền ngoài cụm. Con số 1,3 thay vì 1,0 là do bộ phân giải hỏi cả A lẫn AAAA.
Độ trễ tương ứng
50 lần phân giải mỗi tên:
| Tên | Độ trễ |
|---|---|
web-svc |
0,49 ms |
web-svc.default |
0,22 ms |
web-svc.default.svc.cluster.local |
0,36 ms |
web-svc.default.svc.cluster.local. |
0,11 ms |
google.com |
0,44 ms |
google.com. |
0,10 ms |
Tên ngoài cụm với dấu chấm cuối nhanh gấp 4,4 lần. Tên nội bộ ngắn vẫn khá rẻ, vì hậu tố đầu tiên đã khớp — chỉ một truy vấn hỏng trước đó là không có. Tên ngoài cụm mới là chỗ trả giá, vì nó phải trượt cả ba hậu tố.
Nửa mili giây nghe nhỏ. Nhân với mỗi lời gọi HTTP tới dịch vụ bên ngoài, trong một hệ thống gọi hàng nghìn lần mỗi giây, và nó thành tải thật lên CoreDNS — vốn chỉ có hai bản mặc định.
Ba cách chữa
Thêm dấu chấm cuối cho mọi tên ngoài cụm trong cấu hình:
API_URL=https://api.doitac.com./v1
Rẻ nhất, và không cần đụng vào cụm. Nhược điểm: trông lạ, và một số thư viện xử lý không đúng.
Hạ ndots cho pod gọi nhiều ra ngoài:
dnsConfig:
options:
- {name: ndots, value: "2"}
Khi ấy api.doitac.com (2 dấu chấm) được thử nguyên dạng trước. Đổi lại, web-svc (0 dấu chấm) vẫn dùng search list bình thường, nên dịch vụ nội bộ không ảnh hưởng.
NodeLocal DNSCache cho cụm lớn — một bộ đệm DNS chạy trên mỗi node, nên truy vấn không phải đi qua mạng tới CoreDNS.
Bốn dạng tên trong cụm
web-svc cùng namespace
web-svc.default khác namespace
web-svc.default.svc.cluster.local đầy đủ
db-0.db-svc.default.svc.cluster.local một pod cụ thể (headless)
Dạng cuối chỉ có với Headless Service (phần 56). Đó là cách StatefulSet cho mỗi bản một tên DNS ổn định. Ngoài ra còn bản ghi SRV cho cổng đã đặt tên:
_http._tcp.web-svc.default.svc.cluster.local
CoreDNS chỉ có hai bản
kubectl -n kube-system get deployment coredns
Mặc định là 2 bản cho cả cụm, bất kể cụm có 3 node hay 300. Với cụm lớn và ứng dụng gọi DNS nhiều, đó là nút thắt thật. Ba dấu hiệu CoreDNS đang quá tải:
coredns_dns_request_duration_secondsp99 tăng- lỗi
i/o timeoutkhi phân giải trong log ứng dụng - pod CoreDNS bị
OOMKilled(phần 54)
Cách xử lý theo thứ tự: sửa ndots ở phía ứng dụng trước, rồi mới tăng số bản CoreDNS, rồi mới tới NodeLocal DNSCache.
Cái bẫy khi đo
dig web-svc trả về NXDOMAIN, còn ứng dụng gọi web-svc thì chạy tốt. Không mâu thuẫn: dig không dùng danh sách search — nó hỏi đúng cái tên bạn gõ. Bộ phân giải của hệ điều hành (getaddrinfo) thì có dùng.
Nên khi gỡ lỗi DNS trong Kubernetes, dig cho bạn câu trả lời của máy chủ DNS, không phải câu trả lời ứng dụng sẽ nhận được. Muốn cái sau thì dùng getent hosts hoặc nslookup, hoặc một đoạn mã gọi thẳng getaddrinfo. Tôi vấp đúng chuyện này khi dựng phép đo cho bài: dig cho NXDOMAIN trong khi mọi thứ vẫn chạy, và mất một lúc mới nhận ra công cụ đang trả lời một câu hỏi khác.
Mẫu số chung: mặc định tối ưu một mẫu sẽ đánh thuế mẫu kia, và công cụ chẩn đoán trả lời câu hỏi của riêng nó
Bài này gói hai bài học vượt khỏi DNS. Thứ nhất: mọi mặc định đều mã hoá một kiểu dùng giả định, và khối lượng việc đi ngược giả định đó sẽ âm thầm trả giá. ndots:5 chỉnh cho tên nội bộ (khớp ngay hậu tố đầu), nên tên ngoài cụm — kiểu dùng ngược lại — phải trượt ba lần; dấu chấm cuối là cái nút thoát tường minh nói "đây là tuyệt đối, thôi đoán". Cùng hình dạng khắp nơi: một index tăng tốc SELECT nhưng làm chậm INSERT; một GC chỉnh cho thông lượng thì hại độ trễ; một cache định cỡ cho đọc thì hại ghi; một mặc định vừa 90% ca lặng lẽ tính phí lên 10% còn lại. Biết mặc định của mình đang giả định kiểu dùng nào, rồi để mắt đúng vào cái ca vi phạm giả định đó — vì nó không tự kêu, nó chỉ chậm đi.
Thứ hai: một công cụ chẩn đoán trả lời đúng câu hỏi của riêng nó, mà câu đó có thể không phải câu bạn đang hỏi. dig hỏi thẳng máy chủ DNS (bỏ qua search list); getaddrinfo mới là thứ ứng dụng thật sự đi qua — dùng nhầm cái đầu để chẩn đoán cái sau cho ra một câu trả lời tự tin mà sai. Cùng cái bẫy với curl từ máy bạn thay vì từ trong pod (khác đường mạng), với ping (ICMP) khi ứng dụng dùng TCP, với một test MockMvc không đi qua bộ phân tích multipart thật, với EXPLAIN chạy tham số khác production. Nguyên tắc: cho phép thử đi qua đúng con đường mà thứ thật đi, nếu không nó đang trả lời một câu hỏi khác — và một câu trả lời cho câu hỏi khác nguy hiểm hơn không có câu trả lời nào.
Xen giữa hai bài học là một lưu ý quen thuộc: một chi phí nhỏ trên mỗi lần gọi, nhân với tần suất cao, thành một khoản tải thật — nửa mili giây mỗi lần phân giải là vô hình cho tới khi bạn gọi hàng nghìn lần mỗi giây.
Mẫu số chung: hỏi mặc định của bạn tối ưu cho kiểu dùng nào rồi canh chừng kiểu ngược lại; và khi chẩn đoán, chắc rằng công cụ của bạn đi đúng con đường của thứ thật, chứ không phải một con đường nghe-thì-giống.
Muốn tự đo cái giá của ndots trên cụm của bạn:
kubectl run t --image=python:3.12-alpine --restart=Never --rm -it -- python3 -c "
import socket, time
for n in ['api.github.com','api.github.com.']:
t=time.time()
for _ in range(50): socket.getaddrinfo(n,443)
print(f'{n:20} {(time.time()-t)/50*1000:6.2f} ms')
"
Nếu dòng có dấu chấm nhanh hơn nhiều lần, mọi lời gọi ra ngoài của bạn đang trả đúng cái giá đó.
Phần sau đo Ingress: cùng một yêu cầu, đi thẳng vào Service hay qua bộ điều khiển Ingress, chênh nhau bao nhiêu.