dnsdist 承接查询和健康检查,两个后端节点用 Docker Compose 运行 Technitium DNS Server,并通过集群通道同步权威区数据。这份 runbook 重点回答三件事:机器如何关联、服务如何从零搭起来、上线前如何验证 DNS 与 DDNS。192.0.2.0/24、198.51.100.0/24 和 corp.example.internal 这类文档示例值。落地时需要替换为自己的网段、域名、镜像仓库和凭据。0. 结论和使用边界#
这套 DNS 集群由三台机器组成:
| 角色 | 示例主机名 | 示例 IP | 部署方式 | 职责 |
|---|---|---|---|---|
| DNS 入口 | dns-entry | 192.0.2.211 | systemd 运行 dnsdist | 监听 :53,做 ACL、健康检查、转发和入口观测 |
| 主 DNS | dns-master | 192.0.2.234 | Docker Compose 运行 Technitium | 权威区、递归解析、集群主节点 |
| 从 DNS | dns-slave | 192.0.2.235 | Docker Compose 运行 Technitium | 权威区副本、递归解析、集群从节点 |
当前部署形态:
| 项 | 配置 / 状态 |
|---|---|
| 入口组件 | dnsdist 1.9.14,systemd active,dnsdist --check-config 通过 |
| 后端组件 | Technitium DNS Server 15.2.0,两台后端容器均 running |
| 后端网络 | network_mode: host,直接监听宿主机 53/tcp、53/udp、5380/tcp、53443/tcp |
| 数据目录 | Docker volume 挂载到容器 /etc/dns,宿主机路径形如 /var/lib/docker/volumes/dns-server_config/_data |
| 集群文件 | 后端存在 cluster.config、cluster-catalog.<domain>.zone 和业务 zone 文件 |
| Docker 日志 | json-file,max-size=10m,max-file=3 |
| 系统参数 | Docker LimitNOFILE=524288,dnsdist LimitNOFILE=1000000,somaxconn=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
这条路径里有两层关联:
| 层级 | 关联方式 | 作用 |
|---|---|---|
| 入口到后端 | dnsdist 的 newServer() 指向两个 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 主从关系#
后端节点通过三个机制关联:
固定主机名解析
两台后端的 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"Technitium 集群通道
两台后端的持久化目录里都存在
cluster.config,其中能看到以https://dns-*.corp.example.internal:53443/形式互相识别的集群地址。这个文件属于 Technitium 内部格式,不建议手工改。集群目录 zone
持久化目录里有
cluster-catalog.corp.example.internal.zone,并包含_53443._tcp、dns-master、dns-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-entry | 192.0.2.211 | Ubuntu 22.04 LTS | 2C / 2G / 20G |
| 主 DNS | dns-master | 192.0.2.234 | Ubuntu 22.04 LTS | 2C / 4G / 50G |
| 从 DNS | dns-slave | 192.0.2.235 | Ubuntu 22.04 LTS | 2C / 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 可用 |
| 网络 | 三台机器互通,入口能访问后端 53 和 53443 |
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-master 和 dns-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.somaxconn | 4096 | 提高监听 backlog 上限 |
net.ipv4.ip_local_port_range | 1024 65535 | 扩大本地临时端口范围 |
Docker LimitNOFILE | 524288 | 降低文件描述符不足风险 |
从零部署可以写成独立 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 -s、conntrack、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。
推荐顺序:
- 在
dns-master控制台完成初始化,确认管理员密码、Web 控制台访问来源和上游 DNS。 - 在
dns-slave控制台完成初始化,确认dns-slave可以访问dns-master:53443。 - 在控制台里启用集群,添加对端节点地址:
https://dns-master.corp.example.internal:53443/https://dns-slave.corp.example.internal:53443/
- 确认生成或同步
cluster-catalog.corp.example.internal。 - 在主节点上创建核心业务 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/内存 | top、pidstat 或监控系统无异常尖峰 |
| 日志 | 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(),但升级复盘里记录过“缓存命中需要继续确认”的遗留项。新部署时不要只写缓存配置,要验证命中是否符合预期。
验证思路:
- 用同一批域名跑冷缓存压测。
- 等待很短时间后跑第二轮 warm cache。
- 对比响应时间、后端查询量和 dnsdist 缓存统计。
- 如果缓存统计没有变化,检查是否需要把 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 | 发送 | 接收 | 丢包 | Timeout | Send Error | NOERROR | 实际响应 QPS | 平均延迟 | P50 | P95 | P99 | 最大延迟 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 100 | 3,000 | 3,000 | 0 | 0 | 0 | 3,000 | 100.00 | 0.55 ms | 0.49 ms | 0.96 ms | 1.53 ms | 2.90 ms |
| 500 | 15,000 | 15,000 | 0 | 0 | 0 | 15,000 | 500.00 | 0.36 ms | 0.28 ms | 0.84 ms | 1.35 ms | 3.55 ms |
| 1,000 | 30,000 | 30,000 | 0 | 0 | 0 | 30,000 | 1,000.00 | 0.29 ms | 0.20 ms | 0.75 ms | 1.29 ms | 6.16 ms |
确认前三档稳定后,又补了一轮更高 QPS:
| 目标 QPS | 发送 | 接收 | 丢包 | Timeout | Send Error | NOERROR | 实际响应 QPS | 平均延迟 | P50 | P95 | P99 | 最大延迟 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2,000 | 60,000 | 60,000 | 0 | 0 | 0 | 60,000 | 2,000.00 | 0.29 ms | 0.19 ms | 0.81 ms | 1.33 ms | 6.75 ms |
| 5,000 | 150,000 | 150,000 | 0 | 0 | 0 | 150,000 | 5,000.00 | 0.29 ms | 0.16 ms | 0.90 ms | 1.50 ms | 6.92 ms |
| 10,000 | 300,000 | 300,000 | 0 | 0 | 0 | 300,000 | 10,000.00 | 0.36 ms | 0.18 ms | 1.12 ms | 1.93 ms | 10.82 ms |
压测后复查:
| 复查项 | 结果 |
|---|---|
dnsdist 状态 | active |
dnsdist 版本 | 1.9.14 |
| 入口端口 | 53/udp、53/tcp、5380/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 为准,不能只写“很快” |
| 主从可见性 | 主节点写入后,从节点在可接受时间内可查询 |
| 错误类型 | 没有持续 REFUSED、NOTAUTH、SERVFAIL |
| 日志增长 | 查询日志和应用日志不会快速打满磁盘 |
清理测试记录时同样使用脚本批量 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 ps 和 docker compose version 正常 | Docker 服务异常、宿主机网络异常 |
| Master 部署 | 本机 dig @127.0.0.1 正常,控制台可访问 | 容器反复重启、53 端口不监听 |
| Slave 部署 | 本机解析正常,能访问 Master 53443 | 主从网络不通、控制台鉴权异常 |
| 集群关联 | cluster.config 和 cluster-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 压测失败
按这个顺序查:
- TSIG 或 API 凭据是否正确。
- 更新来源 IP 是否在允许列表里。
- zone 是否允许动态更新。
nsupdate是否指向入口节点,入口是否能转发更新请求。- 主从同步是否在预期时间内完成。
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 必须更严。 - 部署完成后必须用
dig、dnsperf、DDNS add/delete 和日志指标共同验证,不能只看服务状态。 - 性能结论必须有口径:查询集、并发、持续时间、成功率、延迟和压测来源都要写清楚。
- 安全加固失败时先恢复服务,再记录遗留项;不要把临时放宽策略当成生产基线。
