与 DNS 相爱相杀小故事暨科普
Boom!
昨天下午 15:00,我们发现了 CI 的大规模异常,基本上就是 connection failed
和 Could not resolve host
的连环轰炸。
在查找问题之前,先来看看从我们伪 DevOps 能治理的角度出发可能是什么问题,一个运行中的 CI 任务到底是跑在哪里的:
在我们这里,CI 机器是有一个独立网段的,部署一个 CI 需要若干台机器,由 Master 负责调度给 Slave,Slave 在机器上创建新的 Docker 容器运行任务。
那么我们可控(测)范围内,就有三种可能,第一种是网段的问题,第二种是宿主机的问题,第三种是容器的问题。
在排除了目标域名的服务本身的问题后,最优先怀疑的是宿主机的 DNS 炸了,curl 了一波,无异常,这个时候已经很奇怪了,我们的 Docker Container 是 host 模式启动的,理论上来说应该和宿主机共用网络环境,当然,也有可能 DNS Resolver 配置的不一样,所以 docker run -it xxx /bin/bash
进到容器内试了一波 curl,果然炸了,而 ping 却能正常获得内容,在 /etc/resolv.conf
里修改了 DNS 记录到 114.114.114.114
,再次 curl,成功返回——如果故事到这里没事了那就算了,换一个 DNS 就能搞定,但是更换 DNS 后部分内网资源就解析不了了,所以还得接着尝试。
有同事说以前遇到过类似的问题,是 IPV6 的锅,于是我们试了 curl -4
,果然使用 IPV4 可以正常获取数据,开始考虑 disable IPV6——在 sysctl 中应该把 IPV6 配置为:
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
来禁用 IPV6,但是经过检查,我们发现已经禁用了。
之后尝试了 tcpdump,我们发现在 curl 时仍然会对 A 和 AAAA 发起请求,然而 A 记录明明有返回链路结果,却没有被采用,依旧走了 AAAA:
至此就更加让人迷惑了——我们禁用 IPV6 是否成功了?
resolv.conf
本身的文档可能能让我们对上面这个 dump 包了解的更明白一些:
inet6: 将 AAAA 记录优先请求,不过在某一版本后被废弃
single-request: 默认会并行的请求 A 和 AAAA,但是某些程序不能正确处理,如果使用这个选项就能让他按顺序发出请求
single-request-reopen:默认会给 A 和 AAAA 共用同一个 socket,又有某些程序无法正确处理,使用这个可以关闭这个特性。
在 UDP 包中我们基本能看出这些特性,然而无论怎么开关这些 options,只要不改 DNS,一点卵用都没有。到底为什么还是走了 IPV6……
不过在得知了——巧了不是,今天变更了 DNS 以及更巧了不是,变更的时间点正好就是我们出现问题的时间,这个问题总算是逐渐拨开迷雾了。
在我们的采访过后,开发最终「加上 EDNS」搞定了这个问题,顺便提到了「DNS FLAG DAY」。
在开始科普之前,我们最后在确认一个问题——最后我们到底是怎么修好的。
明明 curl -4 可以,为什么我们不直接用这种方法呢——实际上在容器内跑的脚本情况非常复杂,可能包括了不同语言的不同 HTTP 库,包括用户跑的代码也不尽相同,强行要求统一工作量太大也不太可能接受这种变更,而最终我们临时修改了 DNS,把一些内网需要访问的地址存在 hosts 里不经过 DNS……曲线救国。
看了上面的经过,现在你一定满头问号,一脸懵逼,这都是啥和啥——接下来就是科普时间。
DNS 是怎么工作的?
老生常谈的问题,面试也有可能面到,几幅图就能搞定:
通过 dig A codesky.me +trace
,我们可以方便的看清楚整个链路的情况。
第一层是根域名服务器。
根域名服务器是 DNS 中最高级别的域名服务器,这些服务器负责返回顶级域的权威域名服务器地址,这些域名服务器的数量总共有 13 组,域名的格式从上面返回的结果可以看到是 .root-servers.net,每个根域名服务器中只存储了顶级域服务器的 IP 地址,大小其实也只有 2MB 左右,虽然域名服务器总共只有 13 组,但是每一组服务器都通过提供了镜像服务,全球大概也有几百台的根域名服务器在运行。
然后找到了 5台机器中 .me
域名的记录,找到之后在去找 codesky.me,然后找到了 DNSPOD 的记录,从 DNSPOD 中我们最终拿到了 A 记录的 IP,找到了这台机器。
问题又来了?什么是 A 记录?
- A / AAAA:直接指向服务器所在的 IP,其中 A 记录为 IPV4 的 IP 记录,AAAA 是 IPV6 的 IP 记录。
- CNAME:别名记录,指向一个域名,它会向上去找 CNAME 的域名对应的记录,它可能是任何记录,比如另一个 CNAME 或者 A 记录。
- NS:记录 DNS 解析服务的地址,也就是上面根域名服务器和递归 DNS 服务器的地址。
- MX:用于邮箱服务,定位到邮箱地址。
DNS FLAG DAY
DNS FLAG DAY 是由多家大厂共同推进的标准,它甚至有一个翻译不完整的中文网站:https://dnsflagday.net/index-zh-CN.html。在 DNS 标准中,有很多过于陈旧的影响效率的内容,这次删除了这些内容后的 DNS 服务将会更加稳定安全。
简单概括一下这个标准,大部分递归 DNS 要求权威服务器支持 EDNS 了,否则无法正确的解析域名,这确实和我们遇到的问题很像,但是并没有对每一个 DNS 服务器都做此要求,即使不支持 EDNS,依旧可以使用。然而我们的 resolver 也是自家的,理论上不受此影响(但是这个代码没看过也不好说,实际上解决了这个问题应该也代表跟这有关)。
那么问题又来了,EDNS 又是啥?
EDNS 就是在遵循已有的 DNS 消息格式的基础上增加一些字段,来支持更多的 DNS 请求业务。
简单的来说就是一个面向未来的扩展 DNS 标准,因为过去设定的 DNS 标准在未来可能会不够用——有点 HTTP 0.9 到 HTTP 1.1 的味道。
所以为啥我还是连上了 IPV6?
由于 DNS 的 RFC 太多,一个个读估计读一年都不一定能读完,更不要说还会不断的增加了——不过,碰巧被我翻到了一些内容。
首先在此之前我们先知道,在系统中,都是通过实现 getaddrinfo()
来获取 IP 的,但是,他有三种常见的偏好:
- 系统库并不知道到底 IPV6 是否被启用了,他通过调用
getaddrinfo()
,如果命中了 AF_UNSPEC,那么代表这个系统支持 IPV6。 - 只有系统开启了 IPV6,
getaddrinfo()
才去处理 IPV6 请求(但如果 IPV6 链接已经存在就会成为漏网之鱼)。 - 启发式的判断 IPV6 是否存在。
然而在某些情况中,服务器就会优先去考虑 AAAA,并且宁愿请求超时也不愿意去降级到 IPV4。
此外在一些实现中,也会导致错误的应答而导致强行让 AAAA 记录作为返回值的结果。
总之,简单的来说就是 DNS 有很多坑,不是说系统上关闭了就直接不用的,这涉及到了应用和服务器的实现细节上,就这次来说,就是 AAAA 记录实现不标准产生的惨剧。
参考资料
- 详解 DNS 与 CoreDNS 的实现原理
- How does DNS work?
- DNS Records: An Introduction
- Operational Considerations and Issues with IPv6 DNS
- DNS64: DNS Extensions for Network Address Translation from IPv6 Clients to IPv4 Servers
- DNS Extensions to Support IP Version 6
- Extension Mechanisms for DNS (EDNS(0))
- DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION
植入部分
如果您觉得文章不错,可以通过赞助支持我。
如果您不希望打赏,也可以通过关闭广告屏蔽插件的形式帮助网站运作。
学习一波
虽然看不懂,但是感觉很牛逼的样子
开发最终「加上 EDNS」搞定了这个问题 , 是什么意思? 能具体解释下为什么?
我很好奇这个edns是怎么加上的,目前我了解到的一般都是dns resolver支持,你们是在办公网local dns上开启了这个edns的功能,edns用的是啥,ecs么?