您的位置 首页 golang

从net库源码窥探Go程序linux平台Dns解析原理(一)

前言

从net.Dialer到net.Resolver

  • net.Dialer 结构体中有个Resolver就是负责地址时解析的
 type Dialer struct {

Timeout time.Duration

Deadline time.Time

LocalAddr Addr

DualStack bool

FallbackDelay time.Duration

KeepAlive time.Duration

// Resolver optionally specifies an alternate resolver to use.
Resolver *Resolver

Cancel <-chan struct{}

Control func(network, address string, c syscall.RawConn) error
}
  
  • 在创建连接时,Dial调用了 DialContext,在DialContext中调用d.resolver().resolveAddrList()来解析地址
 func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
    ...
// Shadow the nettrace (if any) during resolve so Connect events don't fire for DNS lookups.
resolveCtx := ctx
if trace, _ := ctx.Value(nettrace.TraceKey{}).(*nettrace.Trace); trace != nil {
shadow := *trace
shadow.ConnectStart = nil
shadow.ConnectDone = nil
resolveCtx = context.WithValue(resolveCtx, nettrace.TraceKey{}, &shadow)
}

addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr)
if err != nil {
return nil, &OpError{Op: "dial", Net: network, Source: nil, Addr: nil, Err: err}
}
    ...
  
  • 再往下看会看到r.resolveAddrList() 调用 r.internetAddrList()
 func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) {
afnet, _, err := parseNetwork(ctx, network, true)
if err != nil {
return nil, err
}
if op == "dial" && addr == "" {
return nil, errMissingAddress
}
switch afnet {
case "unix", "unixgram", "unixpacket":
addr, err := ResolveUnixAddr(afnet, addr)
if err != nil {
return nil, err
}
if op == "dial" && hint != nil && addr.Network() != hint.Network() {
return nil, &AddrError{Err: "mismatched local address type", Addr: hint.String()}
}
return addrList{addr}, nil
}
addrs, err := r.internetAddrList(ctx, afnet, addr)
if err != nil || op != "dial" || hint == nil {
return addrs, err
}
var (
tcp      *TCPAddr
udp      *UDPAddr
ip       *IPAddr
wildcard bool
)
  
  • 最后 r.internetAddrList()调用r.lookupIPAddr(ctx, net, host)解析,最后返回解析结果,也就是ip地址,来创建网络连接
 func (r *Resolver) internetAddrList(ctx context.Context, net, addr string) (addrList, error) {
var (
err        error
host, port string
portnum    int
)
...

// Try as a literal IP address, then as a DNS name.
ips, err := r.lookupIPAddr(ctx, net, host)
if err != nil {
return nil, err
}
// Issue 18806: if the machine has halfway configured
// IPv6 such that it can bind on "::" (IPv6unspecified)
// but not connect back to that same address, fall
// back to dialing 0.0.0.0.
if len(ips) == 1 && ips[0].IP.Equal(IPv6unspecified) {
ips = append(ips, IPAddr{IP: IPv4zero})
}

var filter func(IPAddr) bool
if net != "" && net[len(net)-1] == '4' {
filter = ipv4only
}
if net != "" && net[len(net)-1] == '6' {
filter = ipv6only
}
return filterAddrList(filter, ips, inetaddr, host)
}
  
  • 而其实,整个过程并没有验证这个地址是否为ip还是域名,想想这样其实是不合理的,因为不管解析是读/etc/hosts还是使用/etc/resolve.conf中的dns server去解析,整个io耗时和解析之前用逻辑判断避免相比这个代价还是很大的,所以我们继续往下看r.lookupIPAddr(ctx, net, host)
 func (r *Resolver) lookupIPAddr(ctx context.Context, network, host string) ([]IPAddr, error) {
// Make sure that no matter what we do later, host=="" is rejected.
// parseIP, for example, does accept empty strings.
if host == "" {
return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
if ip, zone := parseIPZone(host); ip != nil {
return []IPAddr{{IP: ip, Zone: zone}}, nil
}
    ...
  

发现在这里终于调用parseIPZone()解析host的ip和zone,如果为ip地址不能nil,直接返回

  • 到这里,建立网络连接之前的解析过程大概的逻辑也就看完了

Go程序在Linux 平台的解析逻辑原理

  • 先看下调用net.Resolver来解析域名

方法LookupIP、LookupHost都能用来解析域名返回ip地址数组,唯一的区别是,LookupIP可以根据你给定的ip类型返回对应类型的ip数组

 package main

import (
"context"
"fmt"
"net"
)

func main() {
resolver:= new(net.Resolver)
ctx := context.Background()
domain := "www.baidu.com"
fmt.Println(resolver.LookupIP(ctx,"ip4",domain))
fmt.Println(resolver.LookupHost(ctx,domain))
}
  
  • 先看下r.LookupHost()的逻辑

先验证参数的有效性,比如host是否为空字符串,是否为ip格式,验证通过后传入r.lookupHost(ctx, host)去解析

 func (r *Resolver) LookupHost(ctx context.Context, host string) (addrs []string, err error) {
// Make sure that no matter what we do later, host=="" is rejected.
// parseIP, for example, does accept empty strings.
if host == "" {
return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
}
if ip, _ := parseIPZone(host); ip != nil {
return []string{host}, nil
}
return r.lookupHost(ctx, host)
}
  
  • 看下r.lookupHost()的逻辑

可以看到会先判断r.preferGo是否为false和order是否为hostLookupCgo,如果条件成立,则调用cgoLookupHost(ctx, host)去解析,否则更改order为hostLookupFilesDNS

 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)
}
  
  • 先看下cgoLookupHost的逻辑

可以顺着代码往下看,会看到cgoLookupIPCNAME()调用了c核心库C.getaddrinfo()去获取域名解析结果

 func cgoLookupIPCNAME(network, name string) (addrs []IPAddr, cname string, err error) {
acquireThread()
defer releaseThread()

var hints C.struct_addrinfo
hints.ai_flags = cgoAddrInfoFlags
hints.ai_socktype = C.SOCK_STREAM
hints.ai_family = C.AF_UNSPEC
switch ipVersion(network) {
case '4':
hints.ai_family = C.AF_INET
case '6':
hints.ai_family = C.AF_INET6
}

h := make([]byte, len(name)+1)
copy(h, name)
var res *C.struct_addrinfo
gerrno, err := C.getaddrinfo((*C.char)(unsafe.Pointer(&h[0])), nil, &hints, &res)
...
}  
  • 再往下看下r.goLookupHostOrder()

会根据order值判断在本地/etc/hosts中查找,查找到自己return,查找不到再调用r.goLookupIPCNAMEOrder()

 func (r *Resolver) goLookupHostOrder(ctx context.Context, name string, order hostLookupOrder) (addrs []string, err error) {
if order == hostLookupFilesDNS || order == hostLookupFiles {
// Use entries from /etc/hosts if they match.
addrs = lookupStaticHost(name)
if len(addrs) > 0 || order == hostLookupFiles {
return
}
}
ips, _, err := r.goLookupIPCNAMEOrder(ctx, name, order)
if err != nil {
return
}
addrs = make([]string, 0, len(ips))
for _, ip := range ips {
addrs = append(addrs, ip.String())
}
return
}
  
  • 最后看下,r.goLookupIPCNAMEOrder(ctx, name, order)

再次根据order在/etc/hosts中查找一次,查找不到就从/etc/resolv.conf中读取dns server进行域名解析了

 func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, name string, order hostLookupOrder) (addrs []IPAddr, cname dnsmessage.Name, err error) {
if order == hostLookupFilesDNS || order == hostLookupFiles {
addrs = goLookupIPFiles(name)
if len(addrs) > 0 || order == hostLookupFiles {
return addrs, dnsmessage.Name{}, nil
}
}
if !isDomainName(name) {
// See comment in func lookup above about use of errNoSuchHost.
return nil, dnsmessage.Name{}, &DNSError{Err: errNoSuchHost.Error(), Name: name, IsNotFound: true}
}
resolvConf.tryUpdate("/etc/resolv.conf")
resolvConf.mu.RLock()
conf := resolvConf.dnsConfig
resolvConf.mu.RUnlock()
    ...
}
  
  • 到这里linux平台整个域名解析逻辑大概也就清楚了,官方关于这个过程也给出来大概的解析

The method for resolving domain names, whether indirectly with functions like Dial or directly with functions like LookupHost and LookupAddr, varies by operating system. On Unix systems, the resolver has two options for resolving names. It can use a pure Go resolver that sends DNS requests directly to the servers listed in /etc/resolv.conf, or it can use a cgo-based resolver that calls C library routines such as getaddrinfo and getnameinfo. By default the pure Go resolver is used, because a blocked DNS request consumes only a goroutine, while a blocked C call consumes an operating system thread. When cgo is available, the cgo-based resolver is used instead under a variety of conditions: on systems that do not let programs make direct DNS requests (OS X), when the LOCALDOMAIN environment variable is present (even if empty), when the RES_OPTIONS or HOSTALIASES environment variable is non-empty, when the ASR_CONFIG environment variable is non-empty (OpenBSD only), when /etc/resolv.conf or /etc/nsswitch.conf specify the use of features that the Go resolver does not implement, and when the name being looked up ends in .local or is an mDNS name.

强制使用cgo进行域名解析

只需要在运行的时候添加 export GODEBUG=netdns=cgo即可

 export GODEBUG=netdns=go    # force pure Go resolver
export GODEBUG=netdns=cgo   # force cgo resolver

  

最后

建议还是不要使用cgo的方式来进行域名解析,虽然这样可以利用nscd这样的主机缓存进程加快dns解析,原因有二,1.cgo不是go,go gc并不对cgo起作用,而且cgo的线程也不受golang调度器控制,2. nscd 不靠谱,这是业界公认的 。

那么,如果想解决文章开头域名解析耗时问题,我们该怎么办呢?请关注,转发,点赞,下篇文章会从实战的角度来解决golang dns解析慢的问题

文章来源:智云一二三科技

文章标题:从net库源码窥探Go程序linux平台Dns解析原理(一)

文章地址:https://www.zhihuclub.com/99887.shtml

关于作者: 智云科技

热门文章

网站地图