go dns解析原理及调优

nanjingfm · · 7859 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

# 背景 有同学通过`zipkin`发现`dns`解析偶尔会花费40ms(预期是1ms以内),并且猜测和`alpine`镜像有关系。 ![image-20220111220415183](https://bbk-images.oss-cn-shanghai.aliyuncs.com/typora/20220111220415.png) 第一反应不太可能是`alpine`镜像的问题(`alpine`镜像使用这么频繁,如果有问题应该早就修复了),下面针对这个问题进行分析。 # Go中dns解析过程 首先我们了解下`golang`中如何进行dns解析的。直接看代码,关键函数`goLookupIPCNAMEOrder` ```go // src/net/dnsclient_unix.go func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name string, order hostLookupOrder) (addrs []IPAddr, cname dnsmessage.Name, err error) { // 省略检查代码 // 读取/etc/resolv.conf,防止读取频繁,5秒钟生效一次 resolvConf.tryUpdate("/etc/resolv.conf") // ... // 默认解析ipv4和ipv6 qtypes := []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA} // 【关键】根据network的不同,4结尾的只解析ipv4,6结尾的只解析ipv6 switch ipVersion(network) { case '4': qtypes = []dnsmessage.Type{dnsmessage.TypeA} case '6': qtypes = []dnsmessage.Type{dnsmessage.TypeAAAA} } // ... // 判断/etc/resolv.conf里面的single-request和single-request-reopen参数,如果设置的话,就是串行请求,否者是并行请求 if conf.singleRequest { queryFn = func(fqdn string, qtype dnsmessage.Type) {} responseFn = func(fqdn string, qtype dnsmessage.Type) result { dnsWaitGroup.Add(1) defer dnsWaitGroup.Done() p, server, err := r.tryOneName(ctx, conf, fqdn, qtype) return result{p, server, err} } } else { queryFn = func(fqdn string, qtype dnsmessage.Type) { dnsWaitGroup.Add(1) // 看到go关键字了么?没有设置single-request就是并发解析 go func(qtype dnsmessage.Type) { p, server, err := r.tryOneName(ctx, conf, fqdn, qtype) lane <- result{p, server, err} dnsWaitGroup.Done() }(qtype) } responseFn = func(fqdn string, qtype dnsmessage.Type) result { return <-lane } } // 下面代码也很重要 var lastErr error // len(namelist) = len(search domain) + 1 // 遍历nameserver,resolv.conf中可以配置多个nameserver,比如下面的配置namelist长度就是4: // nameserver 169.254.20.10 // nameserver 172.16.0.10 // search meipian-test.svc.cluster.local svc.cluster.local cluster.local for _, fqdn := range conf.nameList(name) { // ... // 遍历解析类型,这里就是ipv4和ipv6 for _, qtype := range qtypes { // .... } } // ... return addrs, cname, nil } ``` 通过以上代码我们可以得出以下结论: ## go实现了`dns`解析 **`Dns`解析跟是不是`alpine`镜像没有关系**,因为`go` 中`dns解析`是自己实现的,不依赖于系统调用。`go build tag`也证明了这一点 ```go //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris ``` ## 内置解析器会读取配置文件 `go`程序会读取并解析`/etc/resolv.conf`文件,并且[标准选项](http://man7.org/linux/man-pages/man5/resolv.conf.5.html)都有实现,包括`single-request`和`single-request-reopen option`设置。 ```go // src/net/dnsconfig_unix.go case s == "single-request" || s == "single-request-reopen": // Linux option: // http://man7.org/linux/man-pages/man5/resolv.conf.5.html // "By default, glibc performs IPv4 and IPv6 lookups in parallel [...] // This option disables the behavior and makes glibc // perform the IPv6 and IPv4 requests sequentially." conf.singleRequest = true ``` ## single-request参数是有效的 如果设置了`single request`选项,`dns`解析的时候是串行的 ```go if conf.singleRequest { queryFn = func(fqdn string, qtype dnsmessage.Type) {} responseFn = func(fqdn string, qtype dnsmessage.Type) result { dnsWaitGroup.Add(1) defer dnsWaitGroup.Done() p, server, err := r.tryOneName(ctx, conf, fqdn, qtype) return result{p, server, err} } } ``` 如果没有设置`single-request`选项,`dns解析`是并行的(真实情况是并行和串行结合的)。 ```go if conf.singleRequest { // ... } else { queryFn = func(fqdn string, qtype dnsmessage.Type) { dnsWaitGroup.Add(1) go func(qtype dnsmessage.Type) { p, server, err := r.tryOneName(ctx, conf, fqdn, qtype) lane <- result{p, server, err} dnsWaitGroup.Done() }(qtype) } responseFn = func(fqdn string, qtype dnsmessage.Type) result { return <-lane } } ``` ## 解析过程和配置相关 `dns`解析策略、次数和`ndots`、`search domain`和`nameserver`配置强相关: 1. 默认情况下`dns`查询会同时解析`IPv4`和`IPv6`地址(不论容器是否支持`IPv6`) 2. `ndots`和待解析的域名决定要不要优先使用`search domain`,**通俗一点说,如果你的域名请求参数中,`点的个数`比配置的`ndots`小,则会优先拼接`search domain`后去解析**,比如有如下配置: ``` search meipian-test.svc.cluster.local svc.cluster.local cluster.local options ndots:3 ``` 如果现在解析的域名是`www.baidu.com`,`ndots`配置的是`3`,待解析域名中的点数(2)比 ndots 小,所以会优先拼接搜索域名去解析,解析顺序如下: - www.baidu.com.meipian-test.svc.cluster.local. - www.baidu.com.svc.cluster.local. - www.baidu.com.cluster.local. - www.baidu.com. 如果配置文件中`ndots`等于`2`,则解析顺序如下: - www.baidu.com. - www.baidu.com.meipian-test.svc.cluster.local. - www.baidu.com.svc.cluster.local. - www.baidu.com.cluster.local. 3. `serach domain`和`nameserver`决定了`dns`**最多**查询的次数,即查询次数等于`搜素域的数量+1`乘以`dnsserver的数量`。比如有以下配置: ``` nameserver 169.254.20.10 nameserver 172.16.0.10 search meipian-test.svc.cluster.local svc.cluster.local cluster.local options ndots:3 ``` 当我们解析`www.baidu.com`域名时,解析顺序如下: | 解析域名 | 查询类型 | dns server | | -- | -- | -- | | www.baidu.com.meipian-test.svc.cluster.local. | A | 169.254.20.10 | | www.baidu.com.meipian-test.svc.cluster.local. | A | 172.16.0.10 | | www.baidu.com.meipian-test.svc.cluster.local. | AAAA | 169.254.20.10 | | www.baidu.com.meipian-test.svc.cluster.local. | AAAA | 172.16.0.10 | | www.baidu.com.svc.cluster.local. | A | 169.254.20.10 | | www.baidu.com.svc.cluster.local. | A | 172.16.0.10 | | www.baidu.com.svc.cluster.local. | AAAA | 169.254.20.10 | | www.baidu.com.svc.cluster.local. | AAAA | 172.16.0.10 | | www.baidu.com.cluster.local. | A | 169.254.20.10 | | www.baidu.com.cluster.local. | A | 172.16.0.10 | | www.baidu.com.cluster.local. | AAAA | 169.254.20.10 | | www.baidu.com.cluster.local. | AAAA | 172.16.0.10 | | www.baidu.com. | A | 169.254.20.10 | | www.baidu.com. | A | 172.16.0.10 | | www.baidu.com. | AAAA | 169.254.20.10 | | www.baidu.com. | AAAA | 172.16.0.10 | 一共16次,是不是很恐怖?当然只有在最坏的情况(比如域名确实不存在时)才会有这么多次请求。 ![image-20220112015048040](https://bbk-images.oss-cn-shanghai.aliyuncs.com/typora/20220112015048.png) > ⚠️ 串行和并行请求是如何结合的? > > 并行是指**同一个域名**的去**同一个dns server**解析**不同的类型**时是并行的,不同的域名之间还是串行的。 把请求放在时间线上就像下面这样: ![image-20220112094110024](https://bbk-images.oss-cn-shanghai.aliyuncs.com/typora/20220112094110.png) 上图话的是最坏的情况,**实际上过程中只要有一次解析成功就返回了**。 ## 内置解析器参数默认值 ```go ndots: 1, timeout: 5 * time.Second, // dns解析超时时间为5秒,有点太长了 attempts: 2, // 解析失败,重试两次 defaultNS = []string{"127.0.0.1:53", "[::1]:53"} // 默认dns server search:os.Hostname // ``` 其中需要注意的就是`timeout`,建议在`resolv.conf`上加上这个参数,并且写个较小的值。因为`dns`解析默认是`udp`请求(不可靠),如果发生丢包情况就会等5s。 # Dns 解析策略 上面说到`go`使用的是内置解析器,其实并不是所有情况都是这样的。 ## 两种解析器 `golang`有两种域名解析方法:内置`go`解析器和基于`cgo`的系统解析器。 ```go // src/net/cgo_stub.go //go:build !cgo || netgo // +build !cgo netgo func init() { netGo = true } // src/net/conf_netcgo.go //go:build netcgo // +build netcgo func init() { netCgo = true } ``` 默认情况下用的是内置解析,如果你想指定使用`cgo`解析器,可以`build`的时候指定。 ```go export GODEBUG=netdns=go # force pure Go resolver export GODEBUG=netdns=cgo # force cgo resolver ``` ## 内置解析器解析策略 当`goos=linux`下使用的是 `hostLookupFilesDNS` ,也就是说,`hosts`解析优先`dns`解析(go1.17.5)。 ```go const ( // hostLookupCgo means defer to cgo. hostLookupCgo hostLookupOrder = iota hostLookupFilesDNS // files first hostLookupDNSFiles // dns first hostLookupFiles // only files hostLookupDNS // only DNS ) var lookupOrderName = map[hostLookupOrder]string{ hostLookupCgo: "cgo", hostLookupFilesDNS: "files,dns", hostLookupDNSFiles: "dns,files", hostLookupFiles: "files", hostLookupDNS: "dns", } ``` 根据操作系统的不同,使用的解析策略也会略有不同,比如`android`平台就会强制使用`cgo` ```go // src/net/conf.go fallbackOrder := hostLookupCgo // ... if c.forceCgoLookupHost || c.resolv.unknownOpt || c.goos == "android" { return fallbackOrder } ``` # 禁用IPv6解析 在`go1.17`之前是没有办法禁用`ipv6`解析的。`1.17`之后`go`提供了一些方式 ```go // 默认是IPv4和IPv6都解析 qtypes := []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA} // 根据network的不同可以只解析ipv4或者只解析ipv6 switch ipVersion(network) { case '4': qtypes = []dnsmessage.Type{dnsmessage.TypeA} case '6': qtypes = []dnsmessage.Type{dnsmessage.TypeAAAA} } // ipVersion returns the provided network's IP version: '4', '6' or 0 // if network does not end in a '4' or '6' byte. func ipVersion(network string) byte { if network == "" { return 0 } n := network[len(network)-1] if n != '4' && n != '6' { n = 0 } return n } ``` 所以想要禁用`IPv6`解析的话就很容易了,我们只需要在建立连接的时候指定`network`类型。以`http`为例,重写`Transport`的`DialContext`方法,将原来的`network`(默认是`tcp`)强制写成`tcp4`。 ```go &http.Client{ Transport: &http.Transport{ // .... DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { // 强制使用ipv4解析 return zeroDialer.DialContext(ctx, "tcp4", addr) }, } } ``` # 总结 1. `go`默认使用内置`dns`解析器,不依赖操作系统,跟基础镜像无关 2. `go`内置解析器会读取`/etc/resov.conf`配置,[标准配置](https://man7.org/linux/man-pages/man5/resolv.conf.5.html)都有实现,手动修改配置5秒后生效 3. `Go1.17`之后可以禁用`ipv6`解析 4. `go`内置解析器解析过程默认是并行和串行结合的 - 相同域名的不同请求类型是并行的 - 不同域名之间是串行的 ## 优化建议 1. 修改`ndots`为合适的值 `k8s`中如何配置的`dnsPolicy`是`ClusterFist`,默认`ndots会是`5` - 如果微服务之前请求使用的是`service name`,那么不需要修改(拼接搜索域名之后是可以成功解析的) - 如果微服务之间请求使用的是域名(或者说拼接搜索域名之后一定解析不到的情况下),**需要将`ndots`设置成合适值**,目标是把原始域名放在前面解析(拼接搜索域名放在后面) 2. 修改`timeout`为合适的值 `go`默认是`5s`,因为`udp`请求的不可靠性,一旦遇到丢包情况,就会让程序等到天荒地老 3. 禁用`Ipv6`解析开启`single-request` 对于`go`内置解析器而言`single-request`和`single-request-reopen`是同一个意思,这决定了不同解析请求(`A`或者`AAAA`)是并发还是串行,默认是并行。如果禁用了`IPv6`,就没有并发解析的必要了,建议开始`single-request` ## 优化效果 dns解析只有有效的A记录查询了,世界突然安静了。 ![image-20220112121135900](https://bbk-images.oss-cn-shanghai.aliyuncs.com/typora/20220112121135.png)

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

7859 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传