一、Overview
我的工作内容是模拟各种现实中的故障,比如网络丢包、延迟,CPU
满载,磁盘爆满等。
最近在盘点「域名访问不通
」这一项功能时,发现某些情况下,没有按照预期生效。
为了讲清楚遇到的问题,我先来解释我到底是如何实现「域名访问不通」这一功能的。
其实原理很简单,那便是直接修改/etc/hosts
文件。
比如你想要实现访问www.baidu.com
不通的情形,你完全可以如此做:
arduino
127.0.0.1 odin.xiaojukeji.comwww.baidu.com #chaosblade
如此一来,当你在浏览器或者终端curl访问www.baidu.com
的时候,会报错。
我遇到的问题是,在某种情况下,即便我进行了上述的设置,但结果还是访问到百度的服务器,而不是我设置的127.0.0.1
。
下面讲一下ping
、go-http
两种失效的场景,失效的现象捕捉以及原因。
二、ping
2.1、问题复现
我们的目标是将www.baidu.com
映射到本地local 127.0.0.1
。
修改 /etc/hosts
文件之前,我们先在目标机器上运行ping www.baidu.com
python
~$ ping www.baidu.com
PING www.baidu.com (110.242.68.4) 56(84) bytes of data.
64 bytes from 110.242.68.4: icmp_seq=1 ttl=55 time=3.32 ms
64 bytes from 110.242.68.4: icmp_seq=2 ttl=55 time=4.39 ms
64 bytes from 110.242.68.4: icmp_seq=3 ttl=55 time=2.39 ms
64 bytes from 110.242.68.4: icmp_seq=4 ttl=55 time=3.66 ms
在不中断上述ping
命令的前提下,进行域名访问不通(修改/etc/hosts
文件 127.0.0.1 www.baidu.com #chaosblade
)。
在修改完/etc/hosts
文件后,发现ping www.baidu.com
的结果未曾改变,依然是from 110.242.68.4
。
但此时开启另一个终端,运行ping www.baidu.com
,发现是符合预期的!
python
~$ ping www.baidu.com
PING www.baidu.com (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.014 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.007 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.011 ms
64 bytes from localhost (127.0.0.1): icmp_seq=4 ttl=64 time=0.009 ms
64 bytes from localhost (127.0.0.1): icmp_seq=5 ttl=64 time=0.007 ms
64 bytes from localhost (127.0.0.1): icmp_seq=6 ttl=64 time=0.006 ms
难道是ping
的实现中有缓存?
2.2、问题排查
其实就是看ping
命令的实现原理
经过源码解读,ping
的实现如下:在进入不断发出icmp
数据包的逻辑之前,用户指定的域名只会被解析一次。所以,在ping
的过程中,即便修改hosts
文件,正在运行的ping
进程是没有感知的。
三、http-go
3.1、问题复现
后来我发现,go
进行http
请求的时候,也会发生与ping
类似的情况。
比如下面的例子,进行50
次www.baidu.com
主页的http get
请求,每次请求间隔为3s
。在程序运行的过程中,修改/etc/hosts
文件,看是否能够正常发出请求和收到响应。
go
# 测试代码
func TestHTTPGet(t *testing.T) {
url := "http://www.baidu.com"
for i := 0; i < 50; i++ {
resp, err := http.Get(url)
if err != nil {
t.Error(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Error(err)
}
fmt.Println(string(body))
time.Sleep(3 * time.Second)
}
}
抓包展示1
:细节
抓包展示2:统计
发现并没有因为更改了hosts
文件,go
的http
库便将www.baidu.com
的IP
指向127.0.0.1
。
3.2、问题排查
通过抓包分析,这50
个http
请求使用了长连接技术,因为源端口都是58443
,当然这也符合HTTP 1.1 persistent connection
的特性。
你可能有疑问,通过这个HTTP
请求头部来看,并没有发现表示长连接 persistent connection
的设置啊?
确实没有,但这不过是因为HTTP/1.1
默认就是开启长连接的。
3.3、另外复现方式
上面go
代码,又得运行又得抓包才能确认http
请求是长连接,所以即便是修改了/etc/hosts
文件,也没有进行新的dns
查询,导致一直沿用首次dns
查询的ip
,而不是设置的127.0.0.1
。
有没有一种方式,不用抓包,我们也能得知dns
查询只进行了一次呢?当然可以。
我们可以利用标准库 httptrace
go
func createHTTPTraceRequest(ctx context.Context, url string) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return req, err
}
// 增加一些行为日志打印
trace := &httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) { // 获取底层连接,http通过底层连接将请求发送出去
fmt.Printf("GotConn info:%+v\n", info)
},
DNSStart: func(info httptrace.DNSStartInfo) { // 进行dns查询,打印之
fmt.Printf("DNSStart info:%+v\n", info)
},
DNSDone: func(info httptrace.DNSDoneInfo) { // 完成dns查询,打印之
fmt.Printf("DNSDone info:%+v\n", info)
},
}
traceCtx := httptrace.WithClientTrace(ctx, trace)
req = req.WithContext(traceCtx)
return req, nil
}
func TestWithTraceHTTP(t *testing.T) {
url := "http://www.baidu.com"
for i := 0; i < 50; i++ {
// 构建request结构
req, err := createHTTPTraceRequest(context.Background(), url)
if err != nil {
t.Error(err)
}
// 发送请求
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Error(err)
}
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Error(err)
}
time.Sleep(3 * time.Second)
}
}
输出如下:
css
DNSStart info:{Host:www.baidu.com}
DNSDone info:{Addrs:[{IP:110.242.68.3 Zone:} {IP:110.242.68.4 Zone:} {IP:2408:871a:2100:2:0:ff:b09f:237 Zone:} {IP:2408:871a:2100:3:0:ff:b025:348d Zone:}] Err:<nil> Coalesced:false}
GotConn info:{Conn:0x14000186000 Reused:false WasIdle:false IdleTime:0s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001353958s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001554083s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.00088375s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.00107s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001237583s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001469083s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.002459209s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.00587425s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001838042s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.007866292s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.00083075s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000906125s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001565917s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.002019208s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001439292s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000538625s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001538042s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001537375s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000745167s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000566916s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001680083s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001349s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001143166s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.00183775s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001327459s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001370041s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001351875s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001454833s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001327166s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000833042s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001082209s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001355333s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.002008875s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001410666s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000633791s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001041583s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000719583s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000613334s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000894167s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000837292s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001611s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001473875s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000864875s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000621208s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.000998083s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001407s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.00110875s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.00126275s}
GotConn info:{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:3.001923709s}
仔细查看上述日志信息,你会发现如下事实:
1、50
个http
请求只进行了一次dns
查询,这是我们修改/etc/hosts
文件未达预期的根本原因。
2、只有第一次发送http
连接时,创建了新的底层连接,后面的请求全都是复用之前的底层连接。
何以见得?
看GotConn
中的Reused
字段,false
表示新建连接,true
表示复用底层连接。
四、Summary
当操作系统收到dns
查询的委托,它会首先查看待查询的域名在/etc/hosts
文件中是否有相应配置,若没有则调用dns
服务来进行递归查询。
修改完/etc/hosts
文件之后,我们期望它能够立马生效,它也确实"立马生效了"。
但还是存在一些特殊情况,即便修改文件,文件的改动也触达不了这些场景,比如上文中讲述的ping
和http
。
我觉得我们可以称之为缓存问题。
啊哈,这就是计算机的世界中两大难题之一:
- 命名
- 缓存
哈哈