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则是三级域名

如下图所示注意点(.)是所有域名的根,也就是说所有域名都以点作为后缀
也可以理解为,在域名解析的过程中,所有域名都以点结束

image-20211228170026385

通过理解这几个概念可以看出,域名主要是为了方便让人记住,而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等多种类型的记录

  1. A记录,用来把域名转换成IP地址
  2. CNAME记录,用来创建别名
  3. 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的输出,主要包括四部分

  1. 第一部分,是从114.114.114.114查到的一些根域名服务器(.)的NS记录
  2. 第二部分,是从NS记录结果中选一个(h.root-servers.net),并查询顶级域名org.的NS记录
  3. 第三部分,是从org.的NS记录中选择一个(b0.org.afilias-nst.org)
    并查询二级域名geekbang.org. 的NS服务器
  4. 最后一部分就是从geekbang.org. 的NS服务器(dns10.hichina.com)查询最终主机time.geekbang.org.的A记录

这个输出里展示的各级域名的NS记录,其实就是各级域名服务器的地址,可以更清楚DNS解析的过程
image-20211228171130126

当然不仅仅是发布到互联网的服务需要域名,
很多时候希望能对局域网内部的主机进行域名解析(即内网域名大多数情况下为主机名)
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服务器,用来解析外网的域名




案例准备

  1. 机器配置

    Ubuntu 18.04
    机器配置:2 CPU,4GB 内存
    预先安装 docker 等工具,如 apt install docker.io
    
    
  2. SSH登录到Ubuntu机器中,然后执行下面的命令,拉取案例中使用的Docker镜像

    root@alnk:~# docker pull feisky/dnsutils
    Using default tag: latest
    latest: Pulling from feisky/dnsutils
    ......
    
    
  3. 运行下面的命令,查看主机当前配置的DNS服务器

    root@alnk:~# cat /etc/resolv.conf
    nameserver 114.114.114.114
    
    

案例1:DNS解析失败

  1. 执行下面的命令,进入今天的第一个案例。如果一切正常,可以看到下面这个输出

    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:/# 表示在容器内部运行的命令
    
    
  2. 在容器终端中,执行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错误
    
    
  3. 用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的调试输出
    查看查询过程中的详细步骤,排查其中是否有异常
    
    
  4. 继续在容器终端中,执行下面的命令

    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服务器就可以了
    
    
  5. 执行下面的命令,在配置好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解析不稳定

  1. 执行下面的命令,启动一个新的容器,并进入它的终端中

    root@alnk:~# docker run -it --rm --cap-add=NET_ADMIN --dns 8.8.8.8 feisky/dnsutils bash
    root@d99479553cb5:/# 
    
    
  2. 运行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服务器的延迟比较大呢?
    
    
  3. 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
    
    
  4. 更换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缓存的需求
    
    
  5. 执行下面的命令,就可以启动dnsmasq

    root@d99479553cb5:/# /te  etc/init.d/dnsmasq start
     * Starting DNS forwarder and DHCP server dnsmasq        [ OK ]
     
    
  6. 修改/etc/resolv.conf,将DNS服务器改为dnsmasq的监听地址,这儿是127.0.0.1

    root@d99479553cb5:/# echo nameserver 127.0.0.1 > /etc/resolv.cong f
    
    
  7. 重新执行多次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优化方法

  1. 对DNS解析的结果进行缓存
    缓存是最有效的方法,但要注意,一旦缓存过期,还是要去DNS服务器重新获取新记录
    不过,这对大部分应用程序来说都是可接受的
  2. 对DNS解析的结果进行预取
    这是浏览器等Web应用中最常用的方法
    也就是说不等用户点击页面上的超链接,浏览器就会在后台自动解析域名,并把结果缓存起来
  3. 使用HTTPDNS取代常规的DNS解析
    这是很多移动应用会选择的方法,特别是如今域名劫持普遍存在
    使用HTTP协议绕过链路中的DNS服务器,就可以避免域名劫持的问题
  4. 基于DNS的全局负载均衡(GSLB)
    这不仅为服务提供了负载均衡和高可用的功能, 还可以根据用户的位置,返回距离最近的IP地址

posted @ 2021-12-28 17:42  李成果  阅读(1299)  评论(0编辑  收藏  举报