遇到了域名解析超时的问题,顺便梳理下 GO 的 DNS 解析流程。出错的域名是这个 p.qpic.cn, 当用 sudo tcpdump port 53 -nn 分析解析过程发现,ipv4 解析过程非常快,ipv6 很慢,过了一段时间才有响应。也就是说 ipv6 解析慢,导致 go http client dns 解析超时。
可以用 dig 确认下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
dig p.qpic.cn ; <<>> DiG 9.9.4-RedHat-9.9.4-51.el7 <<>> p.qpic.cn ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 1729 ;; flags: qr rd ra; QUERY: 1, ANSWER: 16, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;p.qpic.cn. IN A ;; ANSWER SECTION: p.qpic.cn. 5 IN A 14.17.41.196 p.qpic.cn. 5 IN A 183.61.38.193 p.qpic.cn. 5 IN A 113.96.208.23 p.qpic.cn. 5 IN A 14.215.140.25 p.qpic.cn. 5 IN A 113.96.208.22 p.qpic.cn. 5 IN A 183.3.233.232 p.qpic.cn. 5 IN A 113.96.232.48 p.qpic.cn. 5 IN A 113.96.232.105 p.qpic.cn. 5 IN A 14.215.140.21 p.qpic.cn. 5 IN A 183.36.108.188 p.qpic.cn. 5 IN A 14.215.140.24 p.qpic.cn. 5 IN A 14.215.140.53 p.qpic.cn. 5 IN A 113.96.232.100 p.qpic.cn. 5 IN A 113.96.208.21 p.qpic.cn. 5 IN A 183.61.38.202 p.qpic.cn. 5 IN A 183.36.108.166 ;; Query time: 14 msec ;; SERVER: 172.16.185.2#53(172.16.185.2) ;; WHEN: 四 2月 14 10:53:01 CST 2019 ;; MSG SIZE rcvd: 283 |
发现解析的非常快,发现返回的都是 A 记录,也就是 ipv4 的结果,显示的指定查询 ipv6 结果,可以这样
1 2 3 4 5 |
dig AAAA p.qpic.cn ; <<>> DiG 9.9.4-RedHat-9.9.4-51.el7 <<>> AAAA p.qpic.cn ;; global options: +cmd ;; connection timed out; no servers could be reached |
发现,超时解析失败了。
go 进行 dns 查询有两种实现方式, cgo 和 纯 go 的实现。推荐纯 go 的实现。 cgo 在查询时,会占用系统的线程,而 纯 go 的 是使用 goroutine 进行查询的。
下面一步步分析 dns 解析流程。 测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# cat net.go package main import ( "fmt" "net" "os" ) func main() { ns, err := net.LookupHost("p.qpic.cn") if err != nil { fmt.Fprintf(os.Stderr, "Err: %s", err.Error()) return } for _, n := range ns { fmt.Fprintf(os.Stdout, "--%s\n", n) } } |
使用 go build net.go 会生成 net 执行文件。使用 sudo strace -T ./net 跟踪执行流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("172.16.185.2")}, 16) = 0 <0.000009> epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=3685703344, u64=140225777983152}}) = 0 <0.000005> getsockname(3, {sa_family=AF_INET, sin_port=htons(58518), sin_addr=inet_addr("172.16.185.133")}, [16]) = 0 <0.000012> getpeername(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("172.16.185.2")}, [16]) = 0 <0.000004> write(3, "7\352\1\0\0\1\0\0\0\0\0\0\1p\4qpic\2cn\0\0\34\0\1", 27) = 27 <0.000131> read(3, 0xc4200ac000, 512) = -1 EAGAIN (Resource temporarily unavailable) <0.000006> futex(0x5bf520, FUTEX_WAIT, 0, {4, 999552904}) = -1 ETIMEDOUT (Connection timed out) <4.999927> futex(0x5bf7d8, FUTEX_WAKE, 1) = 1 <0.000025> futex(0xc42002cb90, FUTEX_WAKE, 1) = 1 <0.000028> epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc42003bb14) = 0 <0.000023> close(3) = 0 <0.000029> futex(0xc42002cb90, FUTEX_WAKE, 1) = 1 <0.000024> socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3 <0.000224> setsockopt(3, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0 <0.000029> connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("172.16.185.2")}, 16) = 0 <0.000017> epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=3685703344, u64=140225777983152}}) = 0 <0.000013> getsockname(3, {sa_family=AF_INET, sin_port=htons(59338), sin_addr=inet_addr("172.16.185.133")}, [16]) = 0 <0.000008> getpeername(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("172.16.185.2")}, [16]) = 0 <0.000008> futex(0x5bf520, FUTEX_WAKE, 1) = 1 <0.000020> write(3, ">\360\1\0\0\1\0\0\0\0\0\0\1p\4qpic\2cn\0\0\34\0\1", 27) = 27 <0.001253> read(3, 0xc4200ac200, 512) = -1 EAGAIN (Resource temporarily unavailable) <0.000155> |
会发现有两次 connect 操作, 一次是 4.99 秒后超时了,一次成功返回了。从 go 源码分析下。
在 net/lookup.go 中,看到 LookupHost 调用的是 DefaultResolver 的 LookupHost。DefaultResolver 是 Resolver 默认实现实例,在相同的文件里可以找到 func (r *Resolver) LookupHost(ctx context.Context, host string) (addrs []string, err error) 定义。
这个函数首先对 host 进行分析,如果 host 直接 是 IP, 不管是 ipv4 或者 ipv6 形式,就直接返回了。否则继续查询。
后面具体的实现是和系统相关的。在 net/lookup_unix.go 中找到 func (r *Resolver) lookupHost(ctx context.Context, host string) (addrs []string, err error) 。我们是找纯 go 的实现,直接找 goLookupHostOrder 定义。在 net/dnsclient_unix.go 可以找到。
goLookupHostOrder 首先是查找 /etc/hosts 里有没有域名的配置,如果有则直接返回,否则继续查找。
继续在本文件找到 goLookupIPCNAMEOrder 实现。
在这里加载了 /etc/resolv.conf 内容,在这个配置里,包含 dns server 地址及查询超时的具体控制。具体内容参考 resolv.conf 。这个配置会加载到 dnsConfig 中。加载的文件在 net/dnsconfig_unix.go 中,参考函数 dnsReadConfig。
在这里有两个参数比较重要, 一个 timeout 查询 dns server 的超时时间,attempts 指的是重试次数。timeout 默认是 5s, strace 跟踪的超时时间得到验证。这两个选项可以在 /etc/resolv.conf 自定义。
1 2 3 4 5 6 |
cat /etc/resolv.conf ; generated by /usr/sbin/dhclient-script search localdomain nameserver 172.16.185.2 options timeout:2 options attempts:1 |
我们设置了超时时间为 2s, 重试次数为 1 次。再次使用 strace 进行跟踪的话,可以看到超时时间变成了 2s 左右。
继续来看 goLookupIPCNAMEOrder 的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
qtypes := [...]uint16{dnsTypeA, dnsTypeAAAA} var lastErr error for _, fqdn := range conf.nameList(name) { for _, qtype := range qtypes { go func(qtype uint16) { cname, rrs, err := r.tryOneName(ctx, conf, fqdn, qtype) lane <- racer{cname, rrs, err} }(qtype) } hitStrictError := false for range qtypes { racer := <-lane if racer.error != nil { if nerr, ok := racer.error.(Error); ok && nerr.Temporary() && r.StrictErrors { // This error will abort the nameList loop. hitStrictError = true lastErr = racer.error } else if lastErr == nil || fqdn == name+"." { // Prefer error for original name. lastErr = racer.error } continue } addrs = append(addrs, addrRecordList(racer.rrs)...) if cname == "" { cname = racer.cname } } if hitStrictError { // If either family hit an error with StrictErrors enabled, // discard all addresses. This ensures that network flakiness // cannot turn a dualstack hostname IPv4/IPv6-only. addrs = nil break } if len(addrs) > 0 { break } } |
这里启动了两个 goroutine,分别取查询 ipv4 和 ipv6。然后通过 channel 进行结果的汇总。那也就解释了 就算 ipv4 查询很快,ipv6 查询很慢的话,照样会卡主应用,造成 dns 查询超时。
tryOneName 函数就是 dns 查询的具体逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
func (r *Resolver) tryOneName(ctx context.Context, cfg *dnsConfig, name string, qtype uint16) (string, []dnsRR, error) { var lastErr error serverOffset := cfg.serverOffset() sLen := uint32(len(cfg.servers)) for i := 0; i < cfg.attempts; i++ { for j := uint32(0); j < sLen; j++ { server := cfg.servers[(serverOffset+j)%sLen] msg, err := r.exchange(ctx, server, name, qtype, cfg.timeout) if err != nil { lastErr = &DNSError{ Err: err.Error(), Name: name, Server: server, } if nerr, ok := err.(Error); ok && nerr.Timeout() { lastErr.(*DNSError).IsTimeout = true } // Set IsTemporary for socket-level errors. Note that this flag // may also be used to indicate a SERVFAIL response. if _, ok := err.(*OpError); ok { lastErr.(*DNSError).IsTemporary = true } continue } // libresolv continues to the next server when it receives // an invalid referral response. See golang.org/issue/15434. if msg.rcode == dnsRcodeSuccess && !msg.authoritative && !msg.recursion_available && len(msg.answer) == 0 && len(msg.extra) == 0 { lastErr = &DNSError{Err: "lame referral", Name: name, Server: server} continue } cname, rrs, err := answer(name, server, msg, qtype) // If answer errored for rcodes dnsRcodeSuccess or dnsRcodeNameError, // it means the response in msg was not useful and trying another // server probably won't help. Return now in those cases. // TODO: indicate this in a more obvious way, such as a field on DNSError? if err == nil || msg.rcode == dnsRcodeSuccess || msg.rcode == dnsRcodeNameError { return cname, rrs, err } lastErr = err } } return "", nil, lastErr } |
如果配置了多个 name server, 那么是通过轮询的方式查询,还是每次都从第一个 server 进行查询。选项 rotate 控制的。配置了,就是轮询,否则就是从第一个开始来。
这里有两层循环,第一层就是 attempts 控制重试次数,第二层就是遍历 name server,如果查到了就返回,失败了就轮询下一个。
总结
如果遇到了 dns 问题,具体怎么做呢?
- 直接配置映射关系, 放到 /etc/hosts 中。如果 ip, host 关系固定,好说,否则,就无法实现
- 修改 go 源码,控制不解析 ipv6, 小场景下,合适,但不是长期方案
- 精心配置 /etc/resolv.conf,尤其是与应用设置的超时时间对应起来,timeout, attempts, rotate 都要考虑好。name server 可以选不同的服务商,保证可用性。
Pingback引用通告: k8s 里 DNS 解析超时追踪分析 | bruceding.blog