与 DNS 相爱相杀小故事暨科普

Boom!

昨天下午 15:00,我们发现了 CI 的大规模异常,基本上就是 connection failedCould not resolve host 的连环轰炸。

在查找问题之前,先来看看从我们伪 DevOps 能治理的角度出发可能是什么问题,一个运行中的 CI 任务到底是跑在哪里的:

dns.png

在我们这里,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:

图片.png

至此就更加让人迷惑了——我们禁用 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 是怎么工作的?

老生常谈的问题,面试也有可能面到,几幅图就能搞定:

15501539510896.jpg

15501539914578.jpg

15501540068245.jpg
通过 dig A codesky.me +trace,我们可以方便的看清楚整个链路的情况。

15501543399921.jpg

第一层是根域名服务器。

根域名服务器是 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 的,但是,他有三种常见的偏好:

  1. 系统库并不知道到底 IPV6 是否被启用了,他通过调用 getaddrinfo(),如果命中了 AF_UNSPEC,那么代表这个系统支持 IPV6。
  2. 只有系统开启了 IPV6, getaddrinfo() 才去处理 IPV6 请求(但如果 IPV6 链接已经存在就会成为漏网之鱼)。
  3. 启发式的判断 IPV6 是否存在。

15501558831414.jpg

然而在某些情况中,服务器就会优先去考虑 AAAA,并且宁愿请求超时也不愿意去降级到 IPV4。

15501560900965.jpg

此外在一些实现中,也会导致错误的应答而导致强行让 AAAA 记录作为返回值的结果。

总之,简单的来说就是 DNS 有很多坑,不是说系统上关闭了就直接不用的,这涉及到了应用和服务器的实现细节上,就这次来说,就是 AAAA 记录实现不标准产生的惨剧。

参考资料

植入部分

如果您觉得文章不错,可以通过赞助支持我。

如果您不希望打赏,也可以通过关闭广告屏蔽插件的形式帮助网站运作。

标签: 知识, Devops, 运维

已有 3 条评论

  1. 学习一波

  2. 虽然看不懂,但是感觉很牛逼的样子

  3. hao

    开发最终「加上 EDNS」搞定了这个问题 , 是什么意思? 能具体解释下为什么?

添加新评论