1. 为什么还要折腾DNS主从与分离解析
很多人觉得DNS这东西,配一次能解析就行了,搞什么主从、搞什么分离解析,纯属给自己找事。我刚开始接触DNS那会儿也是这么想的,直到有一次线上环境出问题——单台DNS服务器扛不住查询压力,解析延迟飙升,业务方电话直接打到我这里来。从那以后我才认真去研究DNS的高可用架构,也就是主从服务器加分离解析这套组合拳。
先说清楚这套东西到底是什么、能干什么。DNS主从服务器,简单理解就是一台主DNS负责写记录,一台或多台从DNS自动同步主服务器的记录,对外提供查询服务。主挂了,从还能顶上去,解析不中断。分离解析,也叫视图解析,是指同一台DNS服务器根据请求来源的不同,返回不同的解析结果。比如内网用户查某个域名返回内网地址,外网用户查同一个域名返回公网地址。这两个技术组合在一起,基本能覆盖中小规模网络环境里DNS的高可用和内外网隔离需求。
这套方案适合谁?我总结下来是三类人:一是负责企业内网运维的工程师,需要保证内网DNS稳定;二是做IDC或云环境的技术人员,需要给不同网络区域的用户返回不同解析结果;三是自己搭实验环境学习DNS原理的爱好者。不管你基础如何,只要跟着步骤走,这套配置是能直接落地的。
我下面会从整体设计思路讲起,然后拆解主从同步和分离解析的核心配置,再给出一套完整的实操流程,最后把我踩过的坑和排查经验整理出来。内容比较长,建议先收藏,配环境的时候对着看。
2. 整体架构设计与方案选型思路
2.1 为什么选主从加视图这套组合
DNS服务器的高可用方案其实有好几种。最简单的可以用多台独立DNS服务器,每台手动配一样的记录,但这样维护成本极高,改一条记录要登好几台机器。也可以用负载均衡设备在前面挡一层,后面挂多台DNS,但负载均衡本身又成了单点。相比之下,主从架构的优势在于记录只在主服务器上维护,从服务器自动同步,既保证了数据一致性,又实现了查询层面的冗余。
那分离解析为什么也要加进来?因为很多场景下内外网需要不同的解析结果。举个典型例子:公司内网有一台文件服务器,内网访问走192.168段的地址,外网员工访问要走公网地址。如果只有一套解析记录,要么内网用户绕公网走一圈,要么外网用户拿到内网地址根本连不上。分离解析就是解决这个问题的。
我选择的是BIND这套DNS软件来实现,原因很直接:它是目前最成熟、文档最全的开源DNS服务器实现,主从同步和视图功能都支持得很好,社区里遇到问题也容易找到答案。虽然现在也有一些新的DNS服务端软件,但BIND的稳定性和功能完整度在实战中还是最让人放心的。
2.2 网络拓扑与角色划分
在动手之前,先把架构想清楚。我这套方案里涉及三台服务器(虚拟机也行):
| 角色 | 作用 | 说明 |
|---|---|---|
| 主DNS服务器 | 维护所有区域记录,响应从服务器同步请求 | 同时配置内外网视图 |
| 从DNS服务器 | 自动从主服务器同步记录,对外提供查询 | 同样配置内外网视图 |
| 测试客户端 | 验证解析结果 | 分别从内外网段发起查询 |
主从之间的同步走的是DNS协议的区域传送机制。从服务器会定期检查主服务器上区域的SOA记录序列号,如果发现序列号变大了,就发起一次区域传送把最新记录拉过来。这个机制的好处是自动化,你只需要在主服务器上改记录、升序列号,从服务器就会自己更新。
注意:主从服务器的时间要同步,否则SOA记录里的时间戳比对可能出问题。建议都配上时间同步服务。
2.3 分离解析的实现原理
分离解析在BIND里是通过view(视图)来实现的。每个view里可以定义一组区域,并且通过match-clients参数指定这个视图匹配哪些来源IP。当DNS服务器收到查询请求时,会按照view的定义顺序去匹配,找到第一个匹配的view就用那个view里的区域来回答。
这里有个关键点:view的匹配顺序很重要。BIND会按配置文件里view出现的顺序依次匹配,一旦匹配上就不再往下找。所以通常把范围小的view放在前面,范围大的放在后面。比如内网视图匹配192.168.0.0/16,外网视图匹配any,那内网视图必须写在前面,否则所有请求都会被外网视图截获。
还有一个容易忽略的点:一旦使用了view,所有区域都必须定义在view里面,不能再有裸露在view外面的zone配置。包括根区域、localhost这些,都要分别写进每个view。这个坑我后面会详细说。
3. 主从同步的核心配置细节
3.1 主服务器区域配置要点
主服务器的配置文件里,每个需要同步的区域都要标明类型是master,并且用allow-transfer指定允许哪些从服务器来拉取记录。我一般还会配上also-notify,这样主服务器记录一变更就主动通知从服务器来同步,不用等从服务器轮询。
# 主服务器 named.conf 中某个区域的配置示例 zone "example.internal" IN { type master; file "/etc/named/zones/example.internal.zone"; allow-transfer { 192.168.1.12; }; also-notify { 192.168.1.12; }; notify yes; };这里allow-transfer里写的是从服务器的IP。生产环境建议用密钥认证(TSIG)而不是单纯靠IP限制,因为IP是可以伪造的。不过实验环境用IP就够了,先把流程跑通。
区域文件本身有几个关键参数必须注意。SOA记录里的序列号是主从同步的核心,每次修改记录后必须增大这个值,否则从服务器不会触发同步。序列号的格式通常是YYYYMMDDnn,比如2025011501,表示2025年1月15日的第1次修改。这个格式的好处是直观,一看就知道是什么时候改的。
# 区域文件中的SOA记录示例 $TTL 86400 @ IN SOA ns1.example.internal. admin.example.internal. ( 2025011501 ; 序列号,每次修改必须增大 3600 ; 刷新时间,从服务器多久检查一次 1800 ; 重试时间,同步失败后多久重试 604800 ; 过期时间,从服务器多久没同步就放弃 86400 ; 最小TTL )刷新时间我设的是3600秒,也就是从服务器每小时检查一次主服务器的序列号。如果对同步实时性要求高,可以调小到300秒甚至更短。但调太小会增加主服务器负担,需要权衡。重试时间1800秒是同步失败后的重试间隔,这个不用设太短,避免网络抖动时频繁重试。
3.2 从服务器配置与同步验证
从服务器的配置相对简单,区域类型设为slave,然后用masters指定主服务器地址。从服务器不需要自己维护区域文件,BIND会自动把同步过来的记录存到file指定的路径。
# 从服务器 named.conf 中对应区域的配置 zone "example.internal" IN { type slave; file "/etc/named/slaves/example.internal.zone"; masters { 192.168.1.11; }; };配置完成后重启BIND服务,从服务器会自动向主服务器发起一次区域传送。你可以通过查看从服务器的日志来确认同步是否成功。同步成功后,file指定的路径下会出现区域文件,这个文件是BIND自动生成的,不要手动去改。
验证同步是否正常,我常用的方法是两个:一是看日志里有没有"transfer of 'example.internal/IN' from 192.168.1.11#53: Transfer completed"这样的记录;二是直接在从服务器上用dig命令查询一条记录,看能不能返回正确结果。
# 在从服务器上验证解析 dig @localhost www.example.internal A +short如果返回了正确的IP,说明同步没问题。如果返回空或者报错,就要去查日志排查了。
实操心得:从服务器的区域文件目录权限要设对,BIND进程用户必须有写权限,否则同步过来的文件存不下去。我见过好几次因为目录权限问题导致同步失败的案例。
3.3 主从同步的常见故障点
主从同步出问题,排查思路其实很固定。先确认从服务器能不能连到主服务器的53端口,再确认主服务器的allow-transfer有没有放行从服务器IP,然后看序列号有没有增大,最后看区域文件语法有没有错误。
有一个特别隐蔽的坑:区域文件里的域名结尾有没有加点。在BIND的区域文件里,完全限定域名(FQDN)必须以点结尾,比如ns1.example.internal.。如果忘了加点,BIND会把它当成相对域名,自动拼接上当前区域名,变成ns1.example.internal.example.internal.,这就错了。这种错误不会导致BIND启动失败,但解析结果会不对,排查起来很费时间。
另一个常见问题是序列号忘了改。改完记录直接重启BIND,主服务器自己解析是新记录,但从服务器死活不同步。原因就是序列号没变,从服务器认为数据没更新。养成习惯:改完区域文件第一件事就是升序列号。
4. 分离解析的配置与实操
4.1 视图配置的整体结构
分离解析的配置全部围绕view展开。一个典型的双视图配置结构是这样的:
# 内网视图,匹配内网网段 view "internal" { match-clients { 192.168.0.0/16; 10.0.0.0/8; }; recursion yes; zone "example.internal" IN { type master; file "/etc/named/zones/internal/example.internal.zone"; }; zone "." IN { type hint; file "/etc/named/zones/internal/named.ca"; }; zone "localhost" IN { type master; file "/etc/named/zones/internal/localhost.zone"; }; }; # 外网视图,匹配所有其他来源 view "external" { match-clients { any; }; recursion no; zone "example.internal" IN { type master; file "/etc/named/zones/external/example.internal.zone"; }; zone "." IN { type hint; file "/etc/named/zones/external/named.ca"; }; zone "localhost" IN { type master; file "/etc/named/zones/external/localhost.zone"; }; };注意每个view里面都要把根区域、localhost这些基础区域重新定义一遍。这就是前面提到的坑:用了view之后,所有zone都必须在view内部,不能有遗漏。漏了哪个,那个view里就查不到对应的解析。
4.2 内外网区域文件的差异化配置
两个视图里的区域文件内容可以完全不同。内网视图的区域文件里,所有记录都指向内网地址;外网视图的区域文件里,记录指向公网地址。
# 内网视图的区域文件片段 www IN A 192.168.1.100 mail IN A 192.168.1.101 ftp IN A 192.168.1.102 # 外网视图的区域文件片段 www IN A 203.0.113.10 mail IN A 203.0.113.11 ftp IN A 203.0.113.12这样配置之后,内网用户查www.example.internal拿到的是192.168.1.100,外网用户查同一个域名拿到的是203.0.113.10。两边互不干扰。
注意:外网视图里通常要关闭递归(recursion no),只回答自己权威区域的查询,避免被利用做DNS放大攻击。内网视图可以开递归,方便内网用户查询外部域名。
4.3 视图匹配的验证方法
配置完成后怎么验证视图有没有生效?最直接的方法是从不同网段的机器上分别发起查询,看返回结果是否不同。
# 从内网机器查询 dig @192.168.1.11 www.example.internal A +short # 预期返回 192.168.1.100 # 从外网机器查询 dig @203.0.113.11 www.example.internal A +short # 预期返回 203.0.113.10如果两边返回的结果一样,说明视图匹配有问题。排查方向:先确认match-clients的网段写对了没有,再确认view的顺序对不对,最后看查询请求的源IP是不是真的匹配到了预期的view。
BIND提供了一个很好用的调试手段:在named.conf里加上querylog yes;,然后看日志里每条查询匹配到了哪个view。日志里会明确写出类似"client 192.168.1.50#12345: view internal: query: www.example.internal IN A"这样的记录,一眼就能看出匹配结果。
4.4 主从加视图的联合配置
把主从和视图结合起来的时候,配置会稍微复杂一点。每个view里都要分别定义master和slave区域。主服务器的内网视图区域类型是master,从服务器的内网视图区域类型是slave,外网视图同理。
# 主服务器内网视图中的区域 view "internal" { match-clients { 192.168.0.0/16; }; zone "example.internal" IN { type master; file "/etc/named/zones/internal/example.internal.zone"; allow-transfer { 192.168.1.12; }; also-notify { 192.168.1.12; }; }; }; # 从服务器内网视图中的区域 view "internal" { match-clients { 192.168.0.0/16; }; zone "example.internal" IN { type slave; file "/etc/named/slaves/internal/example.internal.zone"; masters { 192.168.1.11; }; }; };这里有个细节:主从同步的区域传送请求本身也是从从服务器发起的,源IP是从服务器的IP。如果从服务器的IP不在主服务器内网视图的match-clients范围内,那同步请求可能会匹配到外网视图,导致同步失败。所以要么确保从服务器IP在内网视图匹配范围内,要么在主服务器的外网视图里也放行从服务器的同步请求。
我一般会把从服务器的IP明确加到内网视图的match-clients里,这样最稳妥。
5. 完整实操流程与参数计算
5.1 环境准备与软件安装
假设我们有两台服务器,主DNS的IP是192.168.1.11,从DNS的IP是192.168.1.12。操作系统以常见的Linux发行版为例。
# 两台服务器都安装BIND # 基于Debian系的系统 apt-get update apt-get install -y bind9 bind9utils bind9-doc # 基于RHEL系的系统 yum install -y bind bind-utils安装完成后,先确认BIND服务能正常启动。默认配置下BIND只监听本地回环地址,我们需要修改监听配置让它监听所有网卡或者指定网卡。
# named.conf.options 中的监听配置 options { listen-on port 53 { any; }; listen-on-v6 { none; }; directory "/var/named"; dump-file "/var/named/data/cache_dump.db"; statistics-file "/var/named/data/named_stats.txt"; recursion yes; dnssec-validation no; };实验环境里我把dnssec-validation关掉了,因为DNSSEC配置比较繁琐,而且和主从同步、视图混在一起容易出问题。生产环境建议开启,但那是另一个话题了。
5.2 主服务器完整配置
主服务器的named.conf整体结构如下:
# 全局选项 options { listen-on port 53 { any; }; directory "/var/named"; recursion yes; allow-query { any; }; dnssec-validation no; }; # 内网视图 view "internal" { match-clients { 192.168.0.0/16; 127.0.0.1; }; recursion yes; zone "example.internal" IN { type master; file "/var/named/zones/internal/example.internal.zone"; allow-transfer { 192.168.1.12; }; also-notify { 192.168.1.12; }; }; zone "." IN { type hint; file "/var/named/named.ca"; }; zone "localhost" IN { type master; file "/var/named/zones/internal/localhost.zone"; }; zone "0.0.127.in-addr.arpa" IN { type master; file "/var/named/zones/internal/127.0.0.zone"; }; }; # 外网视图 view "external" { match-clients { any; }; recursion no; zone "example.internal" IN { type master; file "/var/named/zones/external/example.internal.zone"; allow-transfer { 192.168.1.12; }; also-notify { 192.168.1.12; }; }; zone "." IN { type hint; file "/var/named/named.ca"; }; zone "localhost" IN { type master; file "/var/named/zones/external/localhost.zone"; }; };内网视图的区域文件:
$TTL 86400 @ IN SOA ns1.example.internal. admin.example.internal. ( 2025011501 3600 1800 604800 86400 ) @ IN NS ns1.example.internal. @ IN NS ns2.example.internal. ns1 IN A 192.168.1.11 ns2 IN A 192.168.1.12 www IN A 192.168.1.100 mail IN A 192.168.1.101 ftp IN A 192.168.1.102外网视图的区域文件:
$TTL 86400 @ IN SOA ns1.example.internal. admin.example.internal. ( 2025011501 3600 1800 604800 86400 ) @ IN NS ns1.example.internal. @ IN NS ns2.example.internal. ns1 IN A 203.0.113.11 ns2 IN A 203.0.113.12 www IN A 203.0.113.10 mail IN A 203.0.113.11 ftp IN A 203.0.113.12注意两个视图的区域文件序列号要保持一致,这样从服务器同步的时候不会混乱。实际上从服务器会分别同步两个视图的区域文件,各自独立维护序列号。
5.3 从服务器完整配置
从服务器的配置结构和主服务器类似,区别在于区域类型是slave,并且用masters指定主服务器地址。
# 从服务器 named.conf options { listen-on port 53 { any; }; directory "/var/named"; recursion yes; allow-query { any; }; dnssec-validation no; }; view "internal" { match-clients { 192.168.0.0/16; 127.0.0.1; }; recursion yes; zone "example.internal" IN { type slave; file "/var/named/slaves/internal/example.internal.zone"; masters { 192.168.1.11; }; }; zone "." IN { type hint; file "/var/named/named.ca"; }; zone "localhost" IN { type master; file "/var/named/zones/internal/localhost.zone"; }; }; view "external" { match-clients { any; }; recursion no; zone "example.internal" IN { type slave; file "/var/named/slaves/external/example.internal.zone"; masters { 192.168.1.11; }; }; zone "." IN { type hint; file "/var/named/named.ca"; }; zone "localhost" IN { type master; file "/var/named/zones/external/localhost.zone"; }; };从服务器的slaves目录需要提前创建好,并且给BIND进程用户写权限。
mkdir -p /var/named/slaves/internal mkdir -p /var/named/slaves/external chown -R named:named /var/named/slaves5.4 配置检查与启动
BIND提供了一套配置检查工具,在启动前一定要先检查配置文件语法。
# 检查主配置文件语法 named-checkconf /etc/named.conf # 检查区域文件语法 named-checkzone example.internal /var/named/zones/internal/example.internal.zone named-checkzone example.internal /var/named/zones/external/example.internal.zonenamed-checkconf没有输出就表示语法没问题。named-checkzone会输出区域的序列号、记录数量等信息,如果有语法错误会明确报出来。
确认无误后启动服务:
# 启动BIND systemctl start named # 或者 systemctl start bind9 # 设置开机自启 systemctl enable named启动后查看日志确认没有报错:
journalctl -u named -f # 或者 tail -f /var/log/messages | grep named5.5 同步与解析验证
主从都启动后,在从服务器上观察同步日志。正常情况下会看到区域传送完成的记录。然后在从服务器上查询记录,确认能返回正确结果。
# 从服务器上查询内网视图记录 dig @192.168.1.12 www.example.internal A +short # 预期返回 192.168.1.100 # 从服务器上查询外网视图记录(需要从外网网段发起) dig @192.168.1.12 www.example.internal A +short # 如果从内网发起,返回的仍然是内网地址,因为匹配到了内网视图这里有个验证上的细节:从服务器本身在内网网段,所以从从服务器上直接dig,匹配的是内网视图。要验证外网视图,需要从外网网段的机器发起查询,或者用dig的-b参数指定源IP(如果从服务器有多个网卡)。
# 指定源IP查询,模拟外网请求 dig -b 203.0.113.100 @192.168.1.12 www.example.internal A +short5.6 序列号更新与同步触发实操
日常维护中最常见的操作就是改记录。流程是:改主服务器区域文件,升序列号,重载配置,然后等从服务器自动同步。
# 1. 编辑主服务器区域文件,修改记录 vi /var/named/zones/internal/example.internal.zone # 2. 修改序列号,比如从2025011501改成2025011502 # 3. 检查区域文件语法 named-checkzone example.internal /var/named/zones/internal/example.internal.zone # 4. 重载BIND配置 rndc reload # 5. 在从服务器上观察同步日志 tail -f /var/log/messages | grep transferrndc reload会重新加载所有区域,如果只想重载单个区域可以用rndc reload example.internal。重载后主服务器会通过also-notify主动通知从服务器,从服务器收到通知后发起区域传送。
实操心得:如果从服务器没有及时同步,可以先手动在从服务器上执行
rndc retransfer example.internal强制发起一次区域传送,用来确认主从通道是否正常。如果手动能同步但自动不行,那问题多半出在notify或者序列号上。
6. 常见问题排查与避坑指南
6.1 主从同步失败排查速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 从服务器日志无同步记录 | 主服务器allow-transfer未放行 | 检查主服务器allow-transfer配置 |
| 同步报"transfer failed" | 网络不通或防火墙拦截 | telnet主服务器53端口测试 |
| 同步成功但解析结果旧 | 序列号未增大 | 检查SOA序列号是否比从服务器大 |
| 从服务器区域文件为空 | 目录权限不足 | 检查slaves目录属主和权限 |
| 同步时好时坏 | 网络抖动或主服务器负载高 | 查看主服务器负载和网络质量 |
6.2 视图匹配异常排查
视图匹配出问题,最典型的表现是内外网返回了相同的解析结果。排查步骤我一般按这个顺序来:
先确认请求源IP到底是多少。有时候你以为是从内网发起的请求,实际上走了代理或者NAT,源IP变成了外网地址。用dig加上+subnet参数或者直接在服务器上抓包看源IP最准确。
再确认view的顺序。BIND按顺序匹配,第一个匹配上的view生效。如果外网视图写在了内网视图前面,那所有请求都会被外网视图截获。这个错误很隐蔽,因为配置文件语法检查不会报错。
最后确认match-clients的网段写法。BIND支持CIDR格式,比如192.168.0.0/16,也支持单个IP和网段范围。写错了不会报错,但匹配不上。
6.3 那些年我踩过的坑
坑一:忘了在view里定义根区域。用了view之后,根区域必须在每个view里重新定义。我一开始只在内网视图里定义了根区域,外网视图忘了加,结果外网用户查询外部域名全部失败。因为外网视图里没有根区域,BIND不知道怎么去迭代查询。
坑二:从服务器的视图配置和主服务器不一致。主服务器内网视图匹配192.168.0.0/16,从服务器写成了192.168.1.0/24。结果从服务器从192.168.2.0/24网段收到的查询匹配到了外网视图,返回了公网地址。这种问题在测试阶段不容易发现,因为测试机通常都在同一个网段。
坑三:区域文件里的域名忘了加点。这个前面提过,但真的太常见了。ns1.example.internal和ns1.example.internal.在BIND眼里是两个完全不同的东西。前者会被拼接成ns1.example.internal.example.internal.。养成习惯:区域文件里所有FQDN都加点结尾。
坑四:序列号改小了。有一次我手误把序列号从2025011502改成了2025011401,从服务器认为主服务器的数据比自己的旧,拒绝同步。BIND的序列号比较用的是RFC 1982定义的序列号算术,不是简单的数值比较。改小了会导致同步逻辑混乱。所以改序列号只能往大了改。
坑五:防火墙只开了TCP 53没开UDP 53。DNS查询默认走UDP,区域传送走TCP。如果只开了TCP,普通查询会失败;只开了UDP,区域传送会失败。两个都要开。
6.4 性能与安全优化建议
主从架构下,从服务器承担了大部分查询压力,主服务器主要负责记录维护和同步。可以在从服务器上适当调大缓存,减少对主服务器的查询依赖。
安全方面,allow-transfer一定要限制来源,不要写成any。区域传送如果对所有人开放,相当于把整个区域的记录都暴露了。生产环境建议用TSIG密钥做区域传送认证,比单纯靠IP限制更安全。
外网视图记得关闭递归。开放递归的DNS服务器容易被利用做DNS放大攻击。内网视图可以开递归,方便内网用户。
提示:如果内网用户也需要查询外部域名,可以在内网视图里配置forwarders指向上游DNS,这样内网查询外部域名时由内网视图转发出去,既方便又可控。
7. 日常维护与扩展思路
日常维护主要就是改记录、升序列号、重载配置这三步。我习惯在改之前先备份区域文件,改完用named-checkzone检查语法,确认无误再重载。重载后去从服务器确认同步完成,整个流程走完才算结束。
如果区域比较多,手动改序列号容易忘,可以写个脚本自动生成序列号。最简单的做法是用日期加序号,每次改完自动递增。也可以用rndc的一些高级功能来辅助管理。
扩展方面,这套架构可以进一步做这些事:加第二台从服务器做负载分担;用TSIG密钥加固主从同步;配置DNS查询日志分析,了解哪些域名被查询得最多;结合监控系统对DNS服务做健康检查,主服务器故障时自动把从服务器提升为主。这些都是在现有基础上可以逐步完善的。
我个人在实际操作中的体会是,DNS这东西配置本身不复杂,但细节特别多,一个标点符号错了都可能导致解析异常。所以每次改完一定要验证,主服务器验证完从服务器也要验证,内网验证完外网也要验证。养成这套验证习惯之后,基本就不会出大问题了。