跳过正文
  1. 博客文章/

生产级 DNS 集群从零部署:dnsdist 与 Technitium 三节点架构

·2755 字·13 分钟·
DevOps SRE DNS Dnsdist Technitium Docker DDNS DevOps SRE
Zayn
作者
Zayn
专注 Kubernetes、CI/CD、可观测性等云原生技术栈,记录生产环境中的实战经验与踩坑复盘。
目录
生产变更复盘 - 这篇文章属于一个选集。
3: 本文
入口节点用 dnsdist 承接查询和健康检查,两个后端节点用 Docker Compose 运行 Technitium DNS Server,并通过集群通道同步权威区数据。这份 runbook 重点回答三件事:机器如何关联、服务如何从零搭起来、上线前如何验证 DNS 与 DDNS。
文中统一使用 192.0.2.0/24198.51.100.0/24corp.example.internal 这类文档示例值。落地时需要替换为自己的网段、域名、镜像仓库和凭据。

0. 结论和使用边界
#

这套 DNS 集群由三台机器组成:

角色示例主机名示例 IP部署方式职责
DNS 入口dns-entry192.0.2.211systemd 运行 dnsdist监听 :53,做 ACL、健康检查、转发和入口观测
主 DNSdns-master192.0.2.234Docker Compose 运行 Technitium权威区、递归解析、集群主节点
从 DNSdns-slave192.0.2.235Docker Compose 运行 Technitium权威区副本、递归解析、集群从节点

当前部署形态:

配置 / 状态
入口组件dnsdist 1.9.14,systemd active,dnsdist --check-config 通过
后端组件Technitium DNS Server 15.2.0,两台后端容器均 running
后端网络network_mode: host,直接监听宿主机 53/tcp53/udp5380/tcp53443/tcp
数据目录Docker volume 挂载到容器 /etc/dns,宿主机路径形如 /var/lib/docker/volumes/dns-server_config/_data
集群文件后端存在 cluster.configcluster-catalog.<domain>.zone 和业务 zone 文件
Docker 日志json-filemax-size=10mmax-file=3
系统参数Docker LimitNOFILE=524288,dnsdist LimitNOFILE=1000000somaxconn=4096

主要覆盖三件事:

  • 集群配置:三台机器如何通过 dnsdist、Technitium 集群通道和固定主机名关联起来。
  • 从零部署:没有现成环境时,如何准备系统、安装 Docker、部署后端和入口。
  • 性能验证:部署后如何验证 DNS 查询、后端调度、DDNS 更新和回滚边界。

边界也要先收住:

  • 生产域名、内网地址、镜像仓库和凭据只写示例值。
  • DDNS 性能验证给出流程。没有专用测试 zone 和 TSIG/API 凭据时,不把更新吞吐写成结论。
  • 不建议手工编辑 Technitium 的 cluster.config 或二进制 zone 文件。集群关系应通过控制台/API 维护。
  • 不替代正式变更流程。真正上线前仍需要维护窗口、备份、审批和回滚演练。

已经有在线集群时,可以配合阅读上一篇升级复盘:生产 DNS 集群平滑升级复盘:dnsdist 与 Technitium 的滚动变更

1. 集群配置:三台机器如何关联
#

本节先讲清楚拓扑关系。只有明确入口、主节点、从节点之间的职责边界,后面的安装命令才不会变成散装脚本。

1.1 请求路径
#

客户端只应该把 DNS 请求发给入口节点。入口用 dnsdist 判断来源 ACL、执行健康检查,再把请求转发给后端 Technitium。

flowchart LR
    client["业务客户端"] -->|UDP/TCP 53| entry["dnsdist
192.0.2.211"] entry -->|健康检查 / 转发| master["Technitium Master
192.0.2.234:53"] entry -->|健康检查 / 转发| slave["Technitium Slave
192.0.2.235:53"] master <-->|Cluster HTTPS 53443| slave master --> upstream["上游 DNS"] slave --> upstream

这条路径里有两层关联:

层级关联方式作用
入口到后端dnsdistnewServer() 指向两个 Technitium 节点查询转发、健康检查、后端调度
后端主从Technitium 的集群配置和 cluster-catalog zone同步集群成员、业务 zone 和服务记录

现场 dnsdist 里主节点和从节点各配置了两个后端实例:一个用内部权威域名做健康检查,一个用公网域名做递归解析健康检查。这样可以同时观察“内部权威解析是否正常”和“递归能力是否正常”。

1.2 端口规划
#

生产部署时先把端口边界写清楚。

端口所在节点用途建议暴露范围
53/udp入口、后端DNS 查询入口面向业务网段;后端只面向入口、管理网段和必要的集群节点
53/tcp入口、后端TCP DNS、区域传输或大响应同上
5380/tcp后端Technitium Web 控制台 HTTP只允许管理网段
53443/tcp后端Technitium HTTPS 控制台和集群通道只允许主从节点和管理网段
5380/tcp入口dnsdist Web 控制台只允许管理网段

后端容器使用 host 网络模式,因此 Compose 里的端口映射不会生效,端口由容器内 dotnet 进程直接监听宿主机。这个设计减少了 Docker NAT 的变量,但也意味着宿主机防火墙和上游网络 ACL 必须更明确。

1.3 Technitium 主从关系
#

后端节点通过三个机制关联:

  1. 固定主机名解析

    两台后端的 Compose 都写了 extra_hosts,把主从节点的内部 FQDN 固定到对应 IP,避免集群初始化时依赖外部 DNS 解析自身。

    extra_hosts:
      - "dns-master.corp.example.internal:192.0.2.234"
      - "dns-slave.corp.example.internal:192.0.2.235"
    
  2. Technitium 集群通道

    两台后端的持久化目录里都存在 cluster.config,其中能看到以 https://dns-*.corp.example.internal:53443/ 形式互相识别的集群地址。这个文件属于 Technitium 内部格式,不建议手工改。

  3. 集群目录 zone

    持久化目录里有 cluster-catalog.corp.example.internal.zone,并包含 _53443._tcpdns-masterdns-slave 这类服务发现信息。业务 zone 是否同步到从节点,以 Technitium 控制台里的集群策略为准。

当前主节点有更多业务 zone,从节点至少同步了核心业务 zone。这个现象说明:不是所有 zone 都一定要同步到从节点。上线前要按业务重要性确认每个 zone 的同步策略。

1.4 dnsdist 后端关系
#

入口节点的核心配置可以抽象成下面这样。这里连续写多条 addLocal("0.0.0.0:53", { reusePort = true }) 不是复制错误,而是沿用现场配置方式:让多个 socket 通过 SO_REUSEPORT 监听同一个 DNS 端口,用于提升 UDP 查询并发处理能力。新环境可以按 CPU、QPS 和压测结果调整条数。

-- 多条 addLocal 配合 reusePort,用于增加 UDP 查询处理并发。
addLocal("0.0.0.0:53", { reusePort = true })
addLocal("0.0.0.0:53", { reusePort = true })
addLocal("0.0.0.0:53", { reusePort = true })
setMaxTCPClientThreads(8)

setACL({
  "192.0.2.0/24",
  "198.51.100.0/24",
  "127.0.0.0/8",
  "::1/128"
})

newServer({
  address = "192.0.2.234:53",
  name = "dns-server-master",
  checkInterval = 15,
  checkType = "A",
  checkName = "corp.example.internal.",
  order = 1
})

newServer({
  address = "192.0.2.234:53",
  name = "master-recursion-check",
  checkInterval = 15,
  checkType = "A",
  checkName = "example.com.",
  order = 1
})

newServer({
  address = "192.0.2.235:53",
  name = "dns-server-slave",
  checkInterval = 15,
  checkType = "A",
  checkName = "corp.example.internal.",
  order = 2
})

newServer({
  address = "192.0.2.235:53",
  name = "slave-recursion-check",
  checkInterval = 15,
  checkType = "A",
  checkName = "example.com.",
  order = 2
})

这里的关键点不是 newServer() 数量,而是健康检查口径:

  • 内部域名检查可以发现权威区是否可用。
  • 公网域名检查可以发现递归链路、上游 DNS 或出口网络是否异常。
  • order 可以表达主从优先级,但是否真正作为流量策略,还要结合 dnsdist 的 server policy 验证。

2. 从零部署前的准备
#

从零部署不要先写 Compose。先把系统、网络、时间、用户、目录和回滚路径准备好。

2.1 机器规划
#

示例规划如下,真实环境按自己的地址段替换:

角色主机名IP操作系统最低建议
DNS 入口dns-entry192.0.2.211Ubuntu 22.04 LTS2C / 2G / 20G
主 DNSdns-master192.0.2.234Ubuntu 22.04 LTS2C / 4G / 50G
从 DNSdns-slave192.0.2.235Ubuntu 22.04 LTS2C / 4G / 50G

先配置主机名和基础工具:

hostnamectl set-hostname dns-master

apt-get update
apt-get install -y ca-certificates curl gnupg lsb-release \
  bind9-dnsutils jq tcpdump vim chrony

systemctl enable --now chrony
timedatectl

验证时间同步:

chronyc tracking
date -Is

通过标准:

检查项通过标准
主机名三台机器互不重复,和规划一致
时间NTP 正常,时区明确
DNS 工具dig 可用
网络三台机器互通,入口能访问后端 5353443

2.2 目录规划
#

后端节点建议固定目录:

mkdir -p /data/docker-compose/dns-server
mkdir -p /data/backup/dns-server
mkdir -p /etc/dns-server/secrets
chmod 700 /etc/dns-server/secrets

入口节点建议固定目录:

mkdir -p /data/backup/dnsdist

这两个备份目录只存变更窗口里的配置快照,不替代长期备份系统。

2.3 网络和防火墙基线
#

现场机器的主机侧 INPUT 策略是放行状态,这要求上游网络 ACL 足够可靠。从零部署时更建议把主机防火墙写清楚。

示例规则:

ufw default deny incoming
ufw default allow outgoing

# SSH 管理网段
ufw allow from 198.51.100.0/24 to any port 22 proto tcp

# DNS 入口
ufw allow from 192.0.2.0/24 to any port 53 proto udp
ufw allow from 192.0.2.0/24 to any port 53 proto tcp

# 管理控制台
ufw allow from 198.51.100.0/24 to any port 5380 proto tcp
ufw allow from 198.51.100.0/24 to any port 53443 proto tcp

ufw enable
ufw status verbose

如果生产网络已经由上游防火墙统一控制,主机防火墙也至少要保留一份规则文档,方便审计和灾备恢复。

3. 后端节点:安装 Docker 和系统优化
#

Technitium 后端用 Docker Compose 部署。先把 Docker 安装和运行参数标准化,后面升级或迁移时会轻很多。

3.1 安装 Docker CE
#

dns-masterdns-slave 上执行:

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" \
  > /etc/apt/sources.list.d/docker.list

apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

systemctl enable --now docker
docker --version
docker compose version

现场后端安装的是 Docker CE 26.1.3 和 Compose plugin 2.27.0。从零部署时不必固定旧版本,除非你的变更流程要求版本冻结;真正要固定版本时,先通过 apt-cache policy docker-ce 选择目标版本。

3.2 配置 Docker 日志轮转
#

现场两台后端都配置了 Docker json-file 日志轮转,这是必须保留的生产基线。

cat > /etc/docker/daemon.json <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "live-restore": true
}
EOF

systemctl restart docker
docker info --format '{{json .LoggingDriver}}'

如果机器上已经运行其他容器,重启 Docker 前要先走变更审批。新机器从零部署时可以直接配置。

3.3 系统参数
#

现场参数里有几个对 DNS 服务有意义的点:

参数现场值作用
net.core.somaxconn4096提高监听 backlog 上限
net.ipv4.ip_local_port_range1024 65535扩大本地临时端口范围
Docker LimitNOFILE524288降低文件描述符不足风险

从零部署可以写成独立 sysctl 文件:

cat > /etc/sysctl.d/99-dns-cluster.conf <<'EOF'
net.core.somaxconn = 4096
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.udp_rmem_min = 4096
net.ipv4.udp_wmem_min = 4096
EOF

sysctl --system

为 Docker 补充 systemd override:

mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/override.conf <<'EOF'
[Service]
LimitNOFILE=524288
EOF

systemctl daemon-reload
systemctl restart docker
systemctl show docker --property=LimitNOFILE

这些参数不是越大越好。上线后要结合 ss -sconntrack、dnsdist 指标和系统负载看是否需要继续调整。

4. 部署 Technitium 主从节点
#

后端部署的核心是:两台机器使用同一个镜像和同一套目录约定,只在容器名、主机名、DNS_SERVER_DOMAIN 上区分主从。

4.1 Master Compose
#

dns-master 上创建管理员密码文件:

openssl rand -base64 32 > /etc/dns-server/secrets/admin_password
chmod 600 /etc/dns-server/secrets/admin_password

写入 /data/docker-compose/dns-server/docker-compose.yaml

services:
  dns-server-master:
    container_name: dns-server-master
    hostname: dns-server-master
    image: registry.example.internal/technitium/dns-server:15.2.0
    dns:
      - 223.6.6.6
    network_mode: "host"
    extra_hosts:
      - "dns-master.corp.example.internal:192.0.2.234"
      - "dns-slave.corp.example.internal:192.0.2.235"
    environment:
      - DNS_SERVER_DOMAIN=dns-master
      - TZ=Asia/Shanghai
      - DNS_SERVER_ADMIN_PASSWORD_FILE=/run/secrets/admin_password
      - DNS_SERVER_FORWARDERS=223.5.5.5,8.8.8.8,1.1.1.1,114.114.114.114,114.114.115.115
      - DNS_SERVER_FORWARDER_PROTOCOL=Udp
      - DNS_SERVER_LOG_USING_LOCAL_TIME=true
    volumes:
      - config:/etc/dns
      - /tmp:/tmp
    secrets:
      - admin_password
    restart: always

volumes:
  config:

secrets:
  admin_password:
    file: /etc/dns-server/secrets/admin_password

启动:

cd /data/docker-compose/dns-server
docker compose up -d
docker ps --filter name=dns-server-master

验证监听:

ss -lntup | grep -E ':53|:5380|:53443'
dig @127.0.0.1 example.com A +short

4.2 Slave Compose
#

dns-slave 上同样创建密码文件。初始密码可以不同,后续以控制台权限和集群策略为准。

openssl rand -base64 32 > /etc/dns-server/secrets/admin_password
chmod 600 /etc/dns-server/secrets/admin_password

写入 /data/docker-compose/dns-server/docker-compose.yaml

services:
  dns-server-slave:
    container_name: dns-server-slave
    hostname: dns-server-slave
    image: registry.example.internal/technitium/dns-server:15.2.0
    dns:
      - 223.6.6.6
    network_mode: "host"
    extra_hosts:
      - "dns-master.corp.example.internal:192.0.2.234"
      - "dns-slave.corp.example.internal:192.0.2.235"
    environment:
      - DNS_SERVER_DOMAIN=dns-slave
      - TZ=Asia/Shanghai
      - DNS_SERVER_ADMIN_PASSWORD_FILE=/run/secrets/admin_password
      - DNS_SERVER_FORWARDERS=223.5.5.5,8.8.8.8,1.1.1.1,114.114.114.114,114.114.115.115
      - DNS_SERVER_FORWARDER_PROTOCOL=Udp
      - DNS_SERVER_LOG_USING_LOCAL_TIME=true
    volumes:
      - config:/etc/dns
      - /tmp:/tmp
    secrets:
      - admin_password
    restart: always

volumes:
  config:

secrets:
  admin_password:
    file: /etc/dns-server/secrets/admin_password

启动和验证:

cd /data/docker-compose/dns-server
docker compose up -d
docker ps --filter name=dns-server-slave
ss -lntup | grep -E ':53|:5380|:53443'
dig @127.0.0.1 example.com A +short

4.3 建立 Technitium 集群
#

Technitium 的集群关系通过控制台/API 维护,不要直接改 cluster.config

推荐顺序:

  1. dns-master 控制台完成初始化,确认管理员密码、Web 控制台访问来源和上游 DNS。
  2. dns-slave 控制台完成初始化,确认 dns-slave 可以访问 dns-master:53443
  3. 在控制台里启用集群,添加对端节点地址:
    • https://dns-master.corp.example.internal:53443/
    • https://dns-slave.corp.example.internal:53443/
  4. 确认生成或同步 cluster-catalog.corp.example.internal
  5. 在主节点上创建核心业务 zone,并按业务要求选择是否同步到从节点。

验证集群通道:

curl -kI https://dns-master.corp.example.internal:53443/
curl -kI https://dns-slave.corp.example.internal:53443/

验证持久化目录:

docker volume inspect dns-server_config
find /var/lib/docker/volumes/dns-server_config/_data -maxdepth 1 -type f -printf '%f\n' | sort
find /var/lib/docker/volumes/dns-server_config/_data/zones -maxdepth 1 -type f -printf '%f\n' | sort

通过标准:

检查项通过标准
cluster.config主从节点都存在
cluster-catalog zone主从节点都存在
核心业务 zone从节点按策略同步,不缺关键 zone
53443主从互相可访问
控制台只能从管理网段访问

5. 部署 dnsdist 入口
#

入口节点不跑 Technitium。它只做入口转发、ACL、健康检查和观测。

5.1 安装 dnsdist
#

dns-entry 上安装 PowerDNS 官方仓库的 dnsdist:

curl -fsSL https://repo.powerdns.com/FD380FBB-pub.asc \
  | gpg --dearmor -o /usr/share/keyrings/pdns-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/pdns-archive-keyring.gpg] http://repo.powerdns.com/ubuntu jammy-dnsdist-19 main" \
  > /etc/apt/sources.list.d/pdns.list

apt-get update
apt-get install -y dnsdist

systemctl enable dnsdist
dnsdist --version

现场入口已经升级到 dnsdist 1.9.14,并且 dnsdist --check-config 通过。新部署时同样以配置检查作为启动前门禁。

5.2 写入入口配置
#

编辑 /etc/dnsdist/dnsdist.conf

-- 多条 addLocal 配合 reusePort,用于增加 UDP 查询处理并发。
-- 条数不要照抄,先从 1-3 条开始,再按压测结果调整。
addLocal("0.0.0.0:53", { reusePort = true })
addLocal("0.0.0.0:53", { reusePort = true })
addLocal("0.0.0.0:53", { reusePort = true })
setMaxTCPClientThreads(8)

setACL({
  "192.0.2.0/24",
  "198.51.100.0/24",
  "127.0.0.0/8",
  "::1/128"
})

newServer({
  address = "192.0.2.234:53",
  name = "dns-server-master",
  checkInterval = 15,
  checkType = "A",
  checkName = "corp.example.internal.",
  order = 1
})

newServer({
  address = "192.0.2.234:53",
  name = "master-recursion-check",
  checkInterval = 15,
  checkType = "A",
  checkName = "example.com.",
  order = 1
})

newServer({
  address = "192.0.2.235:53",
  name = "dns-server-slave",
  checkInterval = 15,
  checkType = "A",
  checkName = "corp.example.internal.",
  order = 2
})

newServer({
  address = "192.0.2.235:53",
  name = "slave-recursion-check",
  checkInterval = 15,
  checkType = "A",
  checkName = "example.com.",
  order = 2
})

pc = newPacketCache(500000, {
  maxTTL = 3600,
  minTTL = 5,
  temporaryFailureTTL = 10,
  staleTTL = 60,
  numberOfShards = 20
})

webserver("192.0.2.211:5380")
setWebserverConfig({ password = "<HASHED_WEB_PASSWORD>" })
setWebserverConfig({ apiKey = "<HASHED_API_KEY>" })
setWebserverConfig({ acl = "198.51.100.0/24" })

生成 Web 控制台密码和 API Key hash 时,在 dnsdist -c 交互环境使用 hashPassword(),不要把明文写进配置。

配置检查和启动:

dnsdist --check-config
systemctl restart dnsdist
systemctl is-active dnsdist
ss -lntup | grep -E ':53|:5380'

5.3 入口安全基线
#

生产入口至少要满足这些条件:

配置要求
DNS 查询 ACL只允许业务网段、管理网段和本机
Web 控制台绑定地址绑定管理 IP,不绑定公网
Web 控制台 ACL不使用 0.0.0.0/0,除非外层已有强 ACL 且有审计
控制台密码和 API Key使用 hash,不写明文
配置检查每次重启前执行 dnsdist --check-config
指标观测至少能看到后端 up/down、QPS、latency、dropRate

现场升级复盘里有一个遗留项:Web 控制台 ACL 曾为了恢复运维入口保持较宽策略。新部署时不要沿用这个临时状态,应该在上线前确认真实管理端来源并收紧。

6. 上线前功能验证
#

功能验证要同时查入口和后端。只看容器 running 或 systemd active 不够。

先定义变量:

ENTRY_DNS="192.0.2.211"
MASTER_DNS="192.0.2.234"
SLAVE_DNS="192.0.2.235"
INTERNAL_DOMAIN="corp.example.internal"
PUBLIC_DOMAIN="example.com"

验证矩阵:

验证项命令通过标准
入口内部域名dig @"${ENTRY_DNS}" "${INTERNAL_DOMAIN}" A +short返回预期内网记录
主节点内部域名dig @"${MASTER_DNS}" "${INTERNAL_DOMAIN}" A +short返回预期内网记录
从节点内部域名dig @"${SLAVE_DNS}" "${INTERNAL_DOMAIN}" A +short返回预期内网记录
入口递归解析dig @"${ENTRY_DNS}" "${PUBLIC_DOMAIN}" A +short返回公网解析结果
TCP 查询dig @"${ENTRY_DNS}" "${INTERNAL_DOMAIN}" A +tcp +short返回预期记录
后端健康dnsdist Web/API 或控制台所有后端 up
Web 控制台curl -I "http://${ENTRY_DNS}:5380/"返回认证挑战或登录页,不是连接失败

批量验证:

for server in "${ENTRY_DNS}" "${MASTER_DNS}" "${SLAVE_DNS}"; do
  echo "== ${server} internal =="
  dig @"${server}" "${INTERNAL_DOMAIN}" A +tries=1 +time=2 +short

  echo "== ${server} public =="
  dig @"${server}" "${PUBLIC_DOMAIN}" A +tries=1 +time=2 +short
done

通过后再把业务侧 DNS 指向入口节点,而不是直接指向后端节点。

7. DNS 性能验证
#

性能验证分三层:入口压测、后端直测、稳定性观察。不要在业务高峰期直接对生产入口做满压。

压测工具优先选 DNSPerf/dnsperf。它的输入是查询文件,常用参数包括 -s 指定 DNS 服务器、-d 指定查询文件、-Q 限制 QPS、-c 模拟客户端数量、-T 设置客户端线程数、-u 发送 dynamic update。压测客户端应放在独立机器上,避免客户端 CPU、网卡或中间防火墙把结果压歪。

7.1 准备测试数据
#

在独立压测客户端准备查询文件:

cat > /tmp/dns-queries.txt <<'EOF'
corp.example.internal A
www.corp.example.internal A
api.corp.example.internal A
example.com A
www.example.com A
EOF

安装 dnsperf,不同发行版包名可能不同:

apt-get update
apt-get install -y dnsperf

如果发行版没有 dnsperf,可以换成企业内部标准压测工具,但要保留相同口径:请求数、并发、超时、成功率、平均延迟、P95/P99。

7.2 带护栏的压测脚本
#

压测脚本默认不允许打私网地址。真实压测生产入口前,必须显式设置 ALLOW_PRIVATE_BENCHMARK=yes,并提前确认维护窗口和 QPS 上限。

cat > /tmp/dns-benchmark.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

TARGET="${TARGET:-}"
PORT="${PORT:-53}"
DATA_FILE="${DATA_FILE:-/tmp/dns-queries.txt}"
DURATION="${DURATION:-60}"
RAMP_QPS="${RAMP_QPS:-100 500 1000}"
CLIENTS="${CLIENTS:-32}"
THREADS="${THREADS:-2}"
TIMEOUT="${TIMEOUT:-2}"
OUT_DIR="${OUT_DIR:-./dns-benchmark-$(date +%Y%m%d-%H%M%S)}"
ALLOW_PRIVATE_BENCHMARK="${ALLOW_PRIVATE_BENCHMARK:-no}"

usage() {
  cat <<USAGE
Usage:
  TARGET=192.0.2.211 DATA_FILE=/tmp/dns-queries.txt /tmp/dns-benchmark.sh

Required:
  TARGET                    DNS server address.

Optional:
  PORT=53
  DATA_FILE=/tmp/dns-queries.txt
  DURATION=60
  RAMP_QPS="100 500 1000"
  CLIENTS=32
  THREADS=2
  TIMEOUT=2
  OUT_DIR=./dns-benchmark-<timestamp>
  ALLOW_PRIVATE_BENCHMARK=yes  # required for 10/8, 172.16/12, 192.168/16 targets
USAGE
}

if [ -z "${TARGET}" ]; then
  usage
  exit 2
fi

if [ ! -s "${DATA_FILE}" ]; then
  echo "query file not found or empty: ${DATA_FILE}" >&2
  exit 2
fi

if ! command -v dnsperf >/dev/null 2>&1; then
  echo "dnsperf not found. Install DNSPerf/dnsperf on the benchmark client first." >&2
  exit 2
fi

if [[ "${TARGET}" =~ ^10\. ]] || [[ "${TARGET}" =~ ^192\.168\. ]] || [[ "${TARGET}" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]]; then
  if [ "${ALLOW_PRIVATE_BENCHMARK}" != "yes" ]; then
    echo "refuse to benchmark private address ${TARGET} without ALLOW_PRIVATE_BENCHMARK=yes" >&2
    exit 3
  fi
fi

mkdir -p "${OUT_DIR}"
{
  echo "target=${TARGET}"
  echo "port=${PORT}"
  echo "data_file=${DATA_FILE}"
  echo "duration=${DURATION}"
  echo "ramp_qps=${RAMP_QPS}"
  echo "clients=${CLIENTS}"
  echo "threads=${THREADS}"
  echo "timeout=${TIMEOUT}"
  date -Is
} | tee "${OUT_DIR}/metadata.txt"

echo "== preflight =="
head -n 5 "${DATA_FILE}" | while read -r name type; do
  [ -n "${name:-}" ] || continue
  dig @"${TARGET}" -p "${PORT}" "${name}" "${type:-A}" +tries=1 +time=2 +short || true
done | tee "${OUT_DIR}/preflight.log"

for qps in ${RAMP_QPS}; do
  log="${OUT_DIR}/dnsperf-qps-${qps}.log"
  echo "== dnsperf qps=${qps} =="
  dnsperf \
    -s "${TARGET}" \
    -p "${PORT}" \
    -d "${DATA_FILE}" \
    -l "${DURATION}" \
    -Q "${qps}" \
    -c "${CLIENTS}" \
    -T "${THREADS}" \
    -t "${TIMEOUT}" \
    -S 5 \
    | tee "${log}"

  if grep -E "Queries lost:[[:space:]]*[1-9]" "${log}" >/dev/null; then
    echo "queries lost at qps=${qps}; stop ramping" >&2
    break
  fi
done

echo "logs written to ${OUT_DIR}"
EOF

chmod +x /tmp/dns-benchmark.sh

低压运行:

TARGET="192.0.2.211" \
RAMP_QPS="100 500 1000" \
DURATION=60 \
/tmp/dns-benchmark.sh

如果目标是真实生产私网地址,必须显式确认:

TARGET="10.x.x.x" \
ALLOW_PRIVATE_BENCHMARK=yes \
RAMP_QPS="100 500 1000" \
DURATION=60 \
/tmp/dns-benchmark.sh

dnsperf -T 表示客户端线程数,不表示 TCP 查询。TCP 功能验证继续使用 dig +tcp;如果要做 TCP/DoT/DoH/DoQ 的高并发压测,应换支持对应协议的工具,并单独做变更审批。

7.3 入口压测
#

先低压确认工具链:

dnsperf -s 192.0.2.211 -d /tmp/dns-queries.txt -l 30 -Q 100 -c 8 -T 1

再逐步提高:

dnsperf -s 192.0.2.211 -d /tmp/dns-queries.txt -l 60 -Q 1000 -c 32 -T 2
dnsperf -s 192.0.2.211 -d /tmp/dns-queries.txt -l 60 -Q 5000 -c 64 -T 4

观察项:

指标判断方式
成功率不出现持续 timeout、SERVFAIL 或明显错误响应
延迟以团队 SLO 为准,不写没有口径的固定数
后端状态dnsdist 中四个后端实例保持 up
dropRate不持续增长
CPU/内存toppidstat 或监控系统无异常尖峰
日志journalctl -u dnsdist 无 downstream timeout 或后端抖动

7.4 后端直测
#

入口稳定后,再直测两个后端,用来判断瓶颈是在入口层还是后端解析层。

dnsperf -s 192.0.2.234 -d /tmp/dns-queries.txt -l 60 -Q 1000 -c 32 -T 2
dnsperf -s 192.0.2.235 -d /tmp/dns-queries.txt -l 60 -Q 1000 -c 32 -T 2

如果入口慢、后端快,优先看 dnsdist 线程、ACL、缓存、server policy、网络链路。如果后端直测也慢,优先看 Technitium 上游 DNS、zone 数据、日志数据库、宿主机资源和出口网络。

7.5 缓存验证
#

现场配置定义了 newPacketCache(),但升级复盘里记录过“缓存命中需要继续确认”的遗留项。新部署时不要只写缓存配置,要验证命中是否符合预期。

验证思路:

  1. 用同一批域名跑冷缓存压测。
  2. 等待很短时间后跑第二轮 warm cache。
  3. 对比响应时间、后端查询量和 dnsdist 缓存统计。
  4. 如果缓存统计没有变化,检查是否需要把 cache 显式绑定到对应 pool 或规则。

不要为了追求缓存命中率把 TTL 拉得过长。权威区和递归查询的缓存策略要服从业务变更频率。

7.6 本次生产入口实测结果
#

生产入口做过一轮受控 UDP DNS 压测。目标是 dnsdist 入口,下面用 192.0.2.211 作为文档示例地址。

测试口径:

测试时间2026-06-08 22:20 CST
压测来源独立客户端 5600x
目标DNS 入口 192.0.2.211:53
工具/usr/bin/python3 标准库 UDP DNS benchmark
查询类型A 记录
查询集1 个内部权威域名 + 1 个公网递归域名,正文使用示例值展示
每档时长30 秒
请求超时2 秒
停止条件丢包、timeout、send error、dnsdist 日志异常或后端异常

第一轮保守压测:

目标 QPS发送接收丢包TimeoutSend ErrorNOERROR实际响应 QPS平均延迟P50P95P99最大延迟
1003,0003,0000003,000100.000.55 ms0.49 ms0.96 ms1.53 ms2.90 ms
50015,00015,00000015,000500.000.36 ms0.28 ms0.84 ms1.35 ms3.55 ms
1,00030,00030,00000030,0001,000.000.29 ms0.20 ms0.75 ms1.29 ms6.16 ms

确认前三档稳定后,又补了一轮更高 QPS:

目标 QPS发送接收丢包TimeoutSend ErrorNOERROR实际响应 QPS平均延迟P50P95P99最大延迟
2,00060,00060,00000060,0002,000.000.29 ms0.19 ms0.81 ms1.33 ms6.75 ms
5,000150,000150,000000150,0005,000.000.29 ms0.16 ms0.90 ms1.50 ms6.92 ms
10,000300,000300,000000300,00010,000.000.36 ms0.18 ms1.12 ms1.93 ms10.82 ms

压测后复查:

复查项结果
dnsdist 状态active
dnsdist 版本1.9.14
入口端口53/udp53/tcp5380/tcp 仍监听
近 15 分钟 dnsdist 日志无新增异常日志
Technitium Master容器 running,日志无异常尾部
Technitium Slave容器 running,日志无异常尾部
压测后内部域名查询NOERROR
压测后公网递归查询NOERROR

这个结果只能说明:在这次客户端、网络路径、查询集和 30 秒窗口下,生产入口至少稳定承接了 10,000 QPS 的 UDP A 记录查询,没有观察到丢包、timeout 或明显服务端异常。它不能直接外推为长期容量上限,也不能证明 TCP、DoT、DoH、DDNS 更新或更复杂查询类型的容量。

8. DDNS 验证和性能测试
#

DDNS 测试必须在专用测试 zone 里做。不要拿生产业务记录做 add/delete 压测。

8.1 启用边界
#

建议单独创建:

ddns-test.corp.example.internal

准入要求:

要求
更新方式TSIG 或 Technitium API,二选一即可
来源 ACL只允许 DDNS 客户端或管理网段
TTL测试记录使用低 TTL,例如 60
记录名使用唯一前缀,避免覆盖真实记录
清理策略测试结束必须删除测试记录
审计保留更新时间、来源 IP、成功/失败数量

8.2 单条 DDNS 功能验证
#

如果使用 RFC 2136 nsupdate 和 TSIG,先做单条新增:

cat > /tmp/ddns-add.txt <<'EOF'
server 192.0.2.211 53
zone ddns-test.corp.example.internal
update add smoke-001.ddns-test.corp.example.internal 60 A 192.0.2.50
send
EOF

nsupdate -k /etc/dns/ddns-test.key /tmp/ddns-add.txt
dig @192.0.2.211 smoke-001.ddns-test.corp.example.internal A +short
dig @192.0.2.234 smoke-001.ddns-test.corp.example.internal A +short
dig @192.0.2.235 smoke-001.ddns-test.corp.example.internal A +short

删除测试记录:

cat > /tmp/ddns-delete.txt <<'EOF'
server 192.0.2.211 53
zone ddns-test.corp.example.internal
update delete smoke-001.ddns-test.corp.example.internal A
send
EOF

nsupdate -k /etc/dns/ddns-test.key /tmp/ddns-delete.txt

通过标准:

  • 新增后入口、主节点、从节点都能查询到记录。
  • 删除后入口、主节点、从节点都不再返回旧记录。
  • Technitium 日志能看到更新行为,没有鉴权失败。

8.3 DDNS 并发验证
#

先写一个小脚本,限制在测试 zone 内:

cat > /tmp/ddns-one.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

id="$1"
record="load-${id}.ddns-test.corp.example.internal"
tmp="$(mktemp)"

cat > "${tmp}" <<NSUPDATE
server 192.0.2.211 53
zone ddns-test.corp.example.internal
update add ${record} 60 A 192.0.2.50
send
NSUPDATE

nsupdate -k /etc/dns/ddns-test.key "${tmp}"
rm -f "${tmp}"
EOF

chmod +x /tmp/ddns-one.sh

低并发开始:

seq 1 100 | xargs -n1 -P5 /tmp/ddns-one.sh

再按维护窗口逐步提高:

seq 101 500 | xargs -n1 -P20 /tmp/ddns-one.sh

验证写入数量:

for server in 192.0.2.211 192.0.2.234 192.0.2.235; do
  dig @"${server}" load-101.ddns-test.corp.example.internal A +short
  dig @"${server}" load-500.ddns-test.corp.example.internal A +short
done

DDNS 压测观察项:

指标通过标准
更新成功数和发送数量一致,失败有明确原因
单条更新时间以团队 SLO 为准,不能只写“很快”
主从可见性主节点写入后,从节点在可接受时间内可查询
错误类型没有持续 REFUSEDNOTAUTHSERVFAIL
日志增长查询日志和应用日志不会快速打满磁盘

清理测试记录时同样使用脚本批量 delete。清理完成后抽样查询,确认没有残留。

9. 备份、回滚和停止条件
#

DNS 是基础服务,部署时必须先有回滚路径。

9.1 备份
#

入口备份:

stamp="$(date +%F-%H%M%S)"
mkdir -p "/data/backup/dnsdist/${stamp}"
cp -a /etc/dnsdist/dnsdist.conf "/data/backup/dnsdist/${stamp}/dnsdist.conf"
dnsdist --version > "/data/backup/dnsdist/${stamp}/dnsdist.version"

后端备份:

stamp="$(date +%F-%H%M%S)"
backup_dir="/data/backup/dns-server/${stamp}"
mkdir -p "${backup_dir}"

cp -a /data/docker-compose/dns-server/docker-compose.yaml "${backup_dir}/docker-compose.yaml"
tar --warning=no-file-changed --ignore-failed-read \
  --exclude='./apps/Query Logs (Sqlite)/querylogs.db' \
  -czf "${backup_dir}/dns-server-config.tgz" \
  -C /var/lib/docker/volumes/dns-server_config/_data .

querylogs.db 是实时写入文件,备份时可以排除,避免它阻塞核心配置备份。

9.2 回滚
#

入口回滚:

cp /data/backup/dnsdist/<stamp>/dnsdist.conf /etc/dnsdist/dnsdist.conf
dnsdist --check-config
systemctl restart dnsdist
dig @192.0.2.211 corp.example.internal A +short

后端回滚:

cd /data/docker-compose/dns-server
# 将 image tag 改回上一个已验证版本
docker compose up -d
docker ps
dig @127.0.0.1 corp.example.internal A +short

如果 Technitium 跨版本迁移导致数据格式不可逆,恢复 volume 属于高风险操作,必须单独审批。

9.3 继续和停止条件
#

阶段可以继续必须停止
Docker 安装docker psdocker compose version 正常Docker 服务异常、宿主机网络异常
Master 部署本机 dig @127.0.0.1 正常,控制台可访问容器反复重启、53 端口不监听
Slave 部署本机解析正常,能访问 Master 53443主从网络不通、控制台鉴权异常
集群关联cluster.configcluster-catalog 出现,核心 zone 同步核心 zone 不同步、从节点返回旧数据
dnsdist 部署配置检查通过,入口解析正常dnsdist --check-config 失败、入口无法解析
DNS 压测无持续 timeout、后端 up、日志正常dropRate 持续增长、后端 down、业务侧报错
DDNS 压测测试 zone add/delete 正常,主从可见出现鉴权失败、生产记录被误改、日志或磁盘异常

10. 常见问题
#

问题:Technitium 容器 running,但宿主机 53 没监听

先看容器日志和网络模式:

docker ps --filter name=dns-server
docker logs --tail 200 dns-server-master
grep -n "network_mode" /data/docker-compose/dns-server/docker-compose.yaml
ss -lntup | grep ':53'

如果使用 host 网络,宿主机已有进程占用 53 会导致启动异常。先释放端口,再启动容器。

问题:主节点有 zone,从节点没有

不要直接复制 zone 文件。先在 Technitium 控制台确认这个 zone 是否应该同步到从节点,再检查集群状态、53443 连通性和集群目录 zone。

问题:入口能解析公网域名,但内部域名失败

优先查后端权威区:

dig @192.0.2.234 corp.example.internal A +short
dig @192.0.2.235 corp.example.internal A +short

如果后端直查失败,问题在 Technitium zone 或集群同步;如果后端正常、入口失败,问题在 dnsdist 健康检查、ACL 或转发策略。

问题:Web 控制台打不开

先区分服务不可达和 ACL 不匹配:

curl -I http://192.0.2.211:5380/
systemctl status dnsdist --no-pager
ss -lntp | grep ':5380'

生产里不要为了临时访问把 Web ACL 长期放成全开放。应该确认管理端来源,再补最小 ACL。

问题:DDNS 压测失败

按这个顺序查:

  1. TSIG 或 API 凭据是否正确。
  2. 更新来源 IP 是否在允许列表里。
  3. zone 是否允许动态更新。
  4. nsupdate 是否指向入口节点,入口是否能转发更新请求。
  5. 主从同步是否在预期时间内完成。

11. 遗留项和人工确认
#

这份 runbook 覆盖了拓扑、从零部署和一次生产入口压测结果,但仍有几个点不能靠文档自动闭环。

遗留项原因建议动作
DDNS 实测指标尚未准备可公开复用的 DDNS TSIG/API 测试凭据在专用测试 zone 准备凭据后,按第 8 节单独压测
长稳容量上限本次是每档 30 秒的 UDP A 查询压测,不是长时间 soak test在维护窗口补 30-60 分钟稳定性压测
TCP/DoT/DoH 容量本次只测 UDP A 查询使用对应协议工具单独验证
Packet Cache 命中现场定义了 newPacketCache(),但命中效果仍需结合指标确认在 dnsdist 指标里核对 cache hit/miss,必要时补显式绑定策略
Web 控制台 ACL生产现场曾为了恢复可访问性保留较宽 ACL确认真实管理端来源后收紧到管理网段
业务 zone 同步策略主节点 zone 数量多于从节点,未必所有 zone 都需要同步按业务重要性确认每个 zone 是否必须在从节点可用

这些点不影响 runbook 的部署参考价值,但会影响生产上线前的容量评估和安全审计。执行人需要把它们放进变更检查单,而不是默认为“已经解决”。

12. 可复用原则
#

这套部署可以复用的不是某个 IP 或某个域名,而是几个边界:

  • 业务客户端只连入口,后端节点不直接暴露给所有业务网段。
  • 入口健康检查同时覆盖内部权威解析和公网递归解析。
  • Technitium 集群关系通过控制台/API 管理,不手工改内部配置文件。
  • 后端容器使用 host 网络时,宿主机防火墙和上游 ACL 必须更严。
  • 部署完成后必须用 digdnsperf、DDNS add/delete 和日志指标共同验证,不能只看服务状态。
  • 性能结论必须有口径:查询集、并发、持续时间、成功率、延迟和压测来源都要写清楚。
  • 安全加固失败时先恢复服务,再记录遗留项;不要把临时放宽策略当成生产基线。
生产变更复盘 - 这篇文章属于一个选集。
3: 本文

相关文章

生产 DNS 集群平滑升级复盘:dnsdist 与 Technitium 的滚动变更
·989 字·5 分钟
SRE DNS SRE DevOps Dnsdist +3
基于 macvlan 的 JupyterLab GPU 调试实例部署复盘
·1801 字·9 分钟
GPU DevOps Jupyterlab Docker Macvlan +4
Docker 常用命令速查手册
·378 字·2 分钟
Docker DevOps Docker Container DevOps Cheatsheet