• coredns解析缓慢分析


     
    Go DNS解析原理及调优
    背景
    有同学通过zipkin发现dns解析偶尔会花费40ms(预期是1ms以内),并且猜测和alpine镜像有关系。
    image-20220111220415183
    第一反应不太可能是alpine镜像的问题(alpine镜像使用这么频繁,如果有问题应该早就修复了),下面针对这个问题进行分析。
    Go中dns解析过程
    首先我们了解下golang中如何进行dns解析的。直接看代码,关键函数goLookupIPCNAMEOrder
    // 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:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
    // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
    内置解析器会读取配置文件
    go程序会读取并解析/etc/resolv.conf文件,并且标准选项都有实现,包括single-request和single-request-reopen option设置。
    // 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解析的时候是串行的
    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解析是并行的(真实情况是并行和串行结合的)。
    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配置强相关:
    默认情况下dns查询会同时解析IPv4和IPv6地址(不论容器是否支持IPv6)
    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.
    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
    ⚠️ 串行和并行请求是如何结合的?
    并行是指同一个域名的去同一个dns server解析不同的类型时是并行的,不同的域名之间还是串行的。
    把请求放在时间线上就像下面这样:
    image-20220112094110024
    上图话的是最坏的情况,实际上过程中只要有一次解析成功就返回了。
    内置解析器参数默认值
    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的系统解析器。
    // 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的时候指定。
    export GODEBUG=netdns=go    # force pure Go resolver
    export GODEBUG=netdns=cgo   # force cgo resolver
    内置解析器解析策略
    当goos=linux下使用的是 hostLookupFilesDNS ,也就是说,hosts解析优先dns解析(go1.17.5)。
    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
    // src/net/conf.go
    
    fallbackOrder := hostLookupCgo
    // ...
    if c.forceCgoLookupHost || c.resolv.unknownOpt || c.goos == "android" {
            return fallbackOrder
        }
    禁用IPv6解析
    在go1.17之前是没有办法禁用ipv6解析的。1.17之后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。
    &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配置,标准配置都有实现,手动修改配置5秒后生效
    3. Go1.17之后可以禁用ipv6解析
    4. go内置解析器解析过程默认是并行和串行结合的
    • 相同域名的不同请求类型是并行的
    • 不同域名之间是串行的
    优化建议
    修改ndots为合适的值
    推荐部署 Nodelocal DNS Cache 扩展来解决这个问题,同时也提升 DNS 解析的性能
    k8s中如何配置的dnsPolicy是ClusterFist,默认ndots会是5`
    • 如果微服务之前请求使用的是service name,那么不需要修改(拼接搜索域名之后是可以成功解析的)
    • 如果微服务之间请求使用的是域名(或者说拼接搜索域名之后一定解析不到的情况下),需要将ndots设置成合适值,目标是把原始域名放在前面解析(拼接搜索域名放在后面)
    修改timeout为合适的值
    go默认是5s,因为udp请求的不可靠性,一旦遇到丢包情况,就会让程序等到天荒地老
    禁用Ipv6解析开启single-request
    对于go内置解析器而言single-request和single-request-reopen是同一个意思,这决定了不同解析请求(A或者AAAA)是并发还是串行,默认是并行。如果禁用了IPv6,就没有并发解析的必要了,建议开始single-request
    优化效果
    dns解析只有有效的A记录查询了,世界突然安静了。
    image-20220112121135900
     
  • 相关阅读:
    wtforms 钩子函数
    sqlalchemy 单表增删改查
    sqlalchemy 数据库操作
    flask wtforms组件
    python----------文件操作
    编码 编码的转化
    集合、深浅copy
    字典
    sort 、sorted、range、join方法 数字的正序、倒叙、翻转
    for循环
  • 原文地址:https://www.cnblogs.com/lidong94/p/16287719.html
Copyright © 2020-2023  润新知