Phần 27 liệt kê tra DNS là chặng đầu tiên của một yêu cầu HTTPS và ghi "0 hoặc 1 vòng khứ hồi". Bài này đo, và tìm thấy một con số lớn hơn thế rất nhiều.

Chi phí tra tên theo cấu hình

Chi phí cơ bản

getaddrinfo, 200 lần, đo trung vị:

Tra cái gì Trung vị So với địa chỉ IP
Địa chỉ IP (không tra tên) 2,2 µs
Tên nội bộ, cấu hình bình thường 88,0 µs 40×
Tên ngoài (example.com) 982,6 µs 447×
Tên không tồn tại 2.302,5 µs 1.046×

Hai điều đáng nhớ ngay từ bảng này.

Tra một tên đắt gấp 40 lần dùng thẳng địa chỉ, và đó là trường hợp tốt nhất — máy chủ DNS nằm ngay trên máy, câu trả lời đã có trong bộ đệm.

Tra một tên sai đắt gấp 26 lần tra một tên đúng. Một lỗi chính tả trong tên máy chủ, hoặc một dịch vụ đã bị xoá mà mã nguồn vẫn gọi tới, không cho lỗi ngay — nó cho một khoảng chờ vài mili giây trên mọi yêu cầu.

Rồi cấu hình mặc định của Kubernetes

Chạy lại đúng chương trình đó trong một container có 5 miền tìm kiếm và ndots:5 — đúng cấu hình mà mọi pod Kubernetes nhận được:

search a.svc.cluster.local svc.cluster.local cluster.local vi.du.mot vi.du.hai
options ndots:5

Năm lần đo mỗi tên:

Tra cái gì Năm lần đo (µs)
nsrv 24.038.841 / 24.027.243 / 24.022.974 / 24.021.209 / 24.014.459
example.com 24.070.556 / 24.026.533 / 24.016.385 / 24.022.011 / 24.015.375
example.com. 1.224 / 1.199 / 991 / 845 / 1.168

24 giây cho một lần tra tên. Thêm một dấu chấm ở cuối tên: 1,2 mili giây.

Khoảng hai mươi nghìn lần, chỉ bằng một ký tự.

Vì sao dấu chấm đổi được nhiều đến thế

ndots:5 nghĩa là: nếu tên có ít hơn 5 dấu chấm, hãy thử ghép nó với từng miền tìm kiếm trước, rồi mới thử chính nó.

example.com có một dấu chấm. Vậy trình phân giải hỏi theo thứ tự:

example.com.a.svc.cluster.local
example.com.svc.cluster.local
example.com.cluster.local
example.com.vi.du.mot
example.com.vi.du.hai
example.com                      <- cuoi cung moi toi cai dung

Sáu câu hỏi cho một cái tên. Và trong môi trường tôi đo, năm câu đầu không được trả lời — trình phân giải nội bộ không trả NXDOMAIN mà im lặng, nên mỗi câu phải chờ hết hạn rồi thử lại. Cộng lại thành 24 giây.

Dấu chấm cuối biến tên thành tuyệt đối. Trình phân giải hỏi đúng một lần, không ghép gì.

Trong Kubernetes thật, số câu hỏi còn nhân đôi vì getaddrinfo hỏi cả bản ghi A và AAAA. Sáu tên × hai loại = mười hai truy vấn cho một lần kết nối.

Ba cách chữa

Một — dấu chấm cuối. Rẻ nhất, và không cần quyền gì:

DB_HOST=db.example.com.

Cẩn thận: một số thư viện và trình kiểm tra chứng chỉ TLS xử lý dấu chấm cuối không nhất quán. Kiểm tra trước khi đưa vào sản phẩm.

Hai — hạ ndots cho từng pod:

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

ndots:2 giữ được việc gọi dịch vụ nội bộ bằng tên ngắn (api, db — không dấu chấm nào) trong khi tên ngoài (s3.amazonaws.com — hai dấu chấm) đi thẳng.

Ba — bộ đệm DNS tại chỗ. NodeLocal DNSCache trong Kubernetes, hoặc systemd-resolved/dnsmasq trên máy chủ thường. Nó không sửa việc ghép miền tìm kiếm, nhưng làm mỗi câu hỏi hỏng rẻ đi rất nhiều.

Bộ đệm ở đâu — và chỗ không có

Đây là điều nhiều người bất ngờ: glibc không có bộ đệm DNS. Mỗi lần getaddrinfo là một câu hỏi thật ra ngoài, trừ khi:

  • nscd hoặc systemd-resolved đang chạy và được khai trong /etc/nsswitch.conf.
  • Ứng dụng tự cache.

Ngôn ngữ khác nhau thì hành vi khác nhau, và đây là nguồn của rất nhiều bất ngờ:

Môi trường Bộ đệm
C / glibc không, trừ khi có nscd
Go không (bộ phân giải thuần Go cũng không cache)
Node.js không, trừ khi dùng thư viện
Python không
JVM có, và mặc định cũ là cache vĩnh viễn

Dòng cuối là cái bẫy nổi tiếng nhất: một dịch vụ Java tra tên một lần lúc khởi động rồi giữ mãi địa chỉ đó. Khi máy chủ phía sau đổi IP — chuyện xảy ra mỗi lần bộ cân bằng tải hoặc RDS chuyển đổi dự phòng — dịch vụ tiếp tục gọi vào địa chỉ chết cho tới khi được khởi động lại.

# $JAVA_HOME/conf/security/java.security
networkaddress.cache.ttl=30
networkaddress.cache.negative.ttl=0

Giá trị mặc định phụ thuộc vào việc có trình quản lý bảo mật hay không, nên đừng dựa vào mặc định — khai tường minh.

Tra tên chặn cả luồng

getaddrinfo là lời gọi đồng bộ và chặn. Không có bản bất đồng bộ trong thư viện chuẩn POSIX.

Nghĩa là trong một dịch vụ dùng vòng lặp sự kiện — Node.js, Nginx với resolver mặc định, một máy chủ Go xử lý nhiều kết nối trên ít luồng — một lần tra tên chậm giữ luôn luồng đó. Với 24 giây như bảng trên, đó không còn là vấn đề hiệu năng mà là sự cố.

Node.js chạy getaddrinfo trên bể luồng, và bể đó mặc định có bốn luồng:

UV_THREADPOOL_SIZE=16 node app.js

Bốn lần tra tên chậm đồng thời là đủ để chặn luôn cả việc đọc tệp và nén dữ liệu của tiến trình đó.

Thử ba mươi giây

Đo cấu hình DNS của chính bạn:

cat /etc/resolv.conf | grep -vE '^#|^$'

echo "--- tra ten thanh cong ---"
for i in 1 2 3; do
  /usr/bin/time -f '%e s' getent hosts example.com >/dev/null
done 2>&1

echo "--- tra ten hong ---"
for i in 1 2 3; do
  /usr/bin/time -f '%e s' getent hosts khong-co-ten-nay-dau.example >/dev/null
done 2>&1

echo "--- co dau cham cuoi ---"
for i in 1 2 3; do
  /usr/bin/time -f '%e s' getent hosts example.com. >/dev/null
done 2>&1

Nếu dòng options ndots: của bạn lớn hơn 2 và có nhiều dòng search, hãy so hai nhóm cuối. Chênh lệch giữa chúng là thứ mọi kết nối ra ngoài của bạn đang trả, trên mỗi yêu cầu.

Phần sau: TLS — đo chi phí bắt tay và chi phí mã hoá.