Go代理10S超时

背景

机器通过代理去请求,请求会触发超时,稳定在10S多一点。

分析

几个现象:

首先,抓个包看下:

img

抓包能看出:

猜测是代理机器的问题,但是在代理机器用curl去访问目标机器也没问题,难道是代理程序问题?
想到之前有看到过一篇文章,go dns触发超时,难道是DNS问题?

为了快速解决验证问题,马上将DNS换成网管提供的最新IP,发现超时问题确实解决了。

来看看Go是怎么解析DNS,为什么超时是10S。

func (r *Resolver) lookupHost(ctx context.Context, host string) (addrs []string, err error) {
    order := systemConf().hostLookupOrder(r, host)
    if !r.preferGo() && order == hostLookupCgo {
        if addrs, err, ok := cgoLookupHost(ctx, host); ok {
            return addrs, err
        }
        // cgo not available (or netgo); fall back to Go's DNS resolver
        order = hostLookupFilesDNS
    }
    return r.goLookupHostOrder(ctx, host, order)

Go通过不同环境、配置来采取是用cgo还是pure go来解析DNS。

如果cgo解析失败,尝试用pure go解析。darwin默认强制使用cgo解析,linux使用pure go。当然你也可以通过GODEBUG来改:

⇒  GODEBUG=netdns=go+2 go run test.go
go package net: GODEBUG setting forcing use of Go's resolver
go package net: hostLookupOrder(baidu.com) = files,dns
2020/04/21 22:13:22 [39.156.69.79 220.181.38.148]

看看pure go是怎么解析的:

// Do a lookup for a single name, which must be rooted
// (otherwise answer will not find the answers).
func (r *Resolver) tryOneName(ctx context.Context, cfg *dnsConfig, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) {
    var lastErr error
    serverOffset := cfg.serverOffset()
    sLen := uint32(len(cfg.servers))

    n, err := dnsmessage.NewName(name)
    if err != nil {
        return dnsmessage.Parser{}, "", errCannotMarshalDNSMessage
    }
    q := dnsmessage.Question{
        Name:  n,
        Type:  qtype,
        Class: dnsmessage.ClassINET,
    }

    for i := 0; i < cfg.attempts; i++ {
        for j := uint32(0); j < sLen; j++ {
            server := cfg.servers[(serverOffset+j)%sLen]

            p, h, err := r.exchange(ctx, server, q, cfg.timeout, cfg.useTCP)
            ...
    }

可以看出每个DNS server在timeout内没返回,就会尝试下一个server,直到尝试attempts次,attempts默认是2,timeout默认是5S。

// See resolv.conf(5) on a Linux machine.
func dnsReadConfig(filename string) *dnsConfig {
    conf := &dnsConfig{
        ndots:    1,
        timeout:  5 * time.Second,
        attempts: 2,
    }

可以写个简单的程序验证下:

package main

import "net"
import "log"

func main(){
    res, err := net.LookupHost("baidu.com")
    if err != nil {
        log.Fatal(err)
        return
    }
    log.Printf("%v", res)
}

DNS修改为:

options timeout:5 attempts:3
nameserver 1.1.1.1
nameserver 2.2.2.2
nameserver 3.3.3.3

DNS都不会通,正常程序会45S(5x3x3)后会失败退出:

[root@VM_15_30_centos ~]# time GODEBUG=netdns=go+3 go run main.go
go package net: GODEBUG setting forcing use of Go's resolver
go package net: hostLookupOrder(baidu.com) = files,dns
2020/04/21 22:38:01 lookup baidu.com on 3.3.3.3:53: read udp 192.168.1.2:58704->3.3.3.3:53: i/o timeout
exit status 1

real    0m45.259s
user    0m0.240s
sys 0m0.040s

go也是按标准来实现的:

        case "nameserver": // add one name server
            if len(f) > 1 && len(conf.servers) < 3 { // small, but the standard limit
                // One more check: make sure server name is
                // just an IP address. Otherwise we need DNS
                // to look it up.
              ...
            }

到这里可以判断出:之前代理机器resolv.conf前面两台DNS不能正常解析,第三台DNS是好的。所以才触发了10S超时。

另外pure go的DNS是没有cache的,而且net.Dial里面的Resolver是一个struct,不是interface,不方便去做替换。