前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang 源码分析(21)dns

golang 源码分析(21)dns

作者头像
golangLeetcode
发布2022-08-02 16:38:10
1.3K0
发布2022-08-02 16:38:10
举报
文章被收录于专栏:golang算法架构leetcode技术php

go语言的官方文档 https://golang.org/pkg/net/ 域名解析: 域名解析函数,Dial函数会间接调用到,而LokupHost和LookupAddr则会直接调用域名解析函数,不同 的操作系统实现不同, 在Unix系统中有两种方法进行域名解析:

1)纯GO语言实现的域名解析,从/etc/resolv.conf中取出本地dns server地址列表, 发送DNS请求(UDP 报文)并获得结果 2) 使用cgo方式, 最终会调用到c标准库的getaddrinfo或getnameinfo函数(不建议使用对GO协程不友好)

关于 cgo dns 解析的坑 参照以下链接 https://jira.mongodb.org/browse/MGO-41 https://github.com/golang/go/issues/8602#issuecomment-66098142

GO语言默认使用纯GO的域名解析,因为这样一个阻塞的DNS请求只会消耗一个协程, 使 用cgo的方式 则会阻塞一个系统线程, 只有某些特定条件下才会使用系统提供的cgo方式, 例如: 1) 在OS X系统中不允许程序 直接发送DNS请求; 2) LOCALDOMAINH环境变量存在,即使为空; 3) ES_OPTIONS或HOSTALIASES或ASR _CONFIG环境变量非空; 4)/etc/resolv.conf或/etc/nsswitch.conf指定的使用方式GO解析器没有实现; 5) 当要解析的域名以.local结束, 或者是一个mDNS域名

可以通过GODEBUG环境变量来设置go语言的默认DNS解析方式 纯go或cgo,

代码语言:javascript
复制
export GODEBUG=netdns=go    # force pure Go resolver 纯go 方式
export GODEBUG=netdns=cgo   # force cgo resolver   cgo 方式

也可以在编译时指定netgo或netcgo的编译tag来设置 在plan 9中 域名解析只能通过 /net/cs和 /net/dns 在windows中 域名解析只能通过windows提供的C标准库函数GetAddrInfo或DnsQuery

OK 官方说明看完了, 我们写一个例子试一下

代码语言:javascript
复制
package main

import (
	"net"
	"fmt"
	"os"
)

func main() {
	ns, err := net.LookupHost("www.baidu.com")
	if err != nil {
		fmt.Fprintf(os.Stderr, "Err: %s", err.Error())
		return
	}

	for _, n := range ns {
		fmt.Fprintf(os.Stdout, "--%s\n", n)
	}
}

使用strace命令分析一下, 系统调用过程:

代码语言:javascript
复制
openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 3
read(3, "127.0.0.1   localhost localhost."..., 4096) = 158
read(3, "", 3938)                       = 0
read(3, "", 4096)                       = 0
close(3)                                = 0

//读取本地dns server 配置
stat("/etc/resolv.conf", {st_mode=S_IFREG|0644, st_size=104, ...}) = 0


//创建UDP socket 发送准备发送DNS请求
socket(PF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("172.16.1.3")}, 16) = 0
epoll_create1(EPOLL_CLOEXEC)            = 4
// 将UDP socket 加入到epoll中
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2130514816, u64=140679489328000}}) = 0
getsockname(3, {sa_family=AF_INET, sin_port=htons(57587), sin_addr=inet_addr("10.0.2.15")}, [16]) = 0
getpeername(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("172.16.1.3")}, [16]) = 0

//发送DNS请求
write(3, "\363}\1\0\0\1\0\0\0\0\0\0\3www\5baidu\3com\0\0\34\0\1", 31) = 31
futex(0x645c10, FUTEX_WAIT, 0, NULL)    = 0

read(3, 0xc82007c000, 512)              = -1 EAGAIN (Resource temporarily unavailable)
epoll_wait(4, {{EPOLLOUT, {u32=2130514816, u64=140679489328000}}, {EPOLLOUT, {u32=2130514624, u64=140679489327808}}}, 128, 0) = 2
// epoll等待socket 事件
epoll_wait(4, {{EPOLLIN|EPOLLOUT, {u32=2130514624, u64=140679489327808}}}, 128, -1) = 1
futex(0x645680, FUTEX_WAKE, 1)          = 1
read(5, "g\217\201\200\0\1\0\2\0\r\0\v\3www\5baidu\3com\0\0\1\0\1\300"..., 512) = 474
epoll_ctl(4, EPOLL_CTL_DEL, 5, {0, {u32=0, u64=0}}) = 0
close(5)                                = 0
epoll_wait(4, {}, 128, 0)               = 0
epoll_wait(4, {{EPOLLIN|EPOLLOUT, {u32=2130514816, u64=140679489328000}}}, 128, -1) = 1
futex(0x645680, FUTEX_WAKE, 1)          = 1


// 得到DNS解析结果
read(3, "\363}\201\200\0\1\0\1\0\1\0\0\3www\5baidu\3com\0\0\34\0\1\300"..., 512) = 115
epoll_ctl(4, EPOLL_CTL_DEL, 3, {0, {u32=0, u64=0}}) = 0
close(3)                                = 0

go dns 解析 源码在go/src/net/dnsclient_unix.go, lookupHost()通过向本地dns server发送请求,获得IP和域名的

对应关系然后返回,函数调用关系如下:

lookupHost() ->goLookupHostOrder() -->goLookupIPOrder() --->tryOneName() ---->exchange()

代码语言:javascript
复制
func exchange(server, name string, qtype uint16, timeout time.Duration) (*dnsMsg, error) {
	d := Dialer{Timeout: timeout}
	out := dnsMsg{
		dnsMsgHdr: dnsMsgHdr{
			recursion_desired: true,
		},
		question: []dnsQuestion{
			{name, qtype, dnsClassINET},
		},
	}
	for _, network := range []string{"udp", "tcp"} {
		c, err := d.dialDNS(network, server)    //创建UDP
		if err != nil {
			return nil, err
		}
		defer c.Close()
		if timeout > 0 {
			c.SetDeadline(time.Now().Add(timeout))
		}
		out.id = uint16(rand.Int()) ^ uint16(time.Now().UnixNano())
		if err := c.writeDNSQuery(&out); err != nil {   //发送DNS请求
			return nil, err
		}
		in, err := c.readDNSResponse()   //解析DNS请求得到IP
		if err != nil {
			return nil, err
		}
		if in.id != out.id {
			return nil, errors.New("DNS message ID mismatch")
		}
		if in.truncated { // see RFC 5966
			continue
		}
		return in, nil
	}
	return nil, errors.New("no answer from DNS server")
}

其中的timeout 是 dns 超时时间 是在dnsconfig_unix.go 文件中读取 /etc/reslove.conf 的配置决定的

net.go中的DialTimeout函数也会走到DNS解析流程中,该函数最终会调用到 lookupIPDeadline 启用一个新的协

程去解析DNS, 具体调用栈如下:

DialTimeout() ->resolveAddrList() -->internetAddrList() --->lookupIPDeadline() ---->lookupGroup.DoChan() 在新的协程中去做 dns解析 ----->lookupIP() ------>goLookupIPOrder()

golang 1.11 版本。如果/etc/resolv.conf 文件的nameserver有不可达的地址。那么使用go实现的dns解析将会非常耗时。耗时取决于resolv.conf文件options选项attempts * timeout。默认10秒。

其他版本,我实验了go1.11.1、go1.11.2、go1.9.7。如果/etc/resolv.conf 文件的nameserver有不可达的地址,且设置了options rotate,go实现的dns解析耗费timeout秒,默认5。

其他版本。如果/etc/resolv.conf 文件的nameserver有不可达的地址,没有设置options rotate,go实现与cgo实现耗时相同,取决于nameserver不可达的地址的位置,如果第一位会耗费timeout秒,默认5.

如果你能修改运行主机配置(服务端),那当然是直接修改/etc/resolv.conf文件了。

如果你无权修改运行主机(比如客户端程序),需要在编译时使用-tags ‘netcgo’ 强制go使用cgo方式做dns解析。虽然不能根本解决问题,但至少能表现的和其他工具一样的结果。不要被别人喷你写的东西屎。

使用高版本go。我实验go1.11.1/1.11.2基本解决,但还有问题。

域名解析函数,Dial函数会间接调用到,而LokupHost和LookupAddr则会直接调用域名解析函数,不同的操作系统实现不同, 在Unix系统中有两种方法进行域名解析:

1. 纯GO语言实现的域名解析,从/etc/resolv.conf中取出本地dns server地址列表, 发送DNS请求(UDP报文)并获得结果

2. 使用cgo方式, 最终会调用到c标准库的getaddrinfo或getnameinfo函数

GO语言默认使用纯GO的域名解析,因为这样一个阻塞的DNS请求只会消耗一个协程, 使 用cgo的

方式则会阻塞一个系统线程, 只有某些特定条件下才会使用系统提供的cgo方式, 例如:

1) 在OS X系统中不允许程序直接发送DNS请求;

2) LOCALDOMAINH环境变量存在,即使为空;

3) ES_OPTIONS或HOSTALIASES或ASR_CONFIG环境变量非空;

4) /etc/resolv.conf或/etc/nsswitch.conf指定的使用方式GO解析器没有实现;

5) 当要解析的域名以.local结束, 或者是一个mDNS域名。

可以通过GODEBUG环境变量来设置go语言的默认DNS解析方式 纯go或cgo,

> export GODEBUG=netdns=go # force pure Go resolver 纯go 方式

> export GODEBUG=netdns=cgo # force cgo resolver cgo 方式

也可以在编译时指定netgo或netcgo的编译tag来设置

在plan 9中 域名解析只能通过 /net/cs和 /net/dns

在windows中 域名解析只能通过windows提供的C标准库函数GetAddrInfo或DnsQuery

正是由于这种差异化,造成了curl与go实现程序表现出了不同的结果。

go到底如何实现的?

列出关键代码 net/dnsclient_unix.go :

func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, name string, order hostLookupOrder) (addrs []IPAddr, cname dnsmessage.Name, err error) {

// 读取配置

resolvConf.tryUpdate("/etc/resolv.conf")

conf := resolvConf.dnsConfig

// 同时查询A记录(ipv4),AAAA记录(ipv6)

qtypes := [...]dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}

// 默认conf.nameList(name)会返回两个 xxxx.alicdn.com. 和 xxxx.alicdn.com.bja

for _, fqdn := range conf.nameList(name) {

for _, qtype := range qtypes {

go func(qtype dnsmessage.Type) {

// 起两个goroutine执行dns请求

p, server, err := r.tryOneName(ctx, conf, fqdn, qtype)

lane <- racer{p, server, err}

}(qtype)

}

// 要等到A记录(ipv4),AAAA记录(ipv6)都有结果才结束循环。

for range qtypes {

racer := <-lane

}

}

}

func (r *Resolver) tryOneName(ctx context.Context, cfg *dnsConfig, name string, qtype dnsmessage.Type) (dnsmessage.Parser, string, error) {

// 根据/etc/resolv.conf中options选项rotate计算

serverOffset := cfg.serverOffset()

sLen := uint32(len(cfg.servers))

// 超时时间,重试次数对应与/etc/resolv.conf中options的timeout默认为2, attempts默认为5(秒)

for i := 0; i < cfg.attempts; i++ {

for j := uint32(0); j < sLen; j++ {

// 获取nameserver

server := cfg.servers[(serverOffset+j)%sLen]

// 发起dns请求

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

// 1.11版本 (之前版本不确定)

// 如果没有查询到该记录的结果(errNoSuchHost),重试

// 1.11.1 之后

// 如果没有查询到该记录的结果(errNoSuchHost),返回

// 比1.11版本优化了无效重试

}

}

}

// serverOffset returns an offset that can be used to determine

// indices of servers in c.servers when making queries.

// When the rotate option is enabled, this offset increases.

// Otherwise it is always 0.

func (c *dnsConfig) serverOffset() uint32 {

// 如果/etc/resolv.conf中options的rotate被设置,开始轮训

if c.rotate {

return atomic.AddUint32(&c.soffset, 1) - 1 // return 0 to start

}

return 0

}

总结

从源码中可以看到,

go实现会同时进行A记录(ipv4),AAAA记录(ipv6) 的dns请求。

go实现对于/etc/resolv.conf文件的解析,轮训方式与glibc可能不同的。

如果你有两个nameserver,且设置了options rotate,如果你nameserver中有一个是坏的,那么go实现,肯定会轮到这个坏的,因为会多请求一个AAAA记录,而两个请求用的dnsConfig.soffset是同一变量。

对于3情况,1.11版本 情况更恶劣。如果dns服务器没有你请求域名的AAAA记录,会阻塞timeout*attempts秒,因为造成errNoSuchHost错误,会进行重试,跑满attempts循环条件。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档