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

563 阅读7分钟
原文链接: www.codesky.me

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 是由多家大厂共同推进的标准,它甚至有一个翻译不完整的中文网站:dnsflagday.net/index-zh-CN…。在 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 记录实现不标准产生的惨剧。

参考资料