找回密码
 会员注册
查看: 20|回复: 0

精心设计的DNSFailover策略在Go中竟然带来了反效果,发生了什么?

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
70981
发表于 2024-10-9 02:35:14 | 显示全部楼层 |阅读模式
本期作者卫智雄哔哩哔哩高级运维工程师一. 背景如下配置所示,我们在 /etc/resolv.conf 中配置了两个 nameserver,其中 server2 在灾备机房 ,作为一种 failover 策略。nameserver server1nameserver server2options timeout:1 attempts:1我们的预期是如果 server1 服务正常,则所有的 DNS 请求应该由 server1 处理,且 server2 故障不应对业务有任何影响 。只有当 server1 服务异常,DNS 请求才应该重试到 server2。然而我们在线上观察到一直有 AAAA 类型的 DNS 请求发送到 server2,而且如果 client 到 server2 的网络异常时,业务的 http 请求耗时会增加 1s,这并不符合预期。同时因为我们的内网域名都没有 AAAA 记录,且内网服务器也是关闭了 IPv6 协议的,AAAA 请求也不符合预期。二. 问题排查经过和业务同学求证,相关程序语言为 Go ,请求使用的是 Go 原生 net 库。在 Go net 库中,最经常使用的方式如下:package main import ( ? ?"net" ? ?"net/http") func main() { ? ?http.Get("https://internal.domain.name") ? ?net.Dial("tcp", "internal.domain.name:443")}1. 梳理源码让我们顺着源码分析 net 库的解析逻辑。无论是 http.Get 还是 net.Dial 最终都会到 func (d *Dialer) DialContext() 这个方法。然后层层调用到 func (r *Resolver) lookupIP() 方法,这里定义了何时使用 Go 内置解析器或调用操作系统 C lib 库提供的解析方法,以及?/etc/hosts?的优先级。同时补充一个比较重要的信息:windows?、darwin(MacOS等)优先使用 C lib 库解析,debug 时需要注意。func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) { ? ?... ? ?addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr) ? ?...} func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) { ? ?... ? ?addrs, err := r.internetAddrList(ctx, afnet, addr) ? ?...} func (r *Resolver) internetAddrList(ctx context.Context, net, addr string) (addrList, error) { ? ?... ? ?ips, err := r.lookupIPAddr(ctx, net, host) ? ?...} func (r *Resolver) lookupIPAddr(ctx context.Context, network, host string) ([]IPAddr, error) { ? ?... ? ?resolverFunc := r.lookupIP ? ?... ? ?ch := r.getLookupGroup().DoChan(lookupKey, func() (any, error) { ? ? ? ?return testHookLookupIP(lookupGroupCtx, resolverFunc, network, host) ? ?}) ? ?...} func (r *Resolver) lookupIP(ctx context.Context, network, host string) (addrs []IPAddr, err error) { ? ?if r.preferGo() { ? ? ? ?return r.goLookupIP(ctx, network, host) ? ?} ? ?order, conf := systemConf().hostLookupOrder(r, host) ? ?if order == hostLookupCgo { ? ? ? ?return cgoLookupIP(ctx, network, host) ? ?} ? ?ips, _, err := r.goLookupIPCNAMEOrder(ctx, network, host, order, conf) ? ?return ips, err}我们线上的操作系统是 Debain,确认会使用 Go 内置解析器。所以下一步来到了?func (r *Resolver)?goLookupIPCNAMEOrder()?方法。这里我们可以通过 qtypes 看到如果?net.Dial?的?network?参数传入的是?tcp?,域名的 A 和 AAAA 记录都会被查询,无论服务器是否关闭 ipv6。func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, network, name string, order hostLookupOrder, conf *dnsConfig) (addrs []IPAddr, cname dnsmessage.Name, err error) { ? ?... ? ?lane := make(chan result, 1) ? ?qtypes := []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA} ? ?switch ipVersion(network) { ? ?case '4': ? ? ? ?qtypes = []dnsmessage.Type{dnsmessage.TypeA} ? ?case '6': ? ? ? ?qtypes = []dnsmessage.Type{dnsmessage.TypeAAAA} ? ?} ? ?var queryFn func(fqdn string, qtype dnsmessage.Type) ? ?var responseFn func(fqdn string, qtype dnsmessage.Type) result ? ?if conf.singleRequest { ? ? ? ?queryFn = func(fqdn string, qtype dnsmessage.Type) {} ? ? ? ?responseFn = func(fqdn string, qtype dnsmessage.Type) result { ? ? ? ? ? ?dnsWaitGroup.Add(1) ? ? ? ? ? ?defer dnsWaitGroup.Done() ? ? ? ? ? ?p, server, err := r.tryOneName(ctx, conf, fqdn, qtype) ? ? ? ? ? ?return result{p, server, err} ? ? ? ?} ? ?} else { ? ? ? ?queryFn = func(fqdn string, qtype dnsmessage.Type) { ? ? ? ? ? ?dnsWaitGroup.Add(1) ? ? ? ? ? ?go func(qtype dnsmessage.Type) { ? ? ? ? ? ? ? ?p, server, err := r.tryOneName(ctx, conf, fqdn, qtype) ? ? ? ? ? ? ? ?lane rcode == NOERROR & anhp->ancount == 0 ? ?& anhp->aa == 0 & anhp->ra == 0 & anhp->arcount == 0) { ? ?goto next_ns;}三. 优化经过上面的排查,我们已经确认了 AAAA 请求的源头,以及为什么会重试到下一个 server。接下来可以针对性的优化。1.? 对于 Go 程序中 AAAA 请求重试到下一个 server 的优化方案:? a. 代价相对较小的方案,程序构建时添加?-tags 'netcgo' 编译参数,指定使用 cgo-based 解析器。? b. DNS Server proxy 层支持递归请求。这里有必要说明递归支持不能在 proxy 层简单的直接开启,proxy 和 recursion 在逻辑上有冲突的地方,务必做好必要的验证和确认,否则可能会带来新的问题。2. 如果业务程序不需要支持 IPv6 网络,可以通过指定网络类型为 IPv4,来消除 AAAA 请求,同时避免随之带来的问题。(也顺带减少了相关开销)? ?a.?net.Dial?相关方法可以指定?network?为?tcp4、udp4?来强制使用 IPv4net.Dial("tcp4", "internal.domain.name:443")net.Dial("udp4", "internal.domain.name:443")? b.?net/http?相关方法可以通过如下示例来强制使用 IPv4package main import ( ? ?"context" ? ?"log" ? ?"net" ? ?"net/http" ? ?"time") func main() { ? ?dialer := &net.Dialer{ ? ? ? ?Timeout: ? 30 * time.Second, ? ? ? ?KeepAlive: 30 * time.Second, ? ?} ? ?transport := http.DefaultTransport.(*http.Transport).Clone() ? ?transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { ? ? ? ?return dialer.DialContext(ctx, "tcp4", addr) ? ?} ? ?httpClient := &http.Client{ ? ? ? ?Timeout: 30 * time.Second, ? ?} ? ?httpClient.Transport = transport ? ?resp, err := httpClient.Get("https://internal.domain.name") ? ?if err != nil { ? ? ? ?log.Fatal(err) ? ?} ? ?log.Println(resp.StatusCode)}四. 总结Go net 库中提供了两种解析逻辑:自实现的内置解析器和系统提供的解析函数。windows?、darwin(MacOS等)优先使用系统提供的解析函数,常见的 Debain、Centos 等优先使用内置解析器。Go net 库中的内置解析器和系统提供的解析函数行为和结果并不完全一致,它可能会影响到我们的服务。业务应设置合理的超时时间,不易过短,以确保基础设施的 failover 策略有足够的响应时间。推荐阅读:https://studygolang.com/topics/15021https://pkg.go.dev/net?中的 Name Resolution 章节开发者问答你还遇到过哪些域名解析相关的故障?欢迎在留言区告诉我们。转发并留言,小编将选取1则最有价值的评论,送出哔哩哔哩双层六芒星玻璃杯1个(见下图)。2月23日中午12点开奖。如果喜欢本期内容的话,欢迎点个“在看”吧!往期精彩指路全链路压测改造之全链自动化测试实践哔哩哔哩?数据建设之路—实时DQC篇Apache Kyuubi 在B站大数据场景下的应用实践通用工程丨大前端丨业务线大数据丨AI丨多媒体
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-9 02:38 , Processed in 0.590583 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表