首先申明标题党!
前几天,有一张图片访问404(从第三方站点拉去),后来查到原因是下载超过5s(wget带了超时参数--timeout=5),所以下载失败。但是直接访问原图又是非常快,基本感觉不到延时。开始怀疑是服务器网络原因,用host获取该域名的ip地址,无法ping通。
初步结论是:网络原因,无法ping通
服务器可能会设置禁ping,于是我就用wget不带超时下载了一次,发现确实可以下载,只是耗时非常严重,如图:
花了20s,才将此图完整下下来。换成curl来下载此图,结果如图:
结果就很明显,curl仅用1s的时间下载此图,而wget却用了20s才做完相同的事儿。我这并不是说curl的性能优于wget啊,换成其他网站url,时间消耗就差不多。那到底是什么原因才导致了如此巨大的差异了,勾起了我的好奇心。
用strace、tcpdump分别跟踪了wget和curl,摘取了相对核心的地方来做分析。
由于strace的内容比较多比较杂,为了方便分析摘出了比较核心的几处
- 11:52:37.877356 execve("/usr/bin/wget", ["wget", "http://newpic.jxnews.com.cn/0/11"], [/* 30 vars */]) = 0
- 11:52:37.899984 socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3
- 11:52:37.900043 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, 28) = 0
- 11:52:37.900114 fcntl(3, F_GETFL) = 0x2 (flags O_RDWR)
- 11:52:37.900165 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
- 11:52:37.900215 poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
- 11:52:37.900284 sendto(3, "\353J\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 38, MSG_NOSIGNAL, NULL, 0) = 38
- 11:52:37.900773 poll([{fd=3, events=POLLIN}], 1, 5000) = 0 (Timeout)
- 11:52:47.900462 poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
- 11:52:47.900529 sendto(3, "\353J\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 38, MSG_NOSIGNAL, NULL, 0) = 38
- 11:52:47.900606 poll([{fd=3, events=POLLIN}], 1, 5000) = 0 (Timeout)
- 11:52:57.900996 close(3) = 0
- 11:52:57.901134 socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3
- 11:52:57.901192 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, 28) = 0
- 11:52:57.901270 fcntl(3, F_GETFL) = 0x2 (flags O_RDWR)
- 11:52:57.901322 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
- 11:52:57.901374 poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
- 11:52:57.901436 sendto(3, "c\237\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 50, MSG_NOSIGNAL, NULL, 0) = 50
- 11:52:57.901646 poll([{fd=3, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}])
- 11:52:57.902350 ioctl(3, FIONREAD, [125]) = 0
- 11:52:57.902421 recvfrom(3, "c\237\201\203\0\1\0\0\0\1\0\0\6newpic\6jxnews\3com\2c"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, [16]) = 125
- 11:52:57.902516 close(3) = 0
- 11:52:57.902592 socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3
- 11:52:57.902647 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, 28) = 0
- 11:52:57.902707 fcntl(3, F_GETFL) = 0x2 (flags O_RDWR)
- 11:52:57.902757 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
- 11:52:57.902808 poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
- 11:52:57.902881 sendto(3, "\343\25\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 38, MSG_NOSIGNAL, NULL, 0) = 38
- 11:52:57.903027 poll([{fd=3, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}])
- 11:52:57.983323 ioctl(3, FIONREAD, [92]) = 0
- 11:52:57.983394 recvfrom(3, "\343\25\201\200\0\1\0\1\0\2\0\0\6newpic\6jxnews\3com\2c"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, [16]) = 92
- 11:52:57.983482 close(3) = 0
- 11:52:57.983584 write(2, "59.52.28.153", 12) = 12
- 11:52:57.983707 write(2, "\n", 1) = 1
- 11:52:57.983825 open("/usr/share/locale/zh.GBK/LC_MESSAGES/wget.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
- 11:52:57.983905 open("/usr/share/locale/zh/LC_MESSAGES/wget.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
- 11:52:57.983979 write(2, "Connecting to newpic.jxnews.com."..., 55) = 55
- 11:52:57.984073 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
- 11:52:57.984139 connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("59.52.28.153")}, 16) = 0
- 11:52:58.045641 write(2, "\322\321\301\254\275\323\241\243\n", 9) = 9
- 11:52:58.045760 select(4, NULL, [3], NULL, {900, 0}) = 1 (out [3], left {900, 0})
- 11:52:58.045832 write(3, "GET /0/11/84/34/11843462_991618."..., 155) = 155
在11:52:37这一刻,执行wget命令。第一个较为重要的动作是做dns解析,向域名服务器10.55.21.254发起了两次数据包的长度是38字节查询请求,得到两次timeout,在11:52:57正式关闭了请求。
系统尝试向dns服务器10.55.21.254发送长度为50字节的数据包的查询请求,并获得返回结果(这里无法知道获知返回结果)。
系统在获得返回结果后,第四次向10.55.21.254发送请求,获得返回结果后,得到url的ip地址,发起了http请求。
通过已经分析,可以得到几个结论:
1、对域名解析方式上的差异导致wget和curl的耗时差别巨大
2、第三次改变数据包的查询,虽然获取得了返回,但并没有获取域名的ip
tcpdump的结果图:
相对于strace的日志,tcpdump的就比较干净,为了方便分析,还是摘除了关键部分
- 11:52:37.900351 IP wapcms-169.54429 > dns.domain: 60234+ AAAA? newpic.jxnews.com.cn. (38)
- 11:52:47.900568 IP wapcms-169.54429 > dns.domain: 60234+ AAAA? newpic.jxnews.com.cn. (38)
- 11:52:57.901476 IP wapcms-169.46106 > dns.domain: 25503+ AAAA? newpic.jxnews.com.cn.localdomain. (50)
- 11:52:57.902233 IP dns.domain > wapcms-169.46106: 25503 NXDomain 0/1/0 (125)
- 11:52:57.902924 IP wapcms-169.58634 > dns.domain: 58133+ A? newpic.jxnews.com.cn. (38)
- 11:52:57.983135 IP dns.domain > wapcms-169.58634: 58133 1/2/0 A 59.52.28.153 (92)
很清晰的与strace的结果对应上了,两次38字节的数据包timeout,一次50字节的数据包,获得返回
NXDomain 0/1/0 (125),一次38字节的请求获得域名的ip地址:59.52.28.153
- execve("/usr/bin/curl", ["curl", "http://newpic.jxnews.com.cn/0/11", "-o", "1.jpg"], [/* 31 vars */]) = 0
- = 0
- socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3
- connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, 28) = 0
- fcntl(3, F_GETFL) = 0x2 (flags O_RDWR)
- fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
- poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
- sendto(3, "\370T\1\0\0\1\0\0\0\0\0\0\6newpic\6jxnews\3com\2c"..., 38, MSG_NOSIGNAL, NULL, 0) = 38
- poll([{fd=3, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLIN}])
- ioctl(3, FIONREAD, [92]) = 0
- recvfrom(3, "\370T\201\200\0\1\0\1\0\2\0\0\6newpic\6jxnews\3com\2c"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.55.21.254")}, [16]) = 92
- close(3)
- = 0
- socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
- fcntl(3, F_GETFL) = 0x2 (flags O_RDWR)
- fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
- connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("120.203.216.133")}, 16) = -1 EINPROGRESS (Operation now in progress)
- poll([{fd=3, events=POLLOUT}], 1, 300000) = 1 ([{fd=3, revents=POLLOUT}])
- getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
- sendto(3, "GET /0/11/84/34/11843462_991618."..., 194, MSG_NOSIGNAL, NULL, 0) = 194
很明显的发现,curl通过一次dns查询就获取了url的ip地址,然后就发起http请求。
- 15:19:39.744520 IP wapcms-169.48990 > dns.domain: 63572+ A? newpic.jxnews.com.cn. (38)
- 15:19:39.744905 IP wapcms-169.38502 > dns.domain: 13836+ PTR? 254.21.55.10.in-addr.arpa. (43)
- 15:19:39.745626 IP dns.domain > wapcms-169.38502: 13836* 1/1/0 (86)
- 15:19:39.793797 IP dns.domain > wapcms-169.48990: 63572 1/2/0 A 120.203.216.133 (92)
- 15:19:39.793890 IP wapcms-169.31975 > dns.domain: 54867+ PTR? 133.216.203.120.in-addr.arpa. (46)
- 15:19:39.848460 IP dns.domain > wapcms-169.31975: 54867 NXDomain 0/1/0 (102)
从上面基本上可以得到结论,wget与curl在域名解析上有很大的不同。
通过查阅dns协议和tcpdump的输出数据格式得到结论是:
wget兼容IPv6,而curl不兼容,该url的dns解析20s的时间发生在解析IPv6上
引申:
host命令兼容IPv6 dig不兼容 (均为不带任何附加参数)
追加证明:
wget强制使用IPv4进行dns的结果如图:
注:
dns协议见附件,相关附带知识如下:
域名服务请求的格式是
src>dst:idop?flagsqtypeqclassname(len)
h2opolo.1538>helios.domain:3+A?ucbvax.berkeley.edu.(37)
主机h2opolo访问helios上的域名服务,询问和ucbvax.berkeley.edu.关联的地址记录(qtype=A).查询号是 `3'.` +'表明设置了递归请求标志.查询长度是37字节,不包括UDP和IP头.查询操作是普通的Query操作,因此op域可以忽略.如果op设置成其他什么 东西,它应该显示在`3'和`+'之间.类似的,qclass是普通的C_IN类型,也被忽略了.其他类型的qclass应该在`A'后面显示.
Tcpdump 会检查一些不规则情况,相应的结果作为补充域放在方括号内:如果某个查询包含回答,名字服务或管理机构部分,就把ancount,nscount,或 arcount显示成`[na]',`[nn]'或`[nau]',这里的n代表相应的数量.如果在第二和第三字节中,任何一个回答位(AA,RA或 rcode)或任何一个`必须为零'的位被置位,就显示`[b2&3=x]',这里的x是报头第二和第三字节的16进制数.
UDP名字服务回答
名字服务回答的格式是
src>dst:idoprcodeflagsa/n/autypeclassdata(len)
helios.domain>h2opolo.1538:33/3/7A128.32.137.3(273)
helios.domain>h2opolo.1537:2NXDomain*0/1/0(97)
第一个例子里,helios回答了h2opolo发出的标识为3的询问,一共是3个回答记录,3个名字服务记录和7个管理结构记录.第一个回答纪录的类型是 A(地址),数据是internet地址128.32.137.3.回答的全长为273字节,不包括UDP和IP报头.作为A记录的class (C_IN)可以忽略op(询问)和rcode(NoError).
在第二个例子里,helios对标识为2的询问作出域名不存在(NXDomain)的回答,没有回答记录,一个名字服务记录,而且没有管理结构.
`*'表明设置了权威回答(authoritativeanswer).由于没有回答记录,这里就不显示type,class和data.
其他标志字符可以显示为`-'(没有设置递归有效(RA))和`|'(设置消息截短(TC)).如果`问题'部分没有有效的内容,就显示`[nq]'.
注意名字服务的询问和回答一般说来比较大,68字节的snaplen可能无法捕捉到足够的报文内容.如果你的确在研究名字服务的情况,可以使用-s选项增大捕捉缓冲区.`-s128'应该效果不错了.
a. 正向解析
IPv4的地址正向解析的资源记录是“A”记录。IPv6地址的正向解析目前有两种资源记录,即,“AAAA”和“A6”记录。其中, “AAAA”较早提出<4>,它是对“A”记录的简单扩展,由于IP地址由32位扩展到128位,扩大了4倍,所以资源记录由“A”扩大成4 个“A”。“AAAA”用来表示域名和IPv6地址的对应关系,并不支持地址的层次性。
“A6”在RFC2874<5>中提出,它是把一个IPv6地址与多个“A6”记录建立联系,每个“A6”记录都只包含了IPv6地址的一部分,结合后拼装成一个完整的IPv6地址。“A6”记录支持一些“AAAA”所不具备的新特性,如地址聚合,地址更改(Renumber)等。
首先,“A6”记录方式根据TLA、NLA和SLA的分配层次把128位的IPv6的地址分解成为若干级的地址前缀和地址后缀,构成了一个地址链。每个地址前缀和地址后缀都是地址链上的一环,一个完整的地址链就组成一个IPv6地址。这种思想符合IPv6地址的层次结构,从而支持地址聚合。
其次,用户在改变ISP时,要随ISP改变而改变其拥有的IPv6地址。如果手工修改用户子网中所有在DNS中注册的地址,是一件非常繁琐的事情。而在用“A6”记录表示的地址链中,只要改变地址前缀对应的ISP名字即可,可以大大减少DNS中资源记录的修改。并且在地址分配层次中越靠近底层,所需要改动的越少。
b. 反向解析
IPv6反向解析的记录和IPv4一样,是“PTR”,但地址表示形式有两种。一种是用 “.”分隔的半字节16进制数字格式(Nibble Format),低位地址在前,高位地址在后,域后缀是“IP6.INT.”。另一种是二进制串(Bit-string)格式,以“\<”开头, 16进制地址(无分隔符,高位在前,低位在后)居中,地址后加“>”,域后缀是“IP6.ARPA.”。半字节16进制数字格式与“AAAA”对应,是对IPv4的简单扩展。二进制串格式与“A6”记录对应,地址也象“A6”一样,可以分成多级地址链表示,每一级的授权用“DNAME”记录。和 “A6”一样,二进制串格式也支持地址层次特性。
总之,以地址链形式表示的IPv6地址体现了地址的层次性,支持地址聚合和地址更改。但是,由于一次完整的地址解析分成多个步骤进行,需要按照地址的分配层次关系到不同的DNS服务器进行查询。所有的查询都成功才能得到完整的解析结果。这势必会延长解析时间,出错的机会也增加。因此,需要进一步改进DNS地址链功能,提高域名解析的速度才能为用户提供理想的服务。