news 2026/10/28 3:20:41

Redis内网域名解析实战:用DNS服务告别IP硬编码

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis内网域名解析实战:用DNS服务告别IP硬编码

上周半夜收到一条消息,对方说他们把那套 Redis 从旧机房整体挪到了新机房,IP 段全换了,结果第二天早上十七个业务服务的配置文件挨个改,改到第八个的时候手抖改错了一个端口号,线上报警响了四十分钟。这事听起来挺蠢,但在 IP 直连的场景里几乎是必然会发生的:只要地址是硬编码的,任何一次搬迁、扩容、故障切换,都会变成一次跨团队的配置同步运动。

这篇东西讲的就是怎么把这件事一次性解决掉——在内网搭一套自己的 DNS 服务,给 Redis 配上自定义域名,让业务侧永远只认redis-01.cache.lab.local这种名字,机房搬迁、主从切换、IP 调整全部在 DNS 层消化掉。核心关键词是 DNS 服务、自定义域名、Redis、内网域名解析,这几样东西凑在一起,看起来简单,真做起来坑不少。

内容适合两类人看:一类是手里管着几套中间件、被 IP 变更折腾过的运维或后端;另一类是刚接手一个“配置文件里全是 10.x.x.x”的老项目、想理清楚这块该怎么改的开发者。不需要你对 BIND 有多熟,但至少要能看懂 Linux 的网络配置和 Redis 的基本连接参数。下面从为什么要做、怎么选型、怎么落地、怎么排错一路讲下去,中间会穿插一些我自己踩过的坑,能让你少走点弯路。

1. 给Redis配域名的真实收益与方案取舍

1.1 硬编码IP的隐性成本比想象中高

很多人觉得 IP 直连没什么问题,“反正内网地址又不会变”。但只要系统活过一年,你就会发现它一定会变:机房搬迁、网段规划调整、云上可用区迁移、主从角色互换、容器化改造后 Pod 网段重排,任何一件事都会让那串数字失效。而 IP 一旦写进配置文件,它就不再是一个技术细节,而是一份散落在 N 个仓库、M 份配置中心条目、K 个脚本里的隐式契约。

这份契约的维护成本是隐性的但很贵。改一次 IP,你要找出所有引用点,要协调各个业务团队的发版窗口,要评估哪些服务是灰度发、哪些是全量发,还要防止某个边缘服务漏改导致半夜报警。更麻烦的是故障切换场景:主库挂了要切到从库,如果客户端写死的是主库 IP,切换就意味着一次配置变更加一次重启,而重启本身又可能带来雪崩。用域名的话,改一条 A 记录,TTL 到期后新连接自动落到新地址,业务代码一行不动。

还有一个容易被忽略的点:可读性。10.20.30.21:6379和order-cache-01.cache.lab.local:6379,后者一眼就能看出这是订单业务的缓存主节点,前者要去翻资产表。当你在凌晨三点看日志排障的时候,这个差别很要命。

1.2 四条可选路线摆在一起比一比

给 Redis 配域名不是只有一条路,实际能落地的方案大概有四种,各自适用场景差别很大,选错了后面会很别扭。

方案实现方式变更生效速度运维成本适用规模
本地 hosts 文件每台机器维护/etc/hosts极慢,需逐台改机器越多越崩溃单机调试、临时验证
内网权威 DNS自建 BIND/dnsmasq/CoreDNS快,改记录即可一次性投入,长期低十台以上机器、多团队
配置中心 + 客户端 SDKNacos/Apollo 下发地址快,但依赖 SDK中,需改造业务已经统一配置中心的团队
服务注册发现Consul/Etcd + 客户端解析最快,秒级高,组件多大规模微服务、动态扩缩容

hosts方案我建议只在两种情况下用:一是在正式上 DNS 之前做一次小范围验证,确认解析链路没问题;二是单机开发环境图个方便。千万别在超过五台机器的环境里用 hosts 管 Redis 地址,那玩意儿改起来比改配置文件还痛苦,而且极容易出现“某台机器上还留着一条旧记录”的幽灵问题——因为hosts文件的优先级高于 DNS,残留记录会静默覆盖掉正确的解析结果,排查起来非常费劲。

配置中心方案的好处是能做灰度、能做版本回滚、能带元数据,但缺点是业务代码得引入对应 SDK,老项目改造成本不低。服务注册发现最适合容器化、实例频繁上下线的场景,但如果你的 Redis 是几台固定的物理机或虚机,上 Consul 属于杀鸡用牛刀。

1.3 为什么最后选了内网权威DNS

我自己的判断标准很简单:如果地址变更的频率低于每周一次,但影响面涉及多个团队,那 DNS 就是性价比最高的解。它不侵入业务代码,所有语言所有框架天然支持,变更动作收敛到一个地方,出问题也能用dig这种通用工具排查。

具体到 Redis 这个场景,内网权威 DNS 还有一个额外好处:它可以和 Redis 的高可用组件配合。比如你用 Sentinel 做故障转移,客户端连的是sentinel-01.cache.lab.local:26379这种域名,Sentinel 返回的主节点地址也可以通过 Redis 自身的replica-announce-ip参数改造成域名,整条链路里就不再有裸 IP 了。这部分后面第 4 章会详细展开,是很多人做完 DNS 之后发现“怎么还是 IP”的关键所在。

需要说清楚的是这套方案不适合什么场景:如果你的 Redis 实例是按小时级别动态创建销毁的,那 DNS 的记录管理会成为负担,这时候应该用服务发现或者云厂商的托管方案;如果你的团队只有两个人、三台机器,那老老实实写 IP 加个注释更省事,别为了架构而架构。

2. 整体架构与几个必须先定下来的决策

2.1 组件清单和一次连接请求的完整路径

整套东西其实就三个角色:DNS 服务端(负责应答)、Redis 服务端(被解析的目标)、客户端(发起连接的一方)。但要把链路走通,中间还牵扯到操作系统的名称解析栈,这一段经常被忽略,也是大部分“DNS 明明配了却解析不了”的根源。

客户端发起redis-01.cache.lab.local:6379连接时,实际发生的过程是这样的:应用程序调用getaddrinfo(),glibc 根据/etc/nsswitch.conf里的hosts:行决定查询顺序,通常是files dns,也就是先查/etc/hosts,没有再走 DNS。走 DNS 时会读取/etc/resolv.conf里的nameserver列表和search域,向第一个 nameserver 发查询,超时(默认 5 秒)后切下一个。拿到 A 记录后再发起 TCP 连接。

这条链路上任何一环出问题,表现都是“连不上”,但原因可能完全不同。所以我一般在验证的时候不会只用ping,而是用getent hosts 域名,因为它走的就是应用程序实际使用的那套 NSS 路径,能真实反映应用会拿到什么结果。dig只验证 DNS 服务器本身答得对不对,验证不了客户端侧的解析栈。

至于部署形态,最小可用版本是一台 DNS 服务器 + 若干 Redis 节点 + N 台客户端。但一台 DNS 就是单点,所以生产上至少要两台,主备或者双主都行。这个后面第 6 章会讲。

2.2 域名规划:后缀、层级和命名的讲究

域名规划这事看着随意,其实埋着雷。第一件事是后缀选什么。绝对不要用公网真实存在的域名,也不要用.local——.local是 mDNS(多播 DNS)的保留后缀,在 macOS 和一些启用了 avahi/nss-mdns 的 Linux 上会被本地多播解析器接管,导致解析行为诡异。我吃过这个亏:在一台开发机上dig能查到,但应用连不上,查了半天发现是 mDNS 抢在了前面。

推荐用.internal、.lan、.intra这类不会被公网解析的后缀,或者用公司自有域名下的一个子域,比如cache.lab.local、redis.prod.internal。前者简单直接,后者便于分层管理。

第二件事是层级设计。建议按“业务 + 环境 + 角色 + 序号”来组织,比如:

  • order-cache-01.prod.internal—— 订单业务生产环境缓存主节点
  • order-cache-02.prod.internal—— 订单业务生产环境缓存从节点
  • order-cache.prod.internal—— 订单缓存的统一入口(指向 VIP 或哨兵)
  • order-cache-01.test.internal—— 测试环境对应节点

这么设计的好处是,看到域名就知道它在哪、干什么的,而且可以用泛解析或者通配符记录来减少维护量。但通配符记录要慎用,*.prod.internal全部指向同一个 IP 这种做法在排障时会掩盖真实问题,你会分不清到底是这个服务不存在还是它真的指向那台机器。

第三件事是命名里别带会变的信息。我见过有人用redis-10-20-30-21.prod.internal这种把 IP 编进名字的做法,初衷是方便定位,结果机器一换 IP 名字就对不上了,反而制造混乱。名字应该描述角色,不描述位置。

2.3 TTL、连接池和故障切换之间的三角关系

这一节是整篇文章里最容易被忽略、但实际影响最大的部分。很多人配完 DNS 之后发现“切换了记录,业务还是连的旧地址”,就是因为没搞懂 TTL 和连接池的关系。

关键认知:DNS 的 TTL 只影响“新建连接”的解析,不影响已经建立的 TCP 长连接。Redis 客户端普遍使用连接池,一条连接建好之后可能存活几十分钟甚至几小时,期间无论 DNS 记录怎么变,这条连接都不会重新解析,也不会自动断开。

所以 TTL 设置要遵循一个原则:TTL 要略小于客户端的连接池最大存活时间(max lifetime)。比如 TTL 设 60 秒,连接池的 max-lifetime 设 300 秒,那么一次记录变更最坏情况下 5 分钟内全部生效;反过来如果 TTL 设 300 秒、连接池不设上限(很多客户端默认就是无限),那切换可能要等到进程重启才生效,DNS 就白做了。

具体数值建议:TTL 设 30~60 秒,连接池 max-lifetime 设 5~10 分钟,连接池的 idle-timeout 设 30~60 秒。TTL 不要设太短,比如 5 秒,那会导致 DNS 查询量暴增,而且很多操作系统的解析器本身有缓存,短 TTL 没什么意义。

还有一个坑:JVM 有自己的 DNS 缓存,绕过了系统 TTL。Java 的InetAddress缓存由networkaddress.cache.ttl控制,不同 JDK 版本和是否有 SecurityManager 会带来不同默认值,有些环境下默认是永久缓存。所以 Java 系的应用必须显式设置,启动参数加-Dnetworkaddress.cache.ttl=60 -Dnetworkaddress.cache.negative.ttl=5,或者在应用启动最早阶段调用java.security.Security.setProperty("networkaddress.cache.ttl", "60")。这一条不做,前面所有 TTL 设计都是空的。

3. 从零搭一套内网DNS服务

3.1 BIND9的最小可用配置

以最常见的 BIND9 为例,Debian/Ubuntu 系apt install bind9 bind9-utils dnsutils,RHEL 系yum install bind bind-utils。装完之后不要急着启动,先把配置文件改对。

/etc/bind/named.conf.options里的核心是这几项:

options { directory "/var/cache/bind"; listen-on port 53 { 127.0.0.1; 10.20.30.11; }; listen-on-v6 { none; }; allow-query { 10.20.30.0/24; 127.0.0.1; }; recursion yes; allow-recursion { 10.20.30.0/24; 127.0.0.1; }; dnssec-validation no; forwarders { 10.20.30.1; }; };

逐项解释一下,每一项我都踩过对应的坑。

listen-on一定要显式写内网 IP 和 127.0.0.1,不要图省事写any。默认配置里 BIND 只监听 127.0.0.1,你装完发现其他机器查不到,八成就是这个原因。

allow-query限制查询来源网段,防止被内网其他不该用的人蹭,也降低被当成 DNS 放大器的风险。如果只做权威解析,recursion no;更安全;但这里有个大坑:如果你打算让客户端把/etc/resolv.conf直接指向这台机器,那它必须能递归解析公网域名,否则客户端配上去之后所有外网域名全挂。我见过不止一次“加了台内网 DNS,结果全公司上不了外网”的事故,就是recursion no加forwarders空导致的。

我的建议是:这台机器同时承担递归和权威两个角色,recursion yes但用allow-recursion严格限制来源。forwarders指向上游可用 DNS,如果内网完全隔离就留空。

dnssec-validation no;这条特别说一下。BIND 默认开启 DNSSEC 验证,在纯内网、无法访问根服务器的环境里,会导致启动慢、查询超时甚至SERVFAIL。内网权威区自己做 DNSSEC 的团队很少,直接关掉最省心。

3.2 区域文件怎么写:从SOA到A记录

先在/etc/bind/named.conf.local里声明区域:

zone "cache.lab.local" { type master; file "/etc/bind/db.cache.lab.local"; allow-transfer { 10.20.30.12; }; };

allow-transfer是给从库用的,如果暂时单机可以先去掉,但留着也无妨,至少限制到指定 IP。

然后是区域文件/etc/bind/db.cache.lab.local:

$TTL 60 $ORIGIN cache.lab.local. @ IN SOA ns1.cache.lab.local. hostmaster.cache.lab.local. ( 2024052001 ; serial 3600 ; refresh 600 ; retry 604800 ; expire 60 ; minimum ) @ IN NS ns1.cache.lab.local. ns1 IN A 10.20.30.11 redis-01 IN A 10.20.30.21 redis-02 IN A 10.20.30.22 redis-03 IN A 10.20.30.23 sentinel-01 IN A 10.20.30.31 sentinel-02 IN A 10.20.30.32 sentinel-03 IN A 10.20.30.33

几个要点值得展开。

serial 号的格式是YYYYMMDDnn,每次改区域文件必须递增,否则从库不会同步。我习惯写成日期加两位序号,比如2024052001、2024052002。刷新前忘了改 serial 是最常见的人为失误,改完记录rndc reload之后发现从库没更新,查半天就是这个原因。有个小技巧:在部署脚本里自动根据日期生成 serial,能规避掉一大半问题。

SOA 里的 minimum 和 $TTL 的关系:在 RFC 2308 之后,SOA 的最后一个字段表示的是否定应答的缓存时间(也就是 NXDOMAIN 缓存多久),而不是整个区域的最小 TTL。两个都设 60,语义清晰。

$ORIGIN的作用是定义相对名称的基准,有了它redis-01会被自动补全成redis-01.cache.lab.local.。注意末尾那个点,区域文件里的完全限定名必须以点结尾,漏了点会变成redis-01.cache.lab.local.cache.lab.local.,这个错我年轻时犯过,查了一下午。

写完先做语法检查再重载,别直接 reload:

named-checkconf /etc/bind/named.conf named-checkzone cache.lab.local /etc/bind/db.cache.lab.local systemctl reload named

named-checkzone会告诉你 serial 是否合法、记录格式有没有问题,比直接 reload 后看日志快得多。

3.3 轻量替代:dnsmasq和CoreDNS怎么选

BIND 功能全但配置繁琐。如果你的需求只是“把几个域名解析到固定 IP”,dnsmasq 更合适,配置文件一行搞定:

# /etc/dnsmasq.d/redis.conf address=/redis-01.cache.lab.local/10.20.30.21 address=/redis-02.cache.lab.local/10.20.30.22 local-ttl=60

注意address=的匹配规则是域名及其所有子域,address=/cache.lab.local/10.20.30.21会把整个域都指向同一个 IP,所以精确到主机名更安全。local-ttl控制本地应答的 TTL,不设的话默认是 0,也就是不缓存,每次都要问,量大了会有压力。

还有一个更结构化的写法,用 hosts 格式的文件:

# /etc/dnsmasq.d/redis.hosts 10.20.30.21 redis-01.cache.lab.local 10.20.30.22 redis-02.cache.lab.local

然后主配置里加addn-hosts=/etc/dnsmasq.d/redis.hosts。这种写法的好处是格式和/etc/hosts一致,运维同学不用学新语法,而且可以复用现有的资产管理脚本自动生成。

如果环境是 Kubernetes,那基本就是 CoreDNS 的天下了。在 CoreDNS 的 ConfigMap 里加一段:

cache.lab.local:53 { hosts { 10.20.30.21 redis-01.cache.lab.local 10.20.30.22 redis-02.cache.lab.local fallthrough } }

或者用forward把整个域转发给你自建的 BIND:

cache.lab.local:53 { forward . 10.20.30.11 cache 30 }

这里要提醒一句:容器的 DNS 行为和宿主机不一样。Docker 默认会把容器的/etc/resolv.conf指向127.0.0.11这个内置解析器,再由它转发到宿主机配置的 DNS。如果你的内网 DNS 只在宿主机/etc/resolv.conf里配了,容器里可能查不到,需要在 Docker daemon 配置里显式指定dns参数。K8s 里则是节点上的kubelet把/etc/resolv.conf传给 CoreDNS 作为上游,如果节点用的是 systemd-resolved 的127.0.0.53,CoreDNS 会转发失败,因为那个地址在 Pod 网络里不可达。这种情况必须在节点上把真实 DNS 地址写进/run/systemd/resolve/resolv.conf并在 kubelet 里指定。

3.4 客户端接入与解析验证的正确姿势

DNS 服务起来之后,客户端要做的就是把/etc/resolv.conf里加上这台服务器:

nameserver 10.20.30.11 nameserver 10.20.30.12 search cache.lab.local options ndots:2 timeout:2 attempts:2

search域的作用是让你能直接用短名redis-01访问,系统会自动补上后缀。但它也是坑的来源:如果ndots设得太大(默认是 1),系统会先尝试把redis-01.cache.lab.local当成相对名,去拼redis-01.cache.lab.local.cache.lab.local查一遍,失败后再查原名。每次多一次无效查询,延迟增加,日志里还会出现一堆 NXDOMAIN。解决办法是把ndots调小,或者在代码里写全限定名并带上结尾的点。

很多人改完/etc/resolv.conf发现没生效,八成是碰上了 systemd-resolved。现代发行版里/etc/resolv.conf通常是指向/run/systemd/resolve/stub-resolv.conf的软链接,里面只有一行nameserver 127.0.0.53,你手动改的内容重启后就被覆盖。正确的做法是改/etc/systemd/resolved.conf里的DNS=字段,然后systemctl restart systemd-resolved,用resolvectl status确认。要临时验证也可以直接resolvectl query redis-01.cache.lab.local。

验证顺序我一般是这样走的,每一步都能定位到不同层次的问题:

# 1. DNS 服务器本身答得对不对 dig @10.20.30.11 redis-01.cache.lab.local +short # 2. 客户端解析栈拿到的结果(最贴近应用实际) getent hosts redis-01.cache.lab.local # 3. 端口通不通(排除解析之外的问题) redis-cli -h redis-01.cache.lab.local -p 6379 ping

第一步通了第二步不通,说明客户端侧配置有问题(resolv.conf、nsswitch、systemd-resolved);前两步都通第三步不通,那问题在 Redis 服务端或者网络策略上,跟 DNS 没关系了。

4. Redis侧的配合改造与客户端配置

4.1 监听地址、保护模式与访问控制

DNS 配好了不代表 Redis 就能用域名访问,服务端这边也得配合。redis.conf里最关键的三个参数:

bind 10.20.30.21 127.0.0.1 protected-mode yes port 6379

bind决定了 Redis 监听在哪些地址上。默认只监听 127.0.0.1,其他机器连不上;改成0.0.0.0虽然能连,但等于把所有网卡都暴露出去。正确做法是显式列出内网 IP 加环回地址,域名解析到哪个 IP,这里就绑哪个。

protected-mode是 Redis 3.2 之后引入的保护机制:当它开启、且没有设置密码、且没有配置 bind 时,Redis 会拒绝来自非环回地址的连接。很多人在测试环境遇到“明明 bind 了还是连不上”,或者“telnet 端口通但一执行命令就断开”,就是撞在这个机制上。生产环境不要关它,而是配上密码或者 ACL。

Redis 6 之后推荐用 ACL 替代简单的requirepass:

ACL SETUSER app_order on >强密码串 ~order:* +@read +@write +@keyspace -@dangerous

这条命令创建了一个只能操作order:前缀键、只有读写权限、禁掉FLUSHALL、CONFIG这类危险命令的账号。配合域名使用的时候,这个账号会被写进业务配置,一旦域名指向的节点被误换成测试库,至少 ACL 还能挡住一部分越权操作。

再补一点:如果 Redis 和客户端之间跨了安全组或者防火墙,记得放行的是目标 IP + 6379,不是域名。防火墙不认域名,这是个常识但每年都有人在这一步卡住,反复确认 DNS 配得对不对。

4.2 让Sentinel和Cluster也返回域名

这是整篇文章里最“反直觉”的部分。很多人以为把客户端连接地址改成域名就完事了,结果发现 Sentinel 模式下客户端还是连到了 IP 上——因为Sentinel 返回的主节点地址是由 Redis 实例自己上报的,默认是 IP,不是你连 Sentinel 时用的域名。

原因在于 Sentinel 通过INFO replication获取从节点信息,从节点上报的地址默认取本机网卡 IP。要让它上报域名,需要在每个 Redis 实例的配置里加:

replica-announce-ip redis-02.cache.lab.local replica-announce-port 6379

老版本(Redis 5 之前)这个参数叫slave-announce-ip,升级的时候会被自动兼容但配置里最好改成新名字。设置完成后,INFO replication里master_host和slaveN_ip字段就会显示域名,Sentinel 下发给客户端的也是域名。

这里有个副作用要提前知道:本机用 127.0.0.1 连上去看INFO replication,显示的也是域名,有些监控脚本会因此判断异常。另外如果域名解析依赖的那台 DNS 挂了,主从复制本身也会受影响,所以 Redis 节点的/etc/hosts里最好也保留一份静态记录做兜底。

Cluster 模式类似,Redis 6.2 起提供了相关参数:

cluster-announce-hostname redis-03.cache.lab.local cluster-preferred-endpoint-type hostname

cluster-preferred-endpoint-type决定CLUSTER SLOTS、CLUSTER SHARDS返回的是 IP 还是主机名,可选值有ip、hostname、unknown-endpoint。不同版本支持程度有差异,上线前务必用redis-cli cluster slots实际看一眼返回值,别只看文档。我遇到过客户端库版本较老、不认 hostname 类型的 endpoint,导致 MOVED 重定向后连不上,最后只能在客户端侧做地址映射。

4.3 主流客户端的连接配置示例

配置项的名字各框架差别不小,这里列几个常见的,都是可以直接抄的。

Spring Boot 3.x(注意 2.x 和 3.x 的配置前缀不同,2.x 是spring.redis,3.x 改成了spring.data.redis,这个变化坑过不少人):

spring: data: redis: host: redis-01.cache.lab.local port: 6379 password: ${REDIS_PASSWORD} timeout: 2000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 1000ms shutdown-timeout: 200ms

Lettuce 的连接池默认没有 max-lifetime 的概念,需要自己通过ClientOptions或者自定义RedisConnectionFactory来设置。更省事的做法是在基础设施层用commons-pool2的GenericObjectPoolConfig配上maxWait和minEvictableIdleTime,间接控制连接存活时间。

Python 的 redis-py:

import redis pool = redis.ConnectionPool( host="redis-01.cache.lab.local", port=6379, password="***", db=0, socket_connect_timeout=1, socket_timeout=2, health_check_interval=30, max_connections=20, ) client = redis.Redis(connection_pool=pool)

health_check_interval这个参数值得设,它会让连接池在取连接时做一次 PING,能及时发现已经被服务端断掉的死连接,减少ConnectionError的概率。注意 DNS 解析发生在建立连接时,池里已有的连接不会重新解析,所以还是回到 TTL 和连接生命周期的配合问题。

Go 的 go-redis:

rdb := redis.NewClient(&redis.Options{ Addr: "redis-01.cache.lab.local:6379", Password: os.Getenv("REDIS_PASSWORD"), DB: 0, DialTimeout: time.Second, ReadTimeout: 2 * time.Second, WriteTimeout: 2 * time.Second, MaxRetries: 2, })

Go 这边有个细节:标准库的纯 Go 解析器本身不维护 DNS 缓存,每次Dial都会实时查询,所以 TTL 对它是立即生效的。但如果编译时启用了 cgo,就会走 glibc 的解析路径,行为又不一样了。想确认当前二进制用的是哪套,可以设GODEBUG=netdns=go或netdns=cgo强制指定,排查解析相关问题时这个开关很好用。

5. 排查实录:解析对了却连不上的那些情况

5.1 分层排查的固定路径

排这类问题最忌讳东一榔头西一棒子,我一般按固定顺序过一遍,通常五分钟内能定位。

第一层看解析是否成功:dig @DNS服务器 域名 +short有没有返回期望的 IP。没有返回就往下看是 NXDOMAIN 还是 SERVFAIL。NXDOMAIN 说明区域文件里压根没这条记录,或者名字拼错了;SERVFAIL 说明服务器自己出问题了,去看named的日志,常见是区域文件语法错误、serial 没递增、或者 DNSSEC 验证失败。

第二层看客户端拿到的是不是这个结果:getent hosts 域名。如果dig有结果而getent没有,说明客户端侧的 NSS 配置有问题,或者 systemd-resolved 在捣乱,或者/etc/hosts里有冲突记录。

第三层看TCP 层通不通:nc -vz 域名 6379或者redis-cli -h 域名 ping。这一步失败就跟 DNS 无关了,去看 Redis 的 bind 配置、protected-mode、防火墙规则、安全组。

第四层看应用行为:如果命令行能连、应用连不上,那基本是应用侧的连接池配置、DNS 缓存、或者依赖库版本问题。这时候抓包最直接:tcpdump -i any -n port 53看应用到底发了什么 DNS 查询、有没有拿到应答。

5.2 六类高频故障与处理

第一类:/etc/hosts残留记录静默覆盖。现象是某些机器解析结果和其他机器不一致,dig查 DNS 服务器是对的,但getent拿到的是旧 IP。原因是nsswitch.conf里hosts: files dns,files 优先。处理办法是全局搜一遍 hosts 文件,把历史遗留的 Redis 记录清掉,并在规范里明确禁止在 hosts 里写业务地址。

第二类:改了区域文件但没生效。八成是 serial 号没改,从库不会同步;或者是改完没 reload,rndc reload或者systemctl reload named都行。还有一个隐藏原因是区域文件属主或权限不对,BIND 以bind用户运行,读不到文件会静默使用旧数据,日志里只有一行不起眼的警告。

第三类:内网 DNS 加上去之后外网域名全解析不了。前面提过,是recursion no或者forwarders没配。要么开启递归并限制来源,要么在客户端保留原来的公网 DNS 作为备用 nameserver。

第四类:Java 应用改了 DNS 记录但一直连旧地址。JVM 的 DNS 缓存问题,networkaddress.cache.ttl必须显式设置。另外注意这个属性要在 JVM 启动参数里设,代码里设的话必须赶在任何网络操作之前,否则不生效。

第五类:K8s 环境里 Pod 解析不到内网域名。CoreDNS 的上游配置问题。检查/etc/coredns/Corefile或者 ConfigMap 里的forward .指向,确认那个地址在 Pod 网络里可达。如果节点用 systemd-resolved,把127.0.0.53换成/run/systemd/resolve/resolv.conf里的真实地址。

第六类:Sentinel 切换后客户端连到了旧主库。这种情况通常不是 DNS 的问题,而是 Sentinel 客户端缓存了旧的拓扑。检查客户端的sentinel配置是否正确订阅了切换事件,以及 Redis 侧有没有配replica-announce-ip。如果配了域名但客户端报“无法解析”,那就是客户端所在机器的 DNS 配置有问题,跟 Sentinel 无关。

5.3 常见问题速查表

现象最可能原因验证命令处理方式
Name or service not known解析失败,DNS 不可达或无记录dig @DNS 域名检查 nameserver 配置和区域文件
Connection refused解析正常但端口未监听nc -vz IP 6379检查 Redis bind 和防火墙
部分机器解析结果不一致hosts 文件残留getent hosts 域名清理/etc/hosts
改记录后从库不更新serial 未递增dig @从库 域名修改 serial 后 reload
应用连旧 IPJVM 或客户端 DNS 缓存抓包看 53 端口设置networkaddress.cache.ttl
SERVFAIL区域文件语法错或 DNSSEC 失败named-checkzone修正文件,关闭 dnssec-validation
容器内解析失败Docker/K8s DNS 未透传容器内cat /etc/resolv.conf配置 daemon.json 或 CoreDNS

6. 上线之后的稳定性与变更管理

6.1 别让DNS自己变成新的单点

前面折腾半天是为了消除 IP 变更带来的耦合,如果 DNS 服务器本身挂了导致全公司业务连不上 Redis,那就是把一个单点换成了另一个更致命的单点。所以至少两台是底线,用 BIND 的主从(master/slave)或者 dnsmasq + keepalived 做 VIP 都行。

BIND 主从配置不复杂,从库的named.conf.local里把type master改成type slave,加一行masters { 10.20.30.11; };,主库那边开allow-transfer指向从库 IP。客户端的/etc/resolv.conf里两个 nameserver 都写上去,glibc 会自动在超时后切换。

需要提醒的是,glibc 的切换是串行的:第一个 nameserver 超时(默认 5 秒)才试第二个。所以如果主 DNS 挂了但网络还能通(比如进程假死),客户端每次解析都要等 5 秒,业务感知就是“偶发超时”。解决办法是把options timeout:1 attempts:2加上,把单次超时压到 1 秒,代价是网络抖动时可能误判。另一个办法是在前面挂一个本地缓存(比如systemd-resolved或者nscd),把解析压力挡在本地。

监控方面至少要盯三个指标:DNS 服务的查询 QPS 和响应延迟、区域传送是否成功(从库的 serial 是否跟上主库)、以及从客户端视角定期做一次探测解析(用一个定时任务每分钟dig一次关键域名,比对结果)。最后这个特别有用,能提前发现“服务还在但解析错了”的情况。

6.2 变更流程与灰度顺序

DNS 变更最怕的是“改完立刻全量生效,出问题回滚也要等 TTL”。所以我的习惯是把 TTL 分层:稳定期的记录 TTL 设长一点(300 秒),准备变更前 24 小时先临时调短到 30 秒,变更窗口结束后再调回去。这样既控制了平时的查询量,又保证了变更时的灵活性。

变更顺序也有讲究。切换主库这种操作,正确顺序是:新主库就绪 → 验证新主库可读写 → 修改 DNS 记录 → 观察一段时间 → 下线旧主库。绝对不要先下线旧库再改 DNS,中间会有一段完全不可用的窗口。

还有一点经验:域名切换要做灰度,别指望一次全量。如果是多台客户端,先改一台低优先级的服务验证,确认新连接确实落到了新地址(用redis-cli client list在服务端看来源 IP,或者用CLIENT INFO看连接详情),再逐步推开。我在一次切换里就是因为没做灰度,改完之后发现有台老服务器的/etc/resolv.conf里第一个 nameserver 是个早就下线的地址,每次解析都等 5 秒超时,结果那台机器上的服务 P99 直接从 20ms 飙到 5 秒。

6.3 安全边界上的几个注意点

内网 DNS 虽然在内网,但也不是随便开放就行。allow-query限制网段是最基本的,allow-recursion更关键——开放递归的 DNS 服务器可以被用来做放大攻击,即使在内网也可能被已经失陷的机器利用。如果这台机器只做权威解析,直接recursion no最干净。

区域传送allow-transfer也要限制到从库 IP。有些不严谨的配置用了any,等于把整个内网的拓扑结构暴露给任何能发起 AXFR 查询的人,资产梳理的成本直接降到零。

Redis 这边的安全边界跟域名无关但同样重要:不要因为“有了域名就好管理”就放松 ACL,不要用同一个账号给所有业务用,CONFIG、FLUSHALL、KEYS这类命令该禁就禁。域名的价值在于解耦地址,不在于提升安全性,这两件事别混为一谈。

最后说一个我觉得挺有用的做法:把 DNS 区域文件纳入版本管理,用 Git 管起来,每次变更走一次提交,提交信息里写清楚改了什么、为什么改。这样出问题的时候能快速 diff 出最近的变化,比翻聊天记录靠谱得多。区域文件的格式本身很规整,很容易做自动化校验,写个脚本在 CI 里跑named-checkzone就能挡住大部分低级错误。

这套东西我在几个不同规模的环境里都落地过,最小的只有三台机器、一个区域文件,最大的涉及几十个域名和多台 DNS 服务器。经验是:别一上来就追求完整的高可用架构,先把最小闭环跑通——一台 DNS、一条 A 记录、一个能连上的客户端,跑通了再往上加从库、加监控、加自动化。很多方案死在过度设计上,而不是死在功能不足上。另外,如果你现在的环境里 Redis 地址还是散落在各个配置文件里,别想着一次性全改完,挑一个不那么核心的业务先试点,把 DNS 这套链路和运维流程跑顺了,再推给核心业务,中间踩的坑代价会小很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 3:20:41

ECC 三重解读:椭圆曲线密码学、SAP ECC 与 LSMW 数据迁移实战

1. ECC 到底是什么:从一条热搜说起ECC 这三个字母,放在不同圈子里,指向的东西完全不一样。做密码学的看到它,第一反应是Elliptic Curve Cryptography,椭圆曲线密码学;做企业信息化的看到它,想到…

作者头像 李华
网站建设 2026/10/28 3:20:07

Cloudflare OS:基于边缘计算与Durable Objects的AI Agent运行底座

1. 从“cloudflare-os”这个标题说起:它到底想解决什么问题第一次看到“cloudflare-os”这个标题,很多人会误以为 Cloudflare 要发布一款传统意义上的操作系统,类似 Linux 发行版或者桌面系统。但如果你长期关注边缘计算和 Serverless 领域&a…

作者头像 李华
网站建设 2026/10/28 3:19:46

Linux文件系统管理:从基础操作到高级技巧

1. Linux文件系统管理入门指南作为Linux系统管理员,文件系统管理是最基础的必修技能。记得我刚接触Linux时,面对各种文件系统概念一头雾水,直到真正理解其运作机制后,才发现这套体系设计的精妙之处。本文将带你深入理解Linux文件系…

作者头像 李华
网站建设 2026/10/28 3:19:29

impeccable 实战:用设计约束层提升 AI 生成前端代码质量

1. 从"impeccable"这个词说起:一个前端设计工具链的野心第一次看到"impeccable"这个词被用作项目名,我的反应是——这名字起得够狂。"无可挑剔",一个做前端设计辅助的工具敢叫这个名字,要么是噱头&…

作者头像 李华
网站建设 2026/10/28 3:19:25

基于YOLO与DeepSeek的智能垃圾分类系统开发指南

1. 项目背景与核心价值这个毕业设计选题融合了当前AI领域最前沿的计算机视觉技术(YOLO)和自然语言处理技术(DeepSeek),结合了流行的Web开发框架(SpringBootFlaskVue),打造了一个完整的智能垃圾分类系统。这种技术组合在实际应用中具有很高的创新性和实用…

作者头像 李华
网站建设 2026/10/28 3:18:55

Claude Code源码泄露事件中的AI工程实践解析

1. 事件背景与技术影响2023年11月,AI领域发生了一起震动整个技术圈的重大事件——Anthropic公司开发的Claude Code项目51.2万行核心源码遭到全面泄露。这次泄露不仅包含了完整的TypeScript实现代码,更涉及AI Agent核心架构、Harness工程实践、账号风控机…

作者头像 李华