docker中的DNS解析问题 诡异报错–DNS解析问题 报错现象 在执行 docker 命令报如下错误
1 2 3 $ docker search centos Error response from daemon: Get "https://index.docker.io/v1/search?q=centos&n=25": dial tcp: lookup index.docker.io on 192.168.1.1:53: read udp 192.168.1.107:50665->192.168.1.1:53: i/o timeout
经过几次尝试,分析发现如下:
1. 发现的报错的命令都是和联网有关系的命令。
2. 只有docker的命令有问题,centos上其他的curl,wget,yum命令都没问题,
都能正常访问网络,正常解析域名的。
3. 之前在Ubuntu系统没遇到这种诡异问题。
做过什么尝试 经过分析发现 如果改了 /etc/resolv.conf 加上个 8.8.8.8 的dns站点就能好使了。
1 2 3 4 5 6 7 8 9 10 11 12 $ cat /etc/resolv.conf # Generated by NetworkManager nameserver 192.168.0.1 nameserver 192.168.1.1 # 这个是我的路由器的IP 最后的解决办法就是 想办法改这个文件,可以直接改,也可以其他方式,总之这个问题和dns解析有关, 我是改的路由器里面 DHCP 上的2个dns地址的,之前设置的都是 0.0.0.0 $ cat /etc/resolv.conf # Generated by NetworkManager nameserver 223.5.5.5 nameserver 119.29.29.29
差异对比分析 为什么会这样呢?我在Ubuntu 环境下面也没这样。
基本上
Ubuntu上这个文件是这样的,然后 127.0.0.53 这个也是本机ip,
然后本机上开启的有个 53 端口的 domain 服务。怀疑这个服务器就是提供dns解析的。
1 2 3 $ cat /etc/resolv.conf nameserver 127.0.0.53 options edns0
Ubuntu的 systemd-resolved 将默认监听在53号端口,
1 2 3 4 5 6 7 8 $ sudo netstat -lnpt|grep 53 tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 983/systemd-resolve sudo lsof -i :53 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME systemd-r 983 systemd-resolve 12u IPv4 27226 0t0 UDP localhost:domain systemd-r 983 systemd-resolve 13u IPv4 27227 0t0 TCP localhost:domain (LISTEN)
centos 上 好像 没有这个 systemd-resolved 服务
如何调试 下载代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 下载 docker cli 仓库代码 https://github.com/golang108/docker-cli.git 尝试编译docker命令行工具,在如下目录是入口。 docker-cli/cmd/docker、docker.go 下载 dockerd 仓库代码 https://github.com/golang108/docker-moby.git 尝试编译dockerd 仓库代码。 docker-moby/cmd/dockerd。docker.go
调试需要的环境
准备个 centos 系统。
准备docker命令
准备dockerd命令
准备containerd命令
准备runc命令
/etc/resolv.conf 文件还原之前报错的那个。
基于以上几个,我们 把工具都复制到centos系统上。
1 2 3 4 $ cat /etc/resolv.conf # Generated by NetworkManager nameserver 192.168.0.1 nameserver 192.168.1.1 # 这个是我的路由器的IP
启动 container 服务 1 2 3 4 ## 切换到root账号下面 直接 执行 命令即可, [root@localhost mamh]# ./containerd
准备runc命令 这个比较简单,直接复制个 runc 命令 过来进行了,放到 /usr/bin/ 等系统环境目录下面 即可。
启动 dockerd 服务 1 2 [root@localhost mamh]# ./dockerd -G mamh --debug --raw-logs
这里加上了 –debug 等调试选项,让打印更多的日志输出。
执行 docker search 命令 执行之后, dockerd 那边 就会发现 如下的一段报错日志。
1 2 3 DEBU[2023-07-15T19:52:23.951931453+08:00] searchRepositories url="https://index.docker.io/v1/search?q=x&n=25" ERRO[2023-07-15T19:52:43.956544773+08:00] Handler for GET /v1.44/images/search returned error: Get "https://index.docker.io/v1/search?q=x&n=25": dial tcp: lookup index.docker.io on 192.168.1.1:53: read udp 192.168.1.102:39461->192.168.1.1:53: i/o timeout
过程分析:
1. 经过 docker 中 search 命令相关源码的 阅读可以知道:执行 ‘docker search x’ 时候其实是 发送了请求到 dockerd 这个进程里面了。cli/command/registry/search.go 相关文件在这里。
调用的 这个 ‘ cli.get(ctx, "/images/search", query, headers) ’ 函数发送的一个get请求。
2. 在dockerd 中 有个这样的路由配置: “ router.NewGetRoute("/images/search", ir.getImagesSearch), ” 相关文件在 api/server/router/image/image.go 中。
3. 这个路由配置里面会调用 getImagesSearch() 函数,相关文件在 api/server/router/image/image_routes.go
4. 后面又经过几个函数的调用,最终调用到searchRepositories 里面的这个“ res, err := r.client.Do(req)”, 这个 Do 方法就来自 http 包下面了
5. http 里面也是调用了 func Dial(network, address string) (Conn, error)去创建一个连接,这个方法里使用了net.Dialer。
6. 最终域名解析在这里 type Dialer struct {结构体里面的 Resolver *Resolver //DNS解析器
后面经过对 dial.go lookup.go 等文件源码分析,发现了一个调试技巧,
可以设置 GODEBUG 的 netdns 控制部分行为
export GODEBUG=netdns=cgo+9 # cgo 表示 使用 libc 库里面方式解析域名
export GODEBUG=netdns=go+9 # go 表示使用纯粹 的go代码库方式解析域名。centos和ubuntu的差异就是这里导致的。
测试的时候 ubuntu 用了cgo。 centos 用来 go 方式。
cgo 的情况日志输出 :
1 2 3 4 5 6 7 8 9 [root@localhost mamh]# export GODEBUG=netdns=cgo+9 [root@localhost mamh]# ./___go_build_dockerd -G mamh --debug --raw-logs go package net: confVal.netCgo = true netGo = false go package net: using cgo DNS resolver go package net: hostLookupOrder(localhost) = cgo # 这里解析 localhost 域名的调试打印输出 go package net: hostLookupOrder(index.docker.io) = cgo # 这里解析 index.docker.io 域名的调试打印输出 time="2023-07-15T22:10:10.605278120+08:00" level=debug msg="after r.client.Do(req)" res="&{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[81] Content-Type:[application/json] Date:[Sat, 15 Jul 2023 14:10:10 GMT] Strict-Transport-Security:[max-age=31536000] X-Trace-Id:[8b8f23ac18f3c788a52b462b76d0d6ec]] 0xc000f160d8 81 [] true false map[] 0xc00049c800 0xc000af0210}"
go的情况日志输出 :
1 2 3 4 5 6 7 8 9 10 11 12 [root@localhost mamh]# export GODEBUG=netdns=go+9 [root@localhost mamh]# [root@localhost mamh]# ./___go_build_dockerd -G mamh --debug --raw-logs go package net: confVal.netCgo = false netGo = true go package net: GODEBUG setting forcing use of Go's resolver go package net: hostLookupOrder(localhost) = files,dns # 这里解析 localhost 域名的调试打印输出 go package net: hostLookupOrder(index.docker.io) = files,dns # 这里解析 index.docker.io 域名的调试打印输出 time="2023-07-15T23:09:56.575716547+08:00" level=debug msg="after r.client.Do(req)" res="<nil>" time="2023-07-15T23:09:56.575826290+08:00" level=error msg="Handler for GET /v1.44/images/search returned error: Get \"https://index.docker.io/v1/search?q=x&n=25\": dial tcp: lookup index.docker.io on 192.168.1.1:53: read udp 192.168.1.102:53988->192.168.1.1:53: i/o timeout"
Golang DNS解析 https://zhuanlan.zhihu.com/p/54989059
相关源码文件 1 2 3 go1.19.1/src/net/dial.go go1.19.1/src/net/lookup.go
大致流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 net.Dial("tcp", service) d.Dial(network, address) # d是 *Dialer 类型 d.DialContext(context.Background(), network, address) d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr) r.internetAddrList(ctx, afnet, addr) # r是 r *Resolver 类型, afnet = tcp, addr=www.baidu.com:443 host, port, err = SplitHostPort(addr) # 经过 这个切分 host 和 port了 ips, err := r.lookupIPAddr(ctx, net, host) # Try as a literal IP address, then as a DNS name., 这个执行完就找到 域名对应的ip 了。 resolverFunc := r.lookupIP 这个就是 lookup_unix.go中的 lookupIP 函数
lookupIP 函数 lookup_unix.go 中的 lookupIP 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (r *Resolver) lookupIP(ctx context.Context, network, host string) (addrs []IPAddr, err error) { if r.preferGo() { # 这里是false,不会进入的 return r.goLookupIP(ctx, network, host) } order := systemConf().hostLookupOrder(r, host) # 这里选择了 hostlooupCgo 这个值, systemConf() 中 是调用初始化 initConfVal()函数的。里面可以设置 godebug.Get("netdns") if order == hostLookupCgo { if addrs, err, ok := cgoLookupIP(ctx, network, host); ok { # 直接进入这里,采用 cgoLookupIP 去解析了。。。。。。。 return addrs, err } 。。。。 }
cgoLookupIP 函数 cgo_unix.go 中的 cgoLookupIP 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func cgoLookupIP(ctx context.Context, network, name string) (addrs []IPAddr, err error, completed bool) { if ctx.Done() == nil { addrs, _, err = cgoLookupIPCNAME(network, name) return addrs, err, true } result := make(chan ipLookupResult, 1) go cgoIPLookup(result, network, name) select { case r := <-result: return r.addrs, r.err, true case <-ctx.Done(): return nil, mapErr(ctx.Err()), false } }
cgoLookupIPCNAME 函数 1 2 func cgoLookupIPCNAME(network, name string) (addrs []IPAddr, cname string, err error) {
C.getaddrinfo 函数, 这个就调用到了 c 库里面的函数啦。。。。
1 gerrno, err := C.getaddrinfo((*C.char)(unsafe.Pointer(&h[0])), nil, &hints, &res)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /* #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <unistd.h> #include <string.h> // If nothing else defined EAI_OVERFLOW, make sure it has a value. #ifndef EAI_OVERFLOW #define EAI_OVERFLOW -12 #endif */ import "C"
golang中 DNS 解析分类 大致分类如下 几个类型
参考 net/dnsclient_unix.go 文件 中定义的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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", }
上面的报错原因 整体分析下来,发现 Ubuntu 里面 用了 hostLookupCgo: "cgo",
而 centos 里面 用了 hostLookupFilesDNS: "files,dns",
具体可以参考 hostLookupOrder() 函数
Ubuntu 下 c.resolv.unknownOpt = true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 dnsReadConfig("/etc/resolv.conf") 因为这个 解析 options edns0 这一行 直接 unknownOpt 设置为 true了。 然后在 hostLookupOrder() 函数里面,直接 提前 判断 返回了 hostLookupCgo 这个类型了。 type dnsConfig struct { servers []string // server addresses (in host:port form) to use search []string // rooted suffixes to append to local name ndots int // number of dots in name to trigger absolute lookup timeout time.Duration // wait before giving up on a query, including retries attempts int // lost packets before giving up on server rotate bool // round robin among servers unknownOpt bool // anything unknown was encountered lookup []string // OpenBSD top-level database "lookup" order err error // any error that occurs during open of resolv.conf mtime time.Time // time of resolv.conf modification soffset uint32 // used by serverOffset singleRequest bool // use sequential A and AAAA queries instead of parallel queries useTCP bool // force usage of TCP for DNS resolutions }
Centos 下 case filesSource && dnsSource: 走到这里了,并且 if first == “files”
1 2 3 4 5 6 7 8 9 10 11 // Cases where Go can handle it without cgo and C thread // overhead. switch { case filesSource && dnsSource: if first == "files" { print("return hostLookupFilesDNS, ", filesSource, dnsSource, first) return hostLookupFilesDNS } else { return hostLookupDNSFiles }
总之 centos 代码逻辑 走到了 解析 /etc/nsswitch.conf 判断出来的条件的情况下了。