37-案例篇:DNS解析时快时慢,我该怎么办?
IP地址虽然方便了机器的通信,却给访问这些服务的人们,带来了很重的记忆负担
没几个人能记得住Github所在的IP地址,因为这串字符,对人脑来说并没有什么含义
不过这并不妨碍经常使用这个服务,为什么呢?
当然是因为还有更简单、方便的方式
可以通过域名github.com访问,而不是必须依靠具体的IP地址,这其实正是域名系统DNS的由来
DNS(Domain Name System)即域名系统,是互联网中最基础的一项服务,主要提供域名和IP地址之间映射关系的查询服务
DNS不仅方便了人们访问不同的互联网服务
更为很多应用提供了,动态服务发现和全局负载均衡(Global Server Load Balance,GSLB)的机制
这样DNS就可以选择离用户最近的IP来提供服务
即使后端服务的IP地址发生变化,用户依然可以用相同域名来访问
域名与DNS解析
域名我由一串用点分割开的字符组成,被用作互联网中的某一台或某一组计算机的名称
目的就是为了方便识别,互联网中提供各种服务的主机位置
要注意域名是全球唯一的,需要通过专门的域名注册商才可以申请注册
为了组织全球互联网中的众多计算机,域名同样用点来分开,形成一个分层的结构
而每个被点分割开的字符串,就构成了域名中的一个层级,并且位置越靠后,层级越高
以极客时间的网站time.geekbang.org为例,来理解域名的含义
这个字符串中, 最后面的org是顶级域名,中间的geekbang是二级域名,而最左边的time则是三级域名
如下图所示注意点(.)是所有域名的根,也就是说所有域名都以点作为后缀
也可以理解为,在域名解析的过程中,所有域名都以点结束
通过理解这几个概念可以看出,域名主要是为了方便让人记住,而IP地址是机器间的通信的真正机制
把域名转换为IP地址的服务,也就是开头提到的,域名解析服务 (DNS)
而对应的服务器就是域名服务器,网络协议则是DNS协议
DNS协议在TCP/IP栈中属于应用层,不过实际传输还是基于UDP或者TCP协议(UDP 居多)
并且域名服务器一般监听在端口53上
既然域名以分层的结构进行管理,相对应的,域名解析其实也是用递归的方式(从顶级开 始,以此类推)
发送给每个层级的域名服务器,直到得到解析结果
不过不要担心,递归查询的过程并不需要亲自操作,DNS服务器会替你完成
我们只要预先配置一个可用的DNS服务器就可以了
当然通常来说,每级DNS服务器,都会有最近解析记录的缓存
当缓存命中时,直接用缓存中的记录应答就可以了
如果缓存过期或者不存在,才需要用刚刚提到的递归方式查询
所以系统管理员在配置Linux系统的网络时,除了需要配置IP地址,还需要给它配置DNS服务器
这样它才可以通过域名来访问外部服务
执行下面的命令来查询系统配置
$ cat /etc/resolv.conf
nameserver 114.114.114.114
DNS服务通过资源记录的方式来管理所有数据,它支持A、CNAME、MX、 NS、PTR等多种类型的记录
- A记录,用来把域名转换成IP地址
- CNAME记录,用来创建别名
- NS记录,表示该域名对应的域名服务器地址
简单来说当访问某个网址时,就需要通过DNS的A记录,查询该域名对应的IP地址,然后再通过该IP来访问Web服务
以极客时间的网站time.geekbang.org为例
执行下面的nslookup命令, 就可以查询到这个域名的A记录
$ nslookup time.geekbang.org
# 域名服务器及端口信息
Server: 114.114.114.114
Address: 114.114.114.114#53
# 非权威查询结果
Non-authoritative answer:
Name: time.geekbang.org
Address: 39.106.233.17
这里要注意由于114.114.114.114并不是直接管理time.geekbang.org的域名服务器,所以查询结果是非权威的
使用上面的命令只能得到114.114.114.114查询的结果
如果没有命中缓存,DNS查询实际上是一个递归过程,那有没有方法可以知道整个递归查询的执行呢?
其实除了nslookup,另外一个常用的DNS解析工具dig
就提供了trace功能,可以展示递归查询的整个过程
比如可以执行下面的命令,得到查询结果
# +trace 表示开启跟踪查询
# +nodnssec 表示禁止 DNS 安全扩展
$ dig +trace +nodnssec time.geekbang.org
; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> +trace +nodnssec time.geekbang.org
;; global options: +cmd
. 322086 IN NS m.root-servers.net.
. 322086 IN NS a.root-servers.net.
. 322086 IN NS i.root-servers.net.
. 322086 IN NS d.root-servers.net.
. 322086 IN NS g.root-servers.net.
. 322086 IN NS l.root-servers.net.
. 322086 IN NS c.root-servers.net.
. 322086 IN NS b.root-servers.net.
. 322086 IN NS h.root-servers.net.
. 322086 IN NS e.root-servers.net.
. 322086 IN NS k.root-servers.net.
. 322086 IN NS j.root-servers.net.
. 322086 IN NS f.root-servers.net.
;; Received 239 bytes from 114.114.114.114#53(114.114.114.114) in 1340 ms
org. 172800 IN NS a0.org.afilias-nst.info.
org. 172800 IN NS a2.org.afilias-nst.info.
org. 172800 IN NS b0.org.afilias-nst.org.
org. 172800 IN NS b2.org.afilias-nst.org.
org. 172800 IN NS c0.org.afilias-nst.info.
org. 172800 IN NS d0.org.afilias-nst.org.
;; Received 448 bytes from 198.97.190.53#53(h.root-servers.net) in 708 ms
geekbang.org. 86400 IN NS dns9.hichina.com.
geekbang.org. 86400 IN NS dns10.hichina.com.
;; Received 96 bytes from 199.19.54.1#53(b0.org.afilias-nst.org) in 1833 ms
time.geekbang.org. 600 IN A 39.106.233.176
;; Received 62 bytes from 140.205.41.16#53(dns10.hichina.com) in 4 ms
dig trace的输出,主要包括四部分
- 第一部分,是从114.114.114.114查到的一些根域名服务器(.)的NS记录
- 第二部分,是从NS记录结果中选一个(h.root-servers.net),并查询顶级域名org.的NS记录
- 第三部分,是从org.的NS记录中选择一个(b0.org.afilias-nst.org)
并查询二级域名geekbang.org. 的NS服务器 - 最后一部分就是从geekbang.org. 的NS服务器(dns10.hichina.com)查询最终主机time.geekbang.org.的A记录
这个输出里展示的各级域名的NS记录,其实就是各级域名服务器的地址,可以更清楚DNS解析的过程
当然不仅仅是发布到互联网的服务需要域名,
很多时候希望能对局域网内部的主机进行域名解析(即内网域名大多数情况下为主机名)
Linux也支持这种行为
所以可以把主机名和IP地址的映射关系,写入本机的/etc/hosts文件中
指定的主机名就可以在本地直接找到目标IP
比如可以执行下面的命令来操作
$ cat /etc/hosts
127.0.0.1 localhost localhost.localdomain
::1 localhost6 localhost6.localdomain6
192.168.0.100 domain.com
或者还可以在内网中,搭建自定义的DNS服务器,专门用来解析内网中的域名
而内网DNS服务器,一般还会设置一个或多个上游DNS服务器,用来解析外网的域名
案例准备
-
机器配置
Ubuntu 18.04 机器配置:2 CPU,4GB 内存 预先安装 docker 等工具,如 apt install docker.io
-
SSH登录到Ubuntu机器中,然后执行下面的命令,拉取案例中使用的Docker镜像
root@alnk:~# docker pull feisky/dnsutils Using default tag: latest latest: Pulling from feisky/dnsutils ......
-
运行下面的命令,查看主机当前配置的DNS服务器
root@alnk:~# cat /etc/resolv.conf nameserver 114.114.114.114
案例1:DNS解析失败
-
执行下面的命令,进入今天的第一个案例。如果一切正常,可以看到下面这个输出
root@alnk:~# echo $(mktemp) /tmp/tmp.egiweqJvFf # 进入案例环境的 SHELL 终端中 root@alnk:~# docker run -it --rm -v $(mktemp):/etc/resolv.conf feisky/dnsutils bash root@7d705f264ae5:/# # root@7d705f264ae5:/# 表示在容器内部运行的命令
-
在容器终端中,执行DNS查询命令,查询time.geekbang.org的IP地址
root@7d705f264ae5:/# nslookup time.geekbang.org ;; connection timed out; no servers could be reached ## 发现这个命令阻塞很久后,还是失败了,报了connection timed out和no servers could be reached错误
-
用ping工具检查网路
root@7d705f264ae5:/# ping 11 -c 3 114.114.114.114 PING 114.114.114.114 (114.114.114.114): 56 data bytes 64 bytes from 114.114.114.114: icmp_seq=0 ttl=67 time=35.739 ms 64 bytes from 114.114.114.114: icmp_seq=1 ttl=91 time=35.738 ms 64 bytes from 114.114.114.114: icmp_seq=2 ttl=77 time=35.693 ms --- 114.114.114.114 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max/stddev = 35.693/35.723/35.739/0.000 ms ## 可以看到网络是通的 那要怎么知道nslookup命令失败的原因呢? 这里其实有很多方法,最简单的一种,就是开启nslookup的调试输出 查看查询过程中的详细步骤,排查其中是否有异常
-
继续在容器终端中,执行下面的命令
root@7d705f264ae5:/# nslookup -debug time.geekbang.org ;; Connection to 127.0.0.1#53(127.0.0.1) for time.geekbang.org failed: connection refused. ;; Connection to ::1#53(::1) for time.geekbang.org failed: address not available. ## 从这次的输出可以看到nslookup连接环回地址(127.0.0.1 和 ::1)的53端口失败 这里就有问题了,为什么会去连接环回地址,而不是先前看到的114.114.114.114呢? ## 有可能是因为容器中没有配置DNS服务器 执行下面的命令确认一下 root@7d705f264ae5:/# cat /etc/resolv.conf ## 果然这个命令没有任何输出,说明容器里的确没有配置DNS服务器 到这一步就知道了解决方法 在/etc/resolv.conf文件中,配置上DNS服务器就可以了
-
执行下面的命令,在配置好DNS服务器后,重新执行nslookup命令
root@7d705f264ae5:/# echo "nameserver 114.114.114.114" > /etc/resolv.conf root@7d705f264ae5:/# nslookup time.geekbang.org Server:114.114.114.114 Address:114.114.114.114#53 Non-authoritative answer: Name:time.geekbang.org Address: 39.106.233.176 ## 到这里第一个案例就轻松解决了 最后,在终端中执行exit命令退出容器,Docker就会自动清理刚才运行的容器
案例2:DNS解析不稳定
-
执行下面的命令,启动一个新的容器,并进入它的终端中
root@alnk:~# docker run -it --rm --cap-add=NET_ADMIN --dns 8.8.8.8 feisky/dnsutils bash root@d99479553cb5:/#
-
运行nslookup命令,解析time.geekbang.org的IP地址
root@d99479553cb5:/# time nslookup time.geekbang.org Server: 8.8.8.8 Address: 8.8.8.8#53 Non-authoritative answer: Name: time.geekbang.org Address: 39.106.233.176 real 0m10.349s user 0m0.004s sys 0m0.0 ## 可以看到这次解析非常慢,居然用了10秒 综合来看,现在DNS解析的结果不但比较慢,而且还会发生超时失败的情况 ## 这是为什么呢?碰到这种问题该怎么处理呢? 其实,根据前面的讲解知道DNS解析,说白了就是客户端与服务器交互的过程,并且这个过程还使用了UDP协议 那么,对于整个流程来说,解析结果不稳定,就有很多种可能的情况了。比方说 1. DNS服务器本身有问题,响应慢并且不稳定 2. 客户端到DNS服务器的网络延迟比较大 3. DNS 请求或者响应包,在某些情况下被链路中的网络设备弄丢了 ## 根据上面nslookup的输出可以看到,现在客户端连接的DNS是8.8.8.8 这是Google提供的DNS服务 对Google还是比较放心的,DNS服务器出问题的概率应该比较小 基本排除了DNS服务器的问题 那是不是第二种可能,本机到DNS服务器的延迟比较大呢?
-
ping可以用来测试服务器的延迟
root@d99479553cb5:/# ping -c3 8.8.8.8 PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: icmp_seq=0 ttl=31 time=137.637 ms 64 bytes from 8.8.8.8: icmp seq=1 ttl=31 time=144.743 ms 64 bytes from 8.8.8.8: icmp_seq=2 ttl=31 time=138.576 ms --- 8.8.8.8 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max/stddev = 137.637/140.319/144.743/3.152 ms ## 从ping的输出可以看到,这里的延迟已经达到了140ms,这也就可以解释,为什么解析这么慢了 ## 碰到这种问题该怎么办呢? 显然,既然延迟太大,那就换一个延迟更小的DNS服务器,比如电信提供的114.114.114.114
-
更换DNS服务器,再次执行nslookup解析命令
root@d99479553cb5:/# echo nameserver 114.114.114.114 > /etc/resolv.conf root@d99479553cb5:/# time. nslookup time.geekbang.rog org Server:114.114.114.114 Address:114.114.114.114#53 Non-authoritative answer: Name:time.geekbang.org Address: 39.106.233.176 real0m0.070s user0m0.000s sys0m0.007s root@d99479553cb5:/# time nslookup time.geekbang.org Server:114.114.114.114 Address:114.114.114.114#53 Non-authoritative answer: Name:time.geekbang.org Address: 39.106.233.176 real0m0.075s user0m0.007s sys0m0.000s ## 现在只需要64ms就可以完成解析,比刚才的10s要好很多 ## 到这里问题看似就解决了 不过,如果多次运行nslookup命令,估计就不是每次都有好结果了 比如有时候需要1s甚至更多的时间 ## 1s的DNS解析时间还是太长了,对很多应用来说也是不可接受的 那么,该怎么解决这个问题呢? 使用DNS缓存 这样,只有第一次查询时需要去DNS服务器请求,以后的查询,只要DNS记录不过期,使用缓存中的记录就可以了 ## 不过要注意,使用的主流Linux发行版,除了最新版本的Ubuntu (如18.04或者更新版本)外 其他版本并没有自动配置DNS缓存 ## 所以想要为系统开启DNS缓存,就需要做额外的配置 比如,最简单的方法,就是使用dnsmasq ## dnsmasq是最常用的DNS缓存服务之一,还经常作为DHCP服务来使用 它的安装和配置都比较简单,性能也可以满足绝大多数应用程序对DNS缓存的需求
-
执行下面的命令,就可以启动dnsmasq
root@d99479553cb5:/# /te etc/init.d/dnsmasq start * Starting DNS forwarder and DHCP server dnsmasq [ OK ]
-
修改/etc/resolv.conf,将DNS服务器改为dnsmasq的监听地址,这儿是127.0.0.1
root@d99479553cb5:/# echo nameserver 127.0.0.1 > /etc/resolv.cong f
-
重新执行多次nslookup命令
root@d99479553cb5:/# time nslookup time.geekbang.org Server:127.0.0.1 Address:127.0.0.1#53 Non-authoritative answer: Name:time.geekbang.org Address: 39.106.233.176 real0m0.007s user0m0.003s sys0m0.003s root@d99479553cb5:/# time nslookup time.geekbang.org Server:127.0.0.1 Address:127.0.0.1#53 Non-authoritative answer: Name:time.geekbang.org Address: 39.106.233.176 real0m0.007s user0m0.003s sys0m0.003s ## 只有第一次的解析很慢,以后的每次解析都很快 并且后面每次DNS解析需要的时间也都很稳定
小结
DNS是互联网中最基础的一项服务,提供了域名和IP地址间映射关系的查询服务
很多应用程序在最初开发时,并没考虑DNS解析的问题
后续出现问题后,排查好几天才能发现,其实是DNS解析慢导致的
试想假如一个Web服务的接口,每次都需要1s时间来等待DNS解析
那么,无论怎么优化应用程序的内在逻辑,对用户来说,这个接口的响应都太慢
因为响应时间总是会大于1秒的
所以,在应用程序的开发过程中,必须考虑到DNS解析可能带来的性能问题,掌握常见的优化方法
总结了几种常见的DNS优化方法
- 对DNS解析的结果进行缓存
缓存是最有效的方法,但要注意,一旦缓存过期,还是要去DNS服务器重新获取新记录
不过,这对大部分应用程序来说都是可接受的 - 对DNS解析的结果进行预取
这是浏览器等Web应用中最常用的方法
也就是说不等用户点击页面上的超链接,浏览器就会在后台自动解析域名,并把结果缓存起来 - 使用HTTPDNS取代常规的DNS解析
这是很多移动应用会选择的方法,特别是如今域名劫持普遍存在
使用HTTP协议绕过链路中的DNS服务器,就可以避免域名劫持的问题 - 基于DNS的全局负载均衡(GSLB)
这不仅为服务提供了负载均衡和高可用的功能, 还可以根据用户的位置,返回距离最近的IP地址