首頁>技術>

前言

某些架構中,業務系統服務或者微服務之間透過域名進行通訊,這樣就會導致在系統呼叫過程中建立網路連線之前先去解析一下域名,拿到域名對應的ip地址再建立連線,併發量小的情況下,這樣的解析對dns域名伺服器沒有太大壓力,但是當併發量激增的時候,每次建立網路連線都要去遠端伺服器去解析一下域名地址,那耗費在網路io上的時間足以讓你的系統併發降低幾個維度。本篇文章從原始碼入手,剖析原始碼中linux平臺dns解析原理,讓你更清晰地看到go語言中dns解析的邏輯實現。

從net.Dialer到net.Resolvernet.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 mainimport (	"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)}
再往下看下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 resolverexport GODEBUG=netdns=cgo   # force cgo resolver
最後

建議還是不要使用cgo的方式來進行域名解析,雖然這樣可以利用nscd這樣的主機快取程序加快dns解析,原因有二,1.cgo不是go,go gc並不對cgo起作用,而且cgo的執行緒也不受golang排程器控制,2. nscd 不靠譜,這是業界公認的 。

6
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 大資料開發基礎之JAVA三大特性