dnsdist 升级到 1.9.14,两个 Technitium DNS Server 节点升级到 15.2.0,生产 DNS 查询保持可用。0. 结论和使用边界#
升级目标:
| 组件 | 升级前 | 升级后 | 结果 |
|---|---|---|---|
dnsdist | 1.9.11 | 1.9.14 | 正常 |
| Technitium 从节点 | 14.2.0 | 15.2.0 | 正常 |
| Technitium 主节点 | 14.2.0 | 15.2.0 | 正常 |
本文不是从零安装教程,而是一份生产变更复盘。它关注的是:已经有一套 DNS 集群在线服务时,如何用最小风险完成入口和后端升级,并在每个阶段验证功能完整性。
读完这篇,你应该能复用三件事:
- 升级顺序:入口先行,后端从节点先于主节点
- Review Gate:每一步变更前后要检查什么
- 证据链:遇到 Drops、ACL、timeout 这类信号时怎么判断
不建议直接复用的部分:
- 示例 IP、域名、镜像仓库地址都已经脱敏
- Web 控制台 ACL 不能照抄,必须按自己的管理端来源收紧
- 回滚数据目录前必须确认 Technitium 版本迁移是否可逆
最终验证:
dig @192.0.2.211 corp.example.internal A +short
# 192.0.2.20
dig @192.0.2.234 corp.example.internal A +short
# 192.0.2.20
dig @192.0.2.235 corp.example.internal A +short
# 192.0.2.20
公网递归解析也正常:
dig @192.0.2.211 example.com A +short
# 返回公开递归解析结果
dnsdist Web 控制台返回 401 Unauthorized,说明服务已起来,等待认证登录:
curl -I http://192.0.2.211:5380/
# HTTP/1.1 401 Unauthorized
验证矩阵:
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| 入口 DNS 查询 | dig @入口 内部域名 | 返回预期 A 记录 |
| 后端主节点查询 | dig @主节点 内部域名 | 返回预期 A 记录 |
| 后端从节点查询 | dig @从节点 内部域名 | 返回预期 A 记录 |
| 递归解析 | dig @入口 example.com | 能返回公网递归结果 |
| Web 控制台 | curl -I :5380 | 返回认证挑战,不是连接失败 |
| 配置合法性 | dnsdist --check-config | 配置检查通过 |
1. 升级前准备#
这一步的目的不是“多做几个检查”,而是避免在变更窗口里临时判断风险。生产 DNS 变更至少要先准备好这些信息:
| 准备项 | 为什么需要 |
|---|---|
| 当前拓扑 | 知道入口、主节点、从节点分别承担什么职责 |
| 当前版本和目标版本 | 判断是否跨大版本、是否涉及配置迁移 |
| 核心验证域名 | 升级后用同一组域名做对比 |
| 备份目录 | 确保配置和数据能快速恢复 |
| 回滚命令 | 出问题时不用现场拼命令 |
| 观察窗口 | 给健康检查、端口监听和缓存恢复留时间 |
建议先把参数写清楚,再执行命令:
ENTRY_DNS="192.0.2.211"
MASTER_DNS="192.0.2.234"
SLAVE_DNS="192.0.2.235"
CHECK_DOMAIN="corp.example.internal"
PUBLIC_DOMAIN="example.com"
用同一组变量做升级前检查:
dig @"${ENTRY_DNS}" "${CHECK_DOMAIN}" A +short
dig @"${MASTER_DNS}" "${CHECK_DOMAIN}" A +short
dig @"${SLAVE_DNS}" "${CHECK_DOMAIN}" A +short
dig @"${ENTRY_DNS}" "${PUBLIC_DOMAIN}" A +short
如果升级前这些检查不稳定,先排查现状,不要把升级当成修复手段。
2. 架构和职责#
这套 DNS 集群由一个入口节点和两个后端 DNS 节点组成。
| 角色 | 主机 | 部署方式 | 作用 |
|---|---|---|---|
| DNS 入口 | 192.0.2.211 | systemd 运行 dnsdist | 对外提供 :53,转发到后端 |
| 主 DNS | 192.0.2.234 | Docker Compose 运行 Technitium | 权威区、递归解析、主节点 |
| 从 DNS | 192.0.2.235 | Docker Compose 运行 Technitium | 权威区、递归解析、从节点 |
入口层的职责是接收查询、健康检查和后端调度;后端 Technitium 节点负责权威区、递归解析和集群同步。这个职责拆分决定了升级顺序:入口先完成安全更新和配置校验,后端再按从节点到主节点滚动。
flowchart LR
client["业务客户端"] -->|UDP/TCP 53| lb["dnsdist
192.0.2.211"]
lb -->|健康检查 / 转发| master["Technitium Master
192.0.2.234:53"]
lb -->|健康检查 / 转发| slave["Technitium Slave
192.0.2.235:53"]
master <-->|Technitium Cluster
HTTPS 53443| slave
master --> upstream["上游 DNS"]
slave --> upstream
后端 Technitium 使用 host 网络模式,容器内 dotnet 进程直接监听宿主机端口:
53/tcp
53/udp
5380/tcp
53443/tcp
3. 为什么先升级 dnsdist#
入口层先动,不是因为它最容易,而是因为它暴露面最大、影响路径最短。变更前 dnsdist 日志持续提示安全更新:
PowerDNS DNSDist Security Update Mandatory: Upgrade now
当前仓库里可升级版本已经是 1.9.14:
Installed: 1.9.11-1pdns.jammy
Candidate: 1.9.14-1pdns.jammy
同时,dnsdist --check-config 暴露了两个配置问题:
newServer: Unknown key 'udpTimeout' given - ignored
Passing a plain-text password via the 'password' parameter to 'setWebserverConfig()' is not advised
Passing a plain-text API key via the 'apiKey' parameter to 'setWebserverConfig()' is not advised
这一步的判断依据有三个:
dnsdist是所有 DNS 查询的统一入口,安全更新优先级高- 配置检查已经提示无效参数和明文凭据风险
- 后端两个 Technitium 节点仍保持可用,入口变更失败时回退面清晰
所以先升级入口,再滚动升级后端。
4. 变更策略#
这次没有把所有组件一起升级,而是拆成三个阶段。
- 先升级入口节点
192.0.2.211上的dnsdist - 再升级从节点
192.0.2.235 - 再升级主节点
192.0.2.234
每个阶段都按同一个模式执行:
Review -> Backup -> Change -> Verify -> Observe
flowchart LR
review["Review
确认现状"] --> backup["Backup
保留回滚点"]
backup --> change["Change
执行最小变更"]
change --> verify["Verify
验证业务查询"]
verify --> observe["Observe
观察日志和指标"]
每个阶段都要有明确的“继续/停止”判断:
| 阶段 | 可以继续的条件 | 应该停止的信号 |
|---|---|---|
| 入口升级后 | dnsdist active,配置检查通过,入口解析正常 | 配置检查失败、入口解析失败、Web 控制台连接失败 |
| 从节点升级后 | 从节点本机解析正常,入口解析仍正常 | 从节点端口未就绪、容器反复重启 |
| 主节点升级后 | 主从节点都 up,入口解析稳定 | 后端被摘除、延迟异常、TCP 查询失败 |
5. 升级前 Review#
Review Gate 的目标是确认“现在是好的”,否则后面的升级失败很难判断是新问题还是旧问题。
先确认入口和两个后端都能解析核心域名。
dig @192.0.2.211 corp.example.internal A +short
dig @192.0.2.234 corp.example.internal A +short
dig @192.0.2.235 corp.example.internal A +short
确认服务状态:
systemctl status dnsdist --no-pager
docker ps
确认 dnsdist 配置可被当前版本加载:
dnsdist --check-config
确认后端 Compose 只需要改镜像 tag:
grep -n "image:" /data/docker-compose/dns-server/docker-compose.yaml
Review Gate 清单:
| 检查项 | 为什么要检查 |
|---|---|
| 入口和后端解析结果 | 确认变更前业务路径正常 |
systemctl status dnsdist | 确认入口服务不是异常状态 |
docker ps | 确认后端容器运行状态 |
dnsdist --check-config | 提前发现升级后可能放大的配置问题 |
| Compose 镜像 tag | 确认后端升级只需要改版本,不引入额外变量 |
6. 升级 dnsdist#
入口节点先做备份。这里备份的重点不是“备份所有系统文件”,而是保留能快速恢复入口能力的最小集合:dnsdist 配置、当前版本信息和软件源候选版本。
stamp="$(date +%F-%H%M%S)"
backup_dir="/root/dns-upgrade-backup-${stamp}"
mkdir -p "${backup_dir}"
cp -a /etc/dnsdist/dnsdist.conf "${backup_dir}/dnsdist.conf"
cp -a /etc/dnsdist "${backup_dir}/etc-dnsdist"
dnsdist --version > "${backup_dir}/dnsdist.version.before" 2>&1
apt-cache policy dnsdist > "${backup_dir}/dnsdist.apt-policy.before" 2>&1
执行升级:
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --only-upgrade dnsdist
升级完成后确认版本:
dnsdist --version
# dnsdist 1.9.14
升级后先不要继续动后端。入口必须完成三类验证:
dnsdist --check-config能通过systemctl is-active dnsdist返回active- 入口
:53能正常解析内部域名和公网域名
只有入口稳定,后端滚动才有意义。
7. 加固 dnsdist 配置#
本次配置改动只做三件事:
- 删除
newServer()中无效的udpTimeout - 将 Web 控制台
password改为hashPassword()生成的 hash - 将 Web 控制台
apiKey改为hashPassword()生成的 hash
验证配置:
dnsdist --check-config
# Configuration '/etc/dnsdist/dnsdist.conf' OK!
重启入口:
systemctl restart dnsdist
systemctl is-active dnsdist
验证 DNS 查询:
dig @192.0.2.211 corp.example.internal A +short
dig @192.0.2.211 example.com A +short
dnsdist Web 控制台 ACL 从全开放收紧到管理网段,但实际访问路径不匹配,导致 http://192.0.2.211:5380/ 无法正常打开。随后将 Web ACL 回退到升级前策略,保留密码和 API Key hash 加固。当前 DNS 查询 ACL 仍保持内网限制:
setACL({'192.0.2.0/24', '198.51.100.10/32', '127.0.0.0/8', '::1/128'})
当前 Web 控制台 ACL 暂时保持升级前策略:
setWebserverConfig({ acl = "0.0.0.0/0", "::/0" })
这是一个遗留项,需要后续确认真实管理端来源后再收紧。
8. 滚动升级 Technitium 从节点#
先升级从节点,主节点保持不动。这样做的收益很直接:如果新版本启动失败、数据迁移异常或端口没有及时起来,主节点仍然能兜住解析能力。
从节点备份:
stamp="$(date +%F-%H%M%S)"
backup_dir="/root/dns-upgrade-backup-${stamp}"
mkdir -p "${backup_dir}"
cp -a /data/docker-compose/dns-server/docker-compose.yaml "${backup_dir}/docker-compose.yaml.before"
tar --warning=no-file-changed --ignore-failed-read \
--exclude='./apps/Query Logs (Sqlite)/querylogs.db' \
-czf "${backup_dir}/dns-server_config_without_live_querylogs.tgz" \
-C /var/lib/docker/volumes/dns-server_config/_data .
querylogs.db 是实时写入文件,备份时排除它,避免 tar 因文件变化返回非 0。
只改镜像 tag:
- image: registry.example.internal/technitium/dns-server:14.2.0
+ image: registry.example.internal/technitium/dns-server:15.2.0
执行升级:
cd /data/docker-compose/dns-server
docker compose pull
docker compose up -d
刚启动时不要立刻判断失败。Technitium 容器启动后,53 端口需要几秒钟才完全就绪。这里的判断标准不是“容器 Up”,而是 DNS 查询能返回预期结果。
验证从节点:
docker ps --filter name=dns-server-slave
docker logs --tail 200 dns-server-slave
dig @127.0.0.1 corp.example.internal A +short
dig @127.0.0.1 example.com A +short
从节点升级后确认入口仍正常:
dig @192.0.2.211 corp.example.internal A +short
从节点通过后,观察入口是否还能稳定转发查询。如果入口查询正常,说明后端池里至少有一个升级后的节点已经可用。
9. 滚动升级 Technitium 主节点#
从节点验证通过后,再升级主节点。流程和从节点一致。
只改镜像 tag:
- image: registry.example.internal/technitium/dns-server:14.2.0
+ image: registry.example.internal/technitium/dns-server:15.2.0
执行:
cd /data/docker-compose/dns-server
docker compose pull
docker compose up -d
验证:
docker ps --filter name=dns-server-master
docker logs --tail 200 dns-server-master
dig @127.0.0.1 corp.example.internal A +short
dig @127.0.0.1 example.com A +short
主节点升级完成后,再回到入口做多次验证:
for i in 1 2 3 4 5; do
dig @192.0.2.211 corp.example.internal A +short
done
结果均返回:
192.0.2.20
主节点升级后要再看一次后端池状态。原因是有些问题不会直接表现为解析失败,而是表现为健康检查延迟、TCP 查询异常或某个后端被摘除。升级窗口里至少要确认:
- 两个后端都处于
up - UDP 查询延迟没有异常抬高
- TCP 查询没有明显失败
- 入口 QPS 和后端分布符合预期
10. Drops 判断:不是攻击#
升级过程中看到 dnsdist 页面里有 Drops 字段,于是单独做了一轮判断。
采集到的关键指标:
dns-server-master dropRate=0
dns-server-slave dropRate=0
dnsdist API 和日志没有看到这些异常:
acl-drops
rule-drop
dyn-blocked
downstream-timeouts
downstream-send-errors
servfail-responses
抓 20 秒 53 端口流量,来源都是内网地址,最高来源约十几 QPS 量级,没有公网来源,也没有明显攻击特征。
判断是否攻击,不应该只盯页面上的一个列名。更稳的证据链是:
| 证据 | 观察结果 | 判断 |
|---|---|---|
后端 dropRate | 0 | 不支持后端丢弃异常 |
| ACL / rule drop | 未见有效非零证据 | 不支持规则丢弃 |
| dyn-block | 未见触发 | 不支持动态封禁 |
| timeout / send error | 未见明显异常 | 不支持后端不可达 |
| 抓包来源 | 内网来源,QPS 不高 | 不支持公网攻击 |
结论:这次看到的 Drops 不支持“正在被攻击”的判断,更像是页面表格空列或误读。真正需要关注的是 dropRate、acl-drops、rule-drop、dyn-blocked、后端 timeout 以及来源 IP 的 QPS。
排查顺序建议固定下来:
- 先看后端
dropRate - 再看 ACL、rule、dyn-block 是否命中
- 再看 timeout、send error、SERVFAIL
- 再抓包确认来源 IP 和 QPS
这样能避免看到一个 Drops 字段就直接归因到攻击。
11. 回滚方案#
回滚方案要按组件拆开。入口和后端不要混在一起回滚,否则会把问题范围扩大。
dnsdist 回滚:
cp /root/dns-upgrade-backup-*/dnsdist.conf /etc/dnsdist/dnsdist.conf
dnsdist --check-config
systemctl restart dnsdist
Technitium 回滚:
cd /data/docker-compose/dns-server
# 将镜像 tag 改回 14.2.0
docker compose up -d
如果跨版本升级导致配置迁移不可逆,需要停止容器后恢复升级前备份的 volume 内容。这个动作会影响节点数据目录,必须单独确认后执行。
回滚后仍要执行同样的验证矩阵:入口、主节点、从节点、内部域名、公网递归和 Web 控制台状态。
12. 常见问题#
问题:服务是 active,但 DNS 查询失败
可能原因:
dnsdist配置加载成功,但后端健康检查失败- Technitium 容器已启动,但
53端口还没就绪 - ACL 允许了管理访问,但没有允许查询来源
处理方式:
dnsdist --check-config
systemctl is-active dnsdist
dig @192.0.2.211 corp.example.internal A +short
docker ps
问题:Web 控制台打不开
先确认是服务没起来,还是 ACL 不匹配:
curl -I http://192.0.2.211:5380/
systemctl status dnsdist --no-pager
如果是 ACL 收紧导致访问路径不匹配,优先恢复可用性,再重新设计管理网段策略。
问题:备份 querylogs.db 时 tar 报错
querylogs.db 是实时写入文件,备份过程中变化很正常。不要让它阻塞配置备份,可以排除这个文件,保留配置和核心数据。
13. 遗留项#
这次升级完成后还有三个后续事项。
Web 控制台 ACL
dnsdistWeb 控制台 ACL 当前保持升级前全开放策略。虽然它监听在内网 IP,且认证凭据已 hash,但仍建议确认真实管理端来源后再收紧。Packet Cache
配置中定义了
newPacketCache(),但 API 统计里没有明显缓存命中。后续要确认是否需要显式绑定到 pool 或 server policy。系统维护窗口
APT 升级时提示有内核和系统服务重启需求。本次只处理 DNS 组件,没有重启宿主机。内核和系统服务重启应放到独立维护窗口。
14. 复盘#
这类基础设施升级,不追求“一步到位”,而是追求每一步都有退路。命令可以复制,顺序不能随便改。
这次比较有效的做法是:
- 入口和后端拆开升级
- 后端先从节点,再主节点
- 每个组件升级前做 Review
- 每个组件升级后立即验证内部域名和公网递归
- 不把实时写入的日志数据库作为阻塞备份对象
- 发现 Web 控制台 ACL 不适配时,先恢复可用性,再记录遗留项
DNS 是很多系统的隐性依赖。升级时不要只看服务是否 active,要直接用 dig 验证业务域名,最好同时验证入口、主节点和从节点。
可以复用到其他基础设施升级的原则:
- 先处理入口风险,再滚动处理后端
- 先证明变更前是好的,再证明变更后仍然是好的
- 每一步只引入一个变量
- 回滚点要能在几分钟内执行
- 安全加固失败时,先恢复可用性,再把加固项作为遗留风险跟踪
