土法炼钢兴趣小组的算法知识备份

【网络工程】DNS 性能优化:预取、TTL 策略与本地缓存

文章导航

分类入口
network
标签入口
#dns#performance#prefetch#ttl#caching#dnsmasq#coredns

目录

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.042311

1.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-prefetchpreconnect 的区别:

特性 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 解析。虽然操作系统和递归解析器有缓存,但在以下场景中缓存可能失效:

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.1

4.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=yes

5.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:latest

ndots 优化的效果:

查询域名 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: TCP

NodeLocal 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:9115

8.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=524288

9.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 性能优化不是调一个参数就结束的事,而是一个从客户端到服务端、从缓存到监控的系统工程:

  1. 量化先行。 不量化就不要优化。用 digdnsperf 和 Prometheus 指标搞清楚 DNS 延迟的组成和瓶颈,才知道优化空间在哪里。

  2. TTL 是核心杠杆。 TTL 不是越短越好,也不是越长越好。根据场景选择——高可用服务用短 TTL(30–120s),静态资源用长 TTL(1h+),迁移前动态调整。关键洞察:对于 10+ QPS 的域名,即使 30 秒 TTL,缓存命中率也能达到 99.67%。

  3. 本地缓存是最有效的手段。 不管上游有多少层缓存,本地缓存都能把延迟从毫秒级降到亚毫秒级。在 K8s 环境中,NodeLocal DNSCache 不仅减少延迟,还能大幅降低 CoreDNS 和 conntrack 的压力。

  4. 预取消除感知延迟。 浏览器的 DNS Prefetch / Preconnect 和服务端的 DNS 预解析是零成本的优化——在需要结果之前提前查询,用户完全无感知。

  5. 监控闭环。 DNS 故障经常表现为其他症状(连接超时、5xx 错误、延迟飙升)。建立 DNS 性能监控 Dashboard,能让你在问题影响用户之前发现异常。

下一篇我们进入 DNSSEC 工程——理解 DNS 签名、验证与信任链的完整工作机制,以及为什么 DNSSEC 部署率至今仍然如此之低。


上一篇:DNS 解析链路:递归、迭代与缓存层级

下一篇:DNSSEC 工程:签名、验证与信任链

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-07-28 · network

【网络工程】DNS 解析链路:递归、迭代与缓存层级

一个域名从浏览器到最终 IP 地址,经历了浏览器缓存、操作系统缓存、Stub Resolver、递归解析器、权威服务器多层解析和缓存。本文完整追踪 DNS 解析的每一跳,剖析递归与迭代查询的差异,详解各级缓存的 TTL 行为和否定缓存的工程影响,分析公共 DNS、运营商 DNS 与自建 DNS 的选型决策。

2026-04-22 · network

网络工程索引

汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。

2025-07-23 · network

【网络工程】零拷贝网络:sendfile、splice 与 MSG_ZEROCOPY

数据从磁盘到网卡的传统路径涉及 4 次拷贝和多次上下文切换。本文系统剖析 sendfile、splice、vmsplice、MSG_ZEROCOPY 四种零拷贝技术的内核实现、适用场景与性能差异,并以 Kafka 和 Nginx 为案例分析零拷贝在生产系统中的工程实践。


By .