DNS 解析通常在几十毫秒内完成,但这”几十毫秒”的影响远超你的预期——一个页面可能包含 20 多个不同域名的资源,首次加载时每个域名都需要一次完整的 DNS 查询。在高延迟网络中,DNS 解析可以占到页面加载时间的 10%–15%。
更关键的是服务端场景:微服务之间通过域名互相调用,DNS 解析的延迟和可用性直接影响服务的 P99 延迟和故障切换速度。TTL 设太长,变更生效慢;TTL 设太短,递归解析器的缓存命中率低,解析延迟增大。
本文从四个层面系统优化 DNS 性能:浏览器预取减少感知延迟、TTL 策略平衡缓存与时效性、本地 DNS 缓存减少外部依赖、服务端 DNS 预解析降低运行时开销。
一、DNS 延迟的量化分析
1.1 DNS 延迟的组成
一次未缓存的 DNS 解析的延迟可以分解为多个阶段:
客户端 → Stub Resolver → 递归解析器 → 根服务器 → TLD 服务器 → 权威服务器
←───────────── 总延迟 = Σ 每跳 RTT ────────────────────────→
典型延迟组成(从国内解析一个海外域名):
| 阶段 | 延迟 | 说明 |
|---|---|---|
| Stub Resolver 到递归解析器 | 1–5ms | 本地网络 |
| 递归解析器到根服务器 | 5–30ms | 有 Anycast,通常较快 |
| 递归解析器到 TLD 服务器 | 10–50ms | 取决于 TLD 的 Anycast 覆盖 |
| 递归解析器到权威服务器 | 20–200ms | 取决于权威服务器地理位置 |
| 总计(冷查询) | 40–300ms | 三级查询加总 |
| 缓存命中 | 0.1–5ms | 递归解析器本地缓存 |
1.2 量化 DNS 延迟的方法
用 dig 测量单次查询延迟:
# 清除递归解析器缓存影响,直接查询权威服务器
dig @8.8.8.8 example.com +stats +noall +answer
# 查看查询时间
# ;; Query time: 45 msec
# 多次查询对比缓存效果
for i in $(seq 1 5); do
dig @8.8.8.8 example.com +noall +stats 2>&1 | grep "Query time"
done
# 第一次: ;; Query time: 48 msec (冷查询)
# 第二次: ;; Query time: 2 msec (缓存命中)
# 第三次: ;; Query time: 1 msec (缓存命中)用 dnsperf 进行批量性能测试:
# 安装 dnsperf
apt-get install -y dnsperf
# 准备查询文件
cat > queries.txt << 'EOF'
example.com A
google.com A
github.com A
cloudflare.com A
amazon.com A
EOF
# 执行性能测试:10 个并发客户端,持续 30 秒
dnsperf -s 8.8.8.8 -d queries.txt -c 10 -T 30
# 输出示例:
# Statistics:
# Queries sent: 15234
# Queries completed: 15230
# Queries lost: 4
# Response codes: NOERROR 15230
# Average packet size: request 32, response 64
# Run time (s): 30.002
# Queries per second: 507.633
# Average Latency (s): 0.019145
# Latency StdDev (s): 0.0423111.3 DNS 延迟对应用的影响
浏览器页面加载:
一个典型的电商首页引用的域名数量:
- 主站域名: 1 个
- 静态资源 CDN: 2–3 个
- 图片 CDN: 1–2 个
- 第三方分析/广告: 3–5 个
- API 域名: 1–2 个
- 字体 CDN: 1 个
---
总计: 9–14 个不同域名
如果每个域名首次解析需要 50ms:
DNS 总延迟 ≈ 9 × 50ms = 450ms(假设串行解析)
实际(浏览器并行解析) ≈ 150–250ms
服务端微服务调用:
一个 API 请求的调用链:
API Gateway → User Service → Order Service → Payment Service → Notification Service
每一跳通过域名调用:
- user-service.internal: DNS 解析 5ms
- order-service.internal: DNS 解析 5ms
- payment-service.internal: DNS 解析 5ms
- notify-service.internal: DNS 解析 5ms
---
DNS 累积延迟: 20ms(在总延迟中占 10%–20%)
如果 DNS 缓存失效或递归解析器过载:
每跳延迟可能飙升到 200ms+
二、浏览器 DNS 预取(Prefetch)
2.1 DNS Prefetch 的原理
DNS Prefetch 让浏览器在用户点击链接之前提前解析目标域名。当用户实际点击时,DNS 解析已经完成,TCP 连接可以立即开始。
传统流程:
用户点击 → DNS 解析(50ms) → TCP 握手(30ms) → TLS 握手(30ms) → 请求
总延迟: 110ms 才开始传输数据
使用 DNS Prefetch:
页面加载时 → 后台 DNS 预解析(50ms)
... 用户阅读页面 ...
用户点击 → TCP 握手(30ms) → TLS 握手(30ms) → 请求
总延迟: 60ms 就开始传输数据(节省 50ms)
2.2 HTML 中的 DNS Prefetch
在 HTML <head> 中添加
dns-prefetch 提示:
<!DOCTYPE html>
<html>
<head>
<!-- DNS Prefetch: 提前解析第三方域名 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="dns-prefetch" href="//api.example.com">
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//analytics.example.com">
<!-- Preconnect: DNS + TCP + TLS 全部提前完成 -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="preconnect" href="https://api.example.com" crossorigin>
</head>
</html>dns-prefetch 与 preconnect
的区别:
| 特性 | dns-prefetch | preconnect |
|---|---|---|
| DNS 解析 | ✓ | ✓ |
| TCP 握手 | ✗ | ✓ |
| TLS 握手 | ✗ | ✓ |
| 资源消耗 | 极低 | 中等(占用连接) |
| 适用场景 | 可能用到的域名 | 确定要用的域名 |
| 浏览器支持 | 全部主流浏览器 | 全部主流浏览器 |
| 建议数量 | 不限 | ≤ 6 个(连接有成本) |
2.3 浏览器的自动 Prefetch
现代浏览器会自动进行 DNS Prefetch:
自动 Prefetch 行为:
1. 页面中所有 <a href> 的域名
2. 页面中所有 <img src> 的域名
3. CSS 中 @import 和 url() 引用的域名
可以通过 HTTP 头或 meta 标签控制:
<!-- 禁用自动 DNS Prefetch -->
<meta http-equiv="x-dns-prefetch-control" content="off">
<!-- 启用自动 DNS Prefetch(默认在 HTTPS 页面中关闭) -->
<meta http-equiv="x-dns-prefetch-control" content="on">注意:HTTPS 页面默认关闭自动 DNS Prefetch(出于隐私考虑——通过 DNS 请求可以推断用户将要访问的链接)。如果需要自动 Prefetch,需要显式启用。
2.4 通过 HTTP 响应头控制 Prefetch
# Nginx 配置: 通过 Link 头发送 Prefetch 提示
server {
location / {
add_header Link "</api>; rel=preconnect; crossorigin";
add_header Link "<//cdn.example.com>; rel=dns-prefetch";
}
}
三、服务端 DNS 预解析
3.1 应用层 DNS 缓存的必要性
服务端应用在运行时频繁通过域名调用外部服务,每次调用都可能触发 DNS 解析。虽然操作系统和递归解析器有缓存,但在以下场景中缓存可能失效:
- 高频调用导致缓存不稳定
- TTL 过短导致频繁重新解析
- 递归解析器过载导致延迟飙升
- DNS 服务故障导致解析超时
3.2 Java 的 DNS 缓存行为
Java 的 DNS 行为由 InetAddress
的缓存策略控制:
import java.net.InetAddress;
import java.security.Security;
public class DnsCacheConfig {
public static void configureDnsCache() {
// JVM 默认: 正向缓存永不过期(在 SecurityManager 存在时)
// 推荐: 设置合理的缓存时间
Security.setProperty("networkaddress.cache.ttl", "30");
// 否定缓存(NXDOMAIN): 默认 10 秒
// 设为 0 可以在域名恢复后立即生效
Security.setProperty("networkaddress.cache.negative.ttl", "0");
}
// 手动预解析并缓存
public static void prefetchDns(String[] hostnames) {
for (String hostname : hostnames) {
try {
long start = System.nanoTime();
InetAddress[] addresses = InetAddress.getAllByName(hostname);
long elapsed = (System.nanoTime() - start) / 1_000_000;
System.out.printf("Resolved %s -> %s (%d ms)%n",
hostname, addresses[0].getHostAddress(), elapsed);
} catch (Exception e) {
System.err.printf("Failed to resolve %s: %s%n",
hostname, e.getMessage());
}
}
}
public static void main(String[] args) {
configureDnsCache();
String[] hosts = {
"user-service.internal",
"order-service.internal",
"payment-service.internal",
"redis.internal",
"mysql-primary.internal"
};
// 启动时预解析所有依赖服务的域名
prefetchDns(hosts);
}
}3.3 Go 的 DNS 解析行为
Go 的 net 包默认使用纯 Go 实现的 DNS
解析器,不经过 libc:
package main
import (
"context"
"fmt"
"net"
"sync"
"time"
)
// DnsCache 简单的应用层 DNS 缓存
type DnsCache struct {
mu sync.RWMutex
entries map[string]cacheEntry
ttl time.Duration
}
type cacheEntry struct {
addrs []string
expiresAt time.Time
}
func NewDnsCache(ttl time.Duration) *DnsCache {
return &DnsCache{
entries: make(map[string]cacheEntry),
ttl: ttl,
}
}
func (c *DnsCache) Resolve(ctx context.Context, host string) ([]string, error) {
c.mu.RLock()
entry, ok := c.entries[host]
c.mu.RUnlock()
if ok && time.Now().Before(entry.expiresAt) {
return entry.addrs, nil
}
// 缓存未命中或已过期,执行实际解析
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, host)
if err != nil {
// 解析失败时,如果有旧缓存则使用旧缓存(stale-on-error)
if ok {
fmt.Printf("[dns-cache] stale entry used for %s\n", host)
return entry.addrs, nil
}
return nil, err
}
c.mu.Lock()
c.entries[host] = cacheEntry{
addrs: addrs,
expiresAt: time.Now().Add(c.ttl),
}
c.mu.Unlock()
return addrs, nil
}
// Prefetch 启动时预热 DNS 缓存
func (c *DnsCache) Prefetch(hosts []string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var wg sync.WaitGroup
for _, host := range hosts {
wg.Add(1)
go func(h string) {
defer wg.Done()
start := time.Now()
addrs, err := c.Resolve(ctx, h)
elapsed := time.Since(start)
if err != nil {
fmt.Printf("[prefetch] %s failed: %v\n", h, err)
} else {
fmt.Printf("[prefetch] %s -> %v (%v)\n", h, addrs, elapsed)
}
}(host)
}
wg.Wait()
}
func main() {
cache := NewDnsCache(30 * time.Second)
hosts := []string{
"google.com",
"github.com",
"cloudflare.com",
}
// 启动时预热
cache.Prefetch(hosts)
// 后续使用缓存结果
ctx := context.Background()
addrs, _ := cache.Resolve(ctx, "google.com")
fmt.Printf("cached: google.com -> %v\n", addrs)
}3.4 DNS 预解析的注意事项
| 问题 | 说明 | 应对 |
|---|---|---|
| 缓存一致性 | 应用层缓存可能持有过期 IP | 设置与 DNS TTL 匹配的缓存时间 |
| 故障切换 | 缓存的 IP 可能已经不可用 | 实现 stale-on-error + 后台刷新 |
| 多 IP 轮询 | DNS 返回多个 IP 用于负载分散 | 缓存所有 IP,随机或轮询选择 |
| 内存占用 | 大量域名的缓存 | 设置缓存条目上限 + LRU 淘汰 |
四、TTL 策略设计
4.1 TTL 的工程权衡
TTL(Time To Live)决定 DNS 记录在缓存中的存活时间。这是 DNS 性能优化中最核心的参数:
TTL 过长(如 86400 秒 = 24 小时):
✓ 缓存命中率高,解析延迟低
✓ 递归解析器负载低
✗ IP 变更后最长需要 24 小时才能全球生效
✗ 故障切换慢,影响可用性
TTL 过短(如 30 秒):
✓ 变更快速生效
✓ 故障切换快
✗ 缓存命中率低,解析延迟增大
✗ 递归解析器负载高
✗ 权威服务器 QPS 剧增
4.2 不同场景的 TTL 推荐
| 场景 | 推荐 TTL | 理由 |
|---|---|---|
| 静态网站 | 3600–86400s | IP 几乎不变,最大化缓存 |
| 普通 Web 服务 | 300–600s | 平衡缓存与变更速度 |
| 高可用服务 | 60–120s | 快速故障切换 |
| CDN CNAME | 300–600s | CDN 自己管理后续 TTL |
| 灰度发布/迁移 | 30–60s | 临时低 TTL,完成后恢复 |
| 数据库内部域名 | 15–30s | 主从切换需要极快生效 |
| MX 记录 | 3600–86400s | 邮件服务器很少变更 |
| NS 记录 | 86400–172800s | 名称服务器极少变更 |
4.3 TTL 动态调整策略
在计划变更前降低 TTL,变更完成后恢复:
# 场景: 需要迁移 www.example.com 的 IP
# 第一步: 提前将 TTL 降低(至少在当前 TTL 之前执行)
# 当前 TTL = 3600s,需要至少提前 1 小时
# 原来: www 3600 IN A 192.0.2.1
# 改为: www 60 IN A 192.0.2.1
# 第二步: 等待旧 TTL 过期(等待 1 小时)
# 所有缓存中的旧 TTL 记录过期,新记录的 TTL=60s 生效
# 第三步: 执行 IP 变更
# www 60 IN A 198.51.100.1
# 第四步: 验证新 IP 全球生效
dig @8.8.8.8 www.example.com +short
dig @1.1.1.1 www.example.com +short
dig @208.67.222.222 www.example.com +short
# 所有解析器应该在 60 秒内返回新 IP
# 第五步: 确认稳定后恢复 TTL
# www 3600 IN A 198.51.100.14.4 TTL 对缓存命中率的影响
#!/usr/bin/env python3
"""TTL 与缓存命中率的关系模拟"""
def simulate_cache_hit_rate(ttl_seconds, queries_per_second, simulation_hours=1):
"""
模拟在给定 TTL 和 QPS 下的缓存命中率。
假设: 只有一个域名,冷启动后第一次查询 miss。
"""
total_seconds = simulation_hours * 3600
total_queries = queries_per_second * total_seconds
# 每 TTL 秒缓存过期一次,第一次查询是 miss
cache_misses = total_seconds // ttl_seconds + 1
cache_hits = total_queries - cache_misses
hit_rate = cache_hits / total_queries * 100
return {
"ttl": ttl_seconds,
"qps": queries_per_second,
"total_queries": total_queries,
"cache_misses": cache_misses,
"hit_rate": f"{hit_rate:.4f}%",
}
print(f"{'TTL':>8s} | {'QPS':>6s} | {'命中率':>12s} | {'Miss 次数':>10s}")
print("-" * 50)
for ttl in [30, 60, 120, 300, 600, 3600]:
for qps in [1, 10, 100]:
result = simulate_cache_hit_rate(ttl, qps)
print(f"{result['ttl']:>8d} | {result['qps']:>6d} | "
f"{result['hit_rate']:>12s} | {result['cache_misses']:>10d}")
print("-" * 50)
# 输出:
# TTL | QPS | 命中率 | Miss 次数
# --------------------------------------------------
# 30 | 1 | 96.6759% | 121
# 30 | 10 | 99.6664% | 121
# 30 | 100 | 99.9664% | 121
# --------------------------------------------------
# 60 | 1 | 98.3333% | 61
# 60 | 10 | 99.8306% | 61
# 60 | 100 | 99.9831% | 61
# --------------------------------------------------
# 3600 | 1 | 99.9722% | 2
# 3600 | 10 | 99.9994% | 2
# 3600 | 100 | 99.9999% | 2
# --------------------------------------------------关键洞察:即使 TTL 只有 30 秒,在 10 QPS 下缓存命中率也已经达到 99.67%。TTL 的核心影响不是缓存命中率,而是变更生效时间。对于低 QPS 的域名(如内部服务),短 TTL 的缓存成本其实很小。
五、本地 DNS 缓存部署
5.1 为什么需要本地 DNS 缓存
即使递归解析器有缓存,本地 DNS 缓存仍然有价值:
没有本地缓存:
应用 → 递归解析器(5ms RTT) → 每次 5ms
有本地缓存:
应用 → 本地缓存(0.1ms) → 命中返回
应用 → 本地缓存 → 未命中 → 递归解析器(5ms) → 缓存并返回
在高 QPS 场景下:
10000 QPS × 5ms = 50 秒的 CPU 等待时间/秒(absurd)
10000 QPS × 0.1ms = 1 秒的 CPU 等待时间/秒(可控)
5.2 systemd-resolved
systemd-resolved 是现代 Linux 发行版的默认
DNS 解析管理器:
# 检查 systemd-resolved 状态
systemctl status systemd-resolved
# 查看当前 DNS 配置
resolvectl status
# 输出示例:
# Global
# Protocols: +LLMNR +mDNS -DNSOverTLS DNSSEC=no/unsupported
# resolv.conf mode: stub
#
# Link 2 (eth0)
# Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6
# Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
# Current DNS Server: 8.8.8.8
# DNS Servers: 8.8.8.8 8.8.4.4
# 查看缓存统计
resolvectl statistics
# 输出示例:
# DNSSEC supported by current servers: no
# Transactions
# Current Transactions: 0
# Total Transactions: 1847
# Cache
# Current Cache Size: 124
# Cache Hits: 1523
# Cache Misses: 324
# DNSSEC Verdicts
# Secure: 0
# Insecure: 0
# Bogus: 0
# Indeterminate: 0
# 清除缓存
resolvectl flush-caches配置 systemd-resolved:
# /etc/systemd/resolved.conf
[Resolve]
DNS=8.8.8.8 1.1.1.1
FallbackDNS=8.8.4.4 1.0.0.1
# 启用 DNS 缓存
Cache=yes
# 缓存条目的最小和最大 TTL
CacheFromLocalhost=no
# 最多使用的 DNS 名称服务器数
DNSStubListener=yes5.3 dnsmasq
dnsmasq 是轻量级的 DNS 缓存和 DHCP
服务器,配置简单,非常适合单机或小规模部署:
# 安装
apt-get install -y dnsmasq
# 配置文件 /etc/dnsmasq.conf
cat > /etc/dnsmasq.conf << 'EOF'
# 监听地址
listen-address=127.0.0.1
bind-interfaces
# 上游 DNS 服务器
server=8.8.8.8
server=1.1.1.1
# 缓存大小(条目数,默认 150)
cache-size=10000
# 不转发不带点号的短名称
domain-needed
# 不转发私有地址段的反向查询
bogus-priv
# 设置最小 TTL(秒),防止上游返回过短的 TTL
min-cache-ttl=60
# 设置最大 TTL(秒)
max-cache-ttl=3600
# 否定缓存的 TTL
neg-ttl=60
# 并行查询所有上游服务器(返回最快的结果)
all-servers
# 日志(调试用)
# log-queries
# log-facility=/var/log/dnsmasq.log
# 自定义域名解析
address=/internal.example.com/10.0.1.100
address=/db.internal/10.0.2.50
# 内部域名使用特定 DNS 服务器
server=/internal.example.com/10.0.0.2
server=/consul/127.0.0.1#8600
EOF# 启动 dnsmasq
systemctl enable --now dnsmasq
# 验证缓存效果
dig @127.0.0.1 example.com # 第一次: ~50ms
dig @127.0.0.1 example.com # 第二次: ~0ms(缓存命中)
# 查看缓存统计
kill -USR1 $(pidof dnsmasq) # 向 syslog 输出统计
journalctl -u dnsmasq --since "1 minute ago" | grep "cache"
# 输出示例: cache size 10000, 0/234 cache insertions re-used unexpired cache entries
# 清除缓存
kill -HUP $(pidof dnsmasq) # SIGHUP 清除缓存5.4 CoreDNS
CoreDNS 是云原生的 DNS 服务器,模块化设计,Kubernetes 集群的默认 DNS 服务:
# Corefile — CoreDNS 配置
.:53 {
# 启用缓存
cache {
success 10000 300 # 正向缓存: 最多 10000 条,TTL 上限 300s
denial 5000 60 # 否定缓存: 最多 5000 条,TTL 上限 60s
prefetch 10 1h 20% # 预取: 10 次查询/分钟以上的热点域名
# 在 TTL 剩余 20% 时提前刷新
}
# 上游 DNS(转发)
forward . 8.8.8.8 1.1.1.1 {
health_check 5s # 每 5 秒检查上游健康
policy sequential # 优先使用第一个上游
max_concurrent 1000 # 最大并发转发数
}
# Prometheus 指标
prometheus :9153
# 日志
log
# 错误日志
errors
}
# 内部域名使用特定上游
internal.example.com {
forward . 10.0.0.2
cache 60
}
CoreDNS 的 prefetch 功能特别值得关注:
prefetch 的工作原理:
1. CoreDNS 统计每个域名的查询频率
2. 当一个域名的查询频率超过阈值(如 10 次/分钟)时
标记为"热点域名"
3. 当热点域名的缓存 TTL 即将过期(剩余 20%)时
CoreDNS 自动向上游发起后台查询,刷新缓存
4. 客户端始终命中缓存,无感知
效果: 热点域名的缓存命中率接近 100%,
消除了 TTL 过期时的延迟毛刺
5.5 三种本地 DNS 缓存的对比
| 特性 | systemd-resolved | dnsmasq | CoreDNS |
|---|---|---|---|
| 定位 | 系统组件 | 轻量级服务 | 云原生 DNS |
| 配置复杂度 | 低 | 低 | 中 |
| 缓存大小控制 | 有限 | cache-size | 细粒度 |
| Prefetch | ✗ | ✗ | ✓ |
| 插件生态 | ✗ | 有限 | 丰富 |
| Prometheus 指标 | ✗ | ✗ | ✓ |
| DNSSEC 验证 | ✓ | ✓ | ✓(需插件) |
| DoH/DoT 支持 | ✓(DoT) | ✗ | ✓(需插件) |
| K8s 集成 | ✗ | ✗ | ✓(原生) |
| 适用场景 | 桌面/单机 | 单机/小规模 | K8s/大规模 |
| 资源占用 | 极低 | 低 | 中 |
六、Kubernetes DNS 性能优化
6.1 K8s DNS 解析链路
Kubernetes 中的 DNS 解析比传统环境复杂:
Pod 内应用发起 DNS 查询
↓
/etc/resolv.conf 配置:
nameserver 10.96.0.10 # CoreDNS Service ClusterIP
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
↓
对于查询 "redis":
1. 先尝试 redis.default.svc.cluster.local → CoreDNS
2. 再尝试 redis.svc.cluster.local → CoreDNS
3. 再尝试 redis.cluster.local → CoreDNS
4. 最后尝试 redis → CoreDNS → 上游 DNS
↓
一个简单的 "redis" 查询可能产生 4 × 2 = 8 次 DNS 请求
(A 和 AAAA 各一次)
6.2 ndots 优化
ndots:5 是 K8s 的默认配置,意味着域名中少于
5 个点的查询会先搜索 search
domain。这导致外部域名的解析非常低效:
# Pod DNS 优化配置
apiVersion: v1
kind: Pod
metadata:
name: optimized-dns-pod
spec:
dnsPolicy: "None"
dnsConfig:
nameservers:
- 10.96.0.10
searches:
- default.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "2" # 降低 ndots,减少无效查询
- name: single-request-reopen
value: "" # A 和 AAAA 使用不同 socket
- name: timeout
value: "2" # 超时时间 2 秒
- name: attempts
value: "3" # 重试 3 次
containers:
- name: app
image: myapp:latestndots 优化的效果:
| 查询域名 | ndots:5 的查询次数 | ndots:2 的查询次数 | 节省 |
|---|---|---|---|
| redis | 8 次(4 搜索域 × A+AAAA) | 8 次 | 0 |
| api.example.com | 8 次 | 2 次(直接查询) | 75% |
| cdn.static.example.com | 8 次 | 2 次 | 75% |
| metrics.internal | 8 次 | 8 次 | 0 |
6.3 NodeLocal DNSCache
NodeLocal DNSCache 是 Kubernetes 官方推荐的 DNS 性能优化方案:
传统 DNS 路径:
Pod → ClusterIP(iptables DNAT) → CoreDNS Pod(可能在其他节点)
↓
跨节点网络延迟 + conntrack 开销
NodeLocal DNSCache:
Pod → 本机 DaemonSet DNS 缓存(169.254.20.10) → 缓存命中直接返回
→ 缓存未命中 → CoreDNS
↓
本机通信,延迟 < 1ms
# NodeLocal DNSCache DaemonSet 关键配置
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-local-dns
namespace: kube-system
spec:
selector:
matchLabels:
k8s-app: node-local-dns
template:
metadata:
labels:
k8s-app: node-local-dns
spec:
hostNetwork: true
dnsPolicy: Default
containers:
- name: node-cache
image: registry.k8s.io/dns/k8s-dns-node-cache:1.23.1
args:
- "-localip"
- "169.254.20.10"
- "-conf"
- "/etc/Corefile"
- "-upstreamsvc"
- "kube-dns"
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
- containerPort: 9253
name: metrics
protocol: TCPNodeLocal DNSCache 的 Corefile:
cluster.local:53 {
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind 169.254.20.10
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
in-addr.arpa:53 {
errors
cache 30
reload
loop
bind 169.254.20.10
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
.:53 {
errors
cache 30
reload
loop
bind 169.254.20.10
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
6.4 CoreDNS 性能调优
# CoreDNS ConfigMap 优化
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
# Kubernetes 域名解析
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
# 缓存优化: 增大缓存、启用 prefetch
cache {
success 30000 300
denial 10000 30
prefetch 10 1h 10%
}
# 转发到上游 DNS
forward . /etc/resolv.conf {
max_concurrent 2000
policy sequential
health_check 5s
}
# 自动加载配置变更
reload
# 监控指标
prometheus :9153
# 循环检测
loop
loadbalance
}七、DNS 预取的高级模式
7.1 连接池的 DNS 刷新
HTTP 客户端的长连接池可能持有过期的 DNS 结果。当后端 IP 变更时,连接池中的旧连接仍然指向旧 IP:
package main
import (
"net"
"net/http"
"time"
)
func createDnsAwareClient() *http.Client {
transport := &http.Transport{
// 限制空闲连接的最大存活时间
// 确保连接定期重建,获取新的 DNS 结果
IdleConnTimeout: 90 * time.Second,
// 限制连接的最大存活时间(Go 1.21+)
// MaxIdleConnsPerHost 配合使用
MaxIdleConnsPerHost: 10,
MaxIdleConns: 100,
// 自定义 Dialer 控制连接超时和 DNS 解析
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
// TLS 握手超时
TLSHandshakeTimeout: 5 * time.Second,
}
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
}7.2 DNS 预取与故障切换
结合健康检查的 DNS 预取策略:
#!/usr/bin/env python3
"""DNS 预取 + 健康检查 + 故障切换"""
import socket
import threading
import time
from dataclasses import dataclass, field
@dataclass
class DnsEntry:
hostname: str
addresses: list = field(default_factory=list)
healthy_addresses: list = field(default_factory=list)
last_resolved: float = 0.0
last_health_check: float = 0.0
resolve_interval: float = 30.0
health_check_interval: float = 10.0
class SmartDnsResolver:
def __init__(self):
self.entries: dict[str, DnsEntry] = {}
self.lock = threading.Lock()
self._running = False
def register(self, hostname: str, resolve_interval: float = 30.0):
with self.lock:
self.entries[hostname] = DnsEntry(
hostname=hostname,
resolve_interval=resolve_interval,
)
self._resolve(hostname)
def _resolve(self, hostname: str):
try:
results = socket.getaddrinfo(hostname, None, socket.AF_INET)
addresses = list(set(r[4][0] for r in results))
with self.lock:
entry = self.entries[hostname]
entry.addresses = addresses
entry.last_resolved = time.time()
# 新解析的地址默认认为健康
new_addrs = set(addresses) - set(entry.healthy_addresses)
entry.healthy_addresses.extend(new_addrs)
# 移除不再解析到的地址
entry.healthy_addresses = [
a for a in entry.healthy_addresses if a in addresses
]
print(f"[dns] {hostname} -> {addresses}")
except Exception as e:
print(f"[dns] resolve {hostname} failed: {e}")
def _health_check(self, hostname: str, port: int = 80, timeout: float = 2.0):
with self.lock:
entry = self.entries.get(hostname)
if not entry:
return
addresses = list(entry.addresses)
healthy = []
for addr in addresses:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((addr, port))
sock.close()
healthy.append(addr)
except Exception:
print(f"[health] {hostname}/{addr}:{port} unhealthy")
with self.lock:
entry = self.entries[hostname]
entry.healthy_addresses = healthy
entry.last_health_check = time.time()
def get_address(self, hostname: str) -> str | None:
with self.lock:
entry = self.entries.get(hostname)
if not entry:
return None
if entry.healthy_addresses:
# 简单轮询
addr = entry.healthy_addresses[0]
entry.healthy_addresses.append(
entry.healthy_addresses.pop(0)
)
return addr
# 没有健康地址时返回第一个解析结果
return entry.addresses[0] if entry.addresses else None
def start_background(self):
self._running = True
thread = threading.Thread(target=self._background_loop, daemon=True)
thread.start()
def _background_loop(self):
while self._running:
now = time.time()
with self.lock:
hostnames = list(self.entries.keys())
for hostname in hostnames:
with self.lock:
entry = self.entries[hostname]
if now - entry.last_resolved >= entry.resolve_interval:
self._resolve(hostname)
if now - entry.last_health_check >= entry.health_check_interval:
self._health_check(hostname)
time.sleep(1)
if __name__ == "__main__":
resolver = SmartDnsResolver()
resolver.register("example.com", resolve_interval=30.0)
resolver.start_background()
for _ in range(5):
addr = resolver.get_address("example.com")
print(f"Selected: {addr}")
time.sleep(1)八、DNS 性能监控
8.1 关键指标
DNS 性能监控的核心指标:
1. 解析延迟(Resolution Latency)
- P50 / P95 / P99 延迟
- 超过 100ms 的查询比例
2. 缓存命中率(Cache Hit Rate)
- 命中率 < 80% 需要关注
- 通过 CoreDNS prometheus 指标获取
3. 查询速率(QPS)
- 异常 QPS 飙升可能意味着缓存失效或 DNS 放大攻击
4. 失败率(Error Rate)
- SERVFAIL / REFUSED / NXDOMAIN 的比例
- SERVFAIL 率 > 1% 需要立即排查
5. 上游健康(Upstream Health)
- 上游 DNS 服务器的可达性和响应时间
8.2 CoreDNS Prometheus 指标
# Prometheus 抓取配置
scrape_configs:
- job_name: 'coredns'
static_configs:
- targets: ['coredns.kube-system:9153']
# 关键 PromQL 查询
# 1. DNS 查询延迟 P99
# histogram_quantile(0.99,
# sum(rate(coredns_dns_request_duration_seconds_bucket[5m])) by (le)
# )
# 2. 缓存命中率
# sum(rate(coredns_cache_hits_total[5m])) /
# (sum(rate(coredns_cache_hits_total[5m])) +
# sum(rate(coredns_cache_misses_total[5m])))
# 3. DNS 查询 QPS(按类型)
# sum(rate(coredns_dns_requests_total[5m])) by (type)
# 4. DNS 错误率
# sum(rate(coredns_dns_responses_total{rcode=~"SERVFAIL|REFUSED"}[5m])) /
# sum(rate(coredns_dns_responses_total[5m]))
# 5. 上游转发延迟 P95
# histogram_quantile(0.95,
# sum(rate(coredns_forward_request_duration_seconds_bucket[5m])) by (le, to)
# )8.3 Blackbox Exporter DNS 探针
用 Prometheus Blackbox Exporter 从外部监控 DNS 解析:
# blackbox.yml — DNS 探针配置
modules:
dns_tcp:
prober: dns
timeout: 5s
dns:
transport_protocol: "tcp"
preferred_ip_protocol: "ip4"
query_name: "www.example.com"
query_type: "A"
valid_rcodes:
- NOERROR
dns_udp:
prober: dns
timeout: 5s
dns:
transport_protocol: "udp"
preferred_ip_protocol: "ip4"
query_name: "www.example.com"
query_type: "A"
valid_rcodes:
- NOERROR
dns_internal:
prober: dns
timeout: 3s
dns:
transport_protocol: "udp"
query_name: "api.internal.example.com"
query_type: "A"
valid_rcodes:
- NOERROR# Prometheus 抓取配置
scrape_configs:
- job_name: 'dns-probe'
metrics_path: /probe
params:
module: [dns_udp]
static_configs:
- targets:
- 8.8.8.8
- 1.1.1.1
- 208.67.222.222
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:91158.4 Grafana Dashboard 设计
DNS 性能 Dashboard 的面板布局:
┌─────────────────────────────┬──────────────────────────────┐
│ DNS QPS(按类型分色) │ 缓存命中率(目标 > 95%) │
│ [A] [AAAA] [CNAME] [SRV] │ ████████████░░ 96.3% │
├─────────────────────────────┼──────────────────────────────┤
│ 解析延迟 P50/P95/P99 │ 错误率(SERVFAIL/REFUSED) │
│ P50: 2ms P95: 15ms │ ░░░░░░░░░░░░░░ 0.02% │
│ P99: 48ms │ │
├─────────────────────────────┼──────────────────────────────┤
│ 上游 DNS 健康状态 │ 缓存大小与淘汰率 │
│ 8.8.8.8: ✓ 3ms │ 当前: 8234 / 30000 │
│ 1.1.1.1: ✓ 2ms │ 淘汰率: 12/min │
│ internal: ✓ 0.5ms │ │
├─────────────────────────────┴──────────────────────────────┤
│ DNS 查询延迟热力图(时间 × 延迟桶) │
│ 按小时显示延迟分布变化趋势 │
└────────────────────────────────────────────────────────────┘
九、实战案例
9.1 案例一:TTL 过长导致的故障切换延迟
现象:数据库主从切换后,应用仍然连接旧主库,持续报错 30 分钟。
原因分析:
数据库域名配置:
db-primary.internal 3600 IN A 10.0.1.100
主从切换流程:
1. 13:00 - 检测到主库故障
2. 13:01 - 将域名指向新主库 10.0.1.200
3. 13:01~13:60 - 旧 TTL=3600 的缓存仍然有效
4. 部分应用 DNS 缓存在 12:30 刷新过
→ 这些应用要到 13:30 才能获取新 IP
5. 最糟糕的情况: 要等 3600 秒(1 小时)
实际恢复时间:
- 最快的应用: 1 分钟(缓存刚好过期)
- 最慢的应用: 59 分钟
- 大多数应用: 约 30 分钟
解决方案:
# 1. 降低数据库域名的 TTL
# db-primary.internal 30 IN A 10.0.1.100
# 故障切换后最多等待 30 秒
# 2. 应用层实现 DNS 感知的连接池
# 连接建立时记录解析到的 IP
# 定期重新解析域名,发现 IP 变化时重建连接
# 3. 数据库代理层(如 ProxySQL / PgBouncer)
# 连接池在代理层管理,切换时只需重配代理9.2 案例二:K8s DNS 延迟毛刺
现象:微服务间调用的 P99 延迟偶发飙升至 5 秒。
排查过程:
# 1. 确认延迟来源
# 在应用代码中增加 DNS 解析计时
# 发现 DNS 解析偶尔耗时 5 秒(与 resolv.conf 的 timeout 设置一致)
# 2. 检查 CoreDNS 日志
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=100
# 发现偶发 i/o timeout 到上游 DNS
# 3. 检查 CoreDNS 资源使用
kubectl top pods -n kube-system -l k8s-app=kube-dns
# CPU: 980m/1000m — CoreDNS 达到 CPU 限制
# 4. 检查 conntrack 表
conntrack -C
# 131072 — conntrack 表已满
sysctl net.netfilter.nf_conntrack_max
# 131072
# 5. 根因: CoreDNS QPS 过高 + conntrack 表满
# 导致 DNS UDP 包被 conntrack 丢弃解决方案:
# 方案一: 部署 NodeLocal DNSCache(推荐)
# 减少跨节点 DNS 流量,避免 conntrack 压力
# 方案二: 增加 CoreDNS 副本数和资源限制
apiVersion: apps/v1
kind: Deployment
metadata:
name: coredns
namespace: kube-system
spec:
replicas: 4 # 从 2 增加到 4
template:
spec:
containers:
- name: coredns
resources:
requests:
cpu: 200m
memory: 128Mi
limits:
cpu: 2000m # 增大 CPU 限制
memory: 256Mi
# 方案三: 增大 conntrack 表
# sysctl -w net.netfilter.nf_conntrack_max=5242889.3 案例三:DNS Prefetch 减少页面加载时间
场景:电商首页加载时间优化。
优化前:
瀑布图分析(Chrome DevTools):
1. HTML 文档 0ms ───── 200ms
2. CSS (cdn.ex) 200ms ──dns── 350ms
3. JS (cdn.ex) 200ms ──dns── 380ms (与 CSS 共享 DNS)
4. 图片 (img.ex) 350ms ──dns── 500ms
5. API (api.ex) 380ms ──dns── 550ms
6. 分析 (ga.com) 200ms ──dns── 380ms
7. 字体 (gf.com) 350ms ──dns── 530ms
DNS 解析耗时:
cdn.example.com: 150ms
img.example.com: 150ms
api.example.com: 170ms
analytics: 180ms
fonts: 160ms
优化后:
<head>
<!-- DNS Prefetch: 所有第三方域名 -->
<link rel="dns-prefetch" href="//img.example.com">
<link rel="dns-prefetch" href="//analytics.google.com">
<!-- Preconnect: 关键资源域名 -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="preconnect" href="https://api.example.com" crossorigin>
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
</head>优化后瀑布图:
1. HTML 文档 0ms ───── 200ms
同时: DNS prefetch cdn/api/img/ga/fonts
同时: preconnect cdn/api/fonts
2. CSS (cdn.ex) 200ms ─(已连接)─ 230ms (节省 150ms+30ms)
3. JS (cdn.ex) 200ms ─(已连接)─ 250ms
4. 图片 (img.ex) 230ms ─(DNS已解析)─ 310ms (节省 150ms DNS)
5. API (api.ex) 250ms ─(已连接)─ 280ms
6. 分析 (ga.com) 200ms ─(DNS已解析)─ 310ms
7. 字体 (gf.com) 200ms ─(已连接)─ 230ms
DNS 延迟节省: ~600ms 总计
页面完全加载: 从 2.1s 降至 1.4s(减少 33%)
十、DNS 性能优化清单
┌─────────────────────────────────────────────────────────────────┐
│ DNS 性能优化清单 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 客户端优化 │
│ □ 对关键第三方域名使用 <link rel="preconnect"> │
│ □ 对可能用到的域名使用 <link rel="dns-prefetch"> │
│ □ HTTPS 页面显式启用 dns-prefetch-control │
│ □ preconnect 数量控制在 6 个以内 │
│ │
│ TTL 策略 │
│ □ 高可用服务 TTL ≤ 120 秒 │
│ □ 数据库域名 TTL ≤ 30 秒 │
│ □ 静态资源域名 TTL ≥ 3600 秒 │
│ □ 迁移前提前降低 TTL,完成后恢复 │
│ □ MX/NS 记录使用长 TTL │
│ │
│ 本地缓存 │
│ □ 单机部署 dnsmasq 或 systemd-resolved │
│ □ K8s 集群部署 NodeLocal DNSCache │
│ □ CoreDNS 启用 prefetch 预取热点域名 │
│ □ 配置 min-cache-ttl 防止过短 TTL │
│ │
│ K8s 特定 │
│ □ 评估降低 ndots 值(从 5 到 2) │
│ □ 外部域名使用 FQDN(末尾加点) │
│ □ 增加 CoreDNS 副本数和资源限制 │
│ □ 监控 conntrack 表使用率 │
│ │
│ 服务端 │
│ □ 实现应用层 DNS 缓存(支持 stale-on-error) │
│ □ 启动时预解析关键域名 │
│ □ 连接池配合 DNS 变更(限制最大连接存活时间) │
│ □ DNS 解析加入超时和重试逻辑 │
│ │
│ 监控 │
│ □ 监控 DNS 解析延迟(P50/P95/P99) │
│ □ 监控缓存命中率(目标 > 95%) │
│ □ 监控 DNS 错误率(SERVFAIL < 0.1%) │
│ □ 设置 Blackbox Exporter DNS 探针 │
│ □ 建立 DNS 性能 Dashboard │
│ │
└─────────────────────────────────────────────────────────────────┘
十一、总结
DNS 性能优化不是调一个参数就结束的事,而是一个从客户端到服务端、从缓存到监控的系统工程:
量化先行。 不量化就不要优化。用
dig、dnsperf和 Prometheus 指标搞清楚 DNS 延迟的组成和瓶颈,才知道优化空间在哪里。TTL 是核心杠杆。 TTL 不是越短越好,也不是越长越好。根据场景选择——高可用服务用短 TTL(30–120s),静态资源用长 TTL(1h+),迁移前动态调整。关键洞察:对于 10+ QPS 的域名,即使 30 秒 TTL,缓存命中率也能达到 99.67%。
本地缓存是最有效的手段。 不管上游有多少层缓存,本地缓存都能把延迟从毫秒级降到亚毫秒级。在 K8s 环境中,NodeLocal DNSCache 不仅减少延迟,还能大幅降低 CoreDNS 和 conntrack 的压力。
预取消除感知延迟。 浏览器的 DNS Prefetch / Preconnect 和服务端的 DNS 预解析是零成本的优化——在需要结果之前提前查询,用户完全无感知。
监控闭环。 DNS 故障经常表现为其他症状(连接超时、5xx 错误、延迟飙升)。建立 DNS 性能监控 Dashboard,能让你在问题影响用户之前发现异常。
下一篇我们进入 DNSSEC 工程——理解 DNS 签名、验证与信任链的完整工作机制,以及为什么 DNSSEC 部署率至今仍然如此之低。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】DNS 解析链路:递归、迭代与缓存层级
一个域名从浏览器到最终 IP 地址,经历了浏览器缓存、操作系统缓存、Stub Resolver、递归解析器、权威服务器多层解析和缓存。本文完整追踪 DNS 解析的每一跳,剖析递归与迭代查询的差异,详解各级缓存的 TTL 行为和否定缓存的工程影响,分析公共 DNS、运营商 DNS 与自建 DNS 的选型决策。
网络工程索引
汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。
【网络工程】零拷贝网络:sendfile、splice 与 MSG_ZEROCOPY
数据从磁盘到网卡的传统路径涉及 4 次拷贝和多次上下文切换。本文系统剖析 sendfile、splice、vmsplice、MSG_ZEROCOPY 四种零拷贝技术的内核实现、适用场景与性能差异,并以 Kafka 和 Nginx 为案例分析零拷贝在生产系统中的工程实践。
【网络工程】CDN 架构原理:PoP、边缘节点与 Origin Shield
系统解剖 CDN 的多层缓存架构——从 DNS 调度到 PoP 内部结构、Origin Shield 回源保护、多 CDN 部署策略。结合实际配置和响应头分析,给出 CDN 架构的工程理解。