golang中的dns问题
问题描述
一个golang写的客户端程序,向云端发起一个http 请求,报错:
Get http://XXXXXXX: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
使用curl请求正常返回。
问题定位
光从错误提示上看,没有任何关于DNS错误的提示。在我一顿分析http请求有没有问题后,抓包发现,根本就没有tcp请求发出去。在/etc/hosts内写死域名地址,程序运行正常。于是定位应该是dns解析有问题。
环境描述
运行环境
[root@localhost ~]# cat /etc/redhat-release
CentOS Linux release 7.2.1511 (Core)
[root@localhost ~]# cat /etc/resolv.conf
# Generated by NetworkManager
nameserver A
nameserver B
编译环境
go version go1.11 linux/amd64
我们先看结论吧,具体分析过程放到最下面,方便遇到相同问题的小伙伴。有兴趣的可以看下面分析过程。
问题结论
- 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.
版本 | rotate | nameserver位置 | 耗时(秒) |
---|---|---|---|
1.11 | - | - | attempts * timeout |
其他 | 是 | - | 1 * timeout |
其他 | 否 | 不可达在第一位 | 1 * timeout |
其他 | 否 | 不可达在第二位 | - |
[root@localhost ~]$ time GODEBUG=netdns=go ./dns -h xxx.alicdn.com
addrs: [x.x.x.2 x.x.x.235 x.x.x.238 x.x.x.237 x.x.x.7 x.x.x.233 x.x.x.234 x.x.x.236 x.x.x.6 x.x.x.239]real 0m10.048s
user 0m0.002s
sys 0m0.011s[root@localhost ~]$ time GODEBUG=netdns=cgo ./dns -h xxx.alicdn.com
addrs: [x.x.x.237 x.x.x.7 x.x.x.233 x.x.x.234 x.x.x.236 x.x.x.6 x.x.x.239 x.x.x.2 x.x.x.235 x.x.x.238]real 0m0.031s
user 0m0.005s
sys 0m0.005s
解决方案
- 如果你能修改运行主机配置(服务端),那当然是直接修改/etc/resolv.conf文件了。
- 如果你无权修改运行主机(比如客户端程序),需要在编译时使用-tags ‘netcgo’ 强制go使用cgo方式做dns解析。虽然不能根本解决问题,但至少能表现的和其他工具一样的结果。不要被别人喷你写的东西屎。
- 使用高版本go。我实验go1.11.1/1.11.2基本解决,但还有问题。github源码对比:net: fail fast for DNS rcode success with no answers of requested type
问题分析
既然 问题定位 已经确认了是DNS问题,那么自然要看/etc/resolv.conf文件了,结果发现,nameserver B无法ping通。
那同样的主机配置为何curl请求没有问题呢?
官方文档 有对golang DNS的说明,这篇文章对其进行了翻译go (golang) DNS域名解析实现 :
域名解析函数,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.bjafor _, 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++ {// 获取nameserverserver := 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循环条件。
参考
resolv.conf man-page
go程序中dns解析无法使用所有域名服务器
DNS Resolution in Go and Cgo
源码
package mainimport ("flag""fmt""net"
)
var arg = flag.String("h", "localhost", "domain")
func main() {flag.Parse()ips, err := net.LookupIP(*arg)if err != nil {fmt.Printf("lookup ip error: %s\n", err)} else {fmt.Printf("addrs: %v\n", ips)}
}
#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>int main(int argc, char **argv)
{char *ptr, **pptr;char str[INET_ADDRSTRLEN];struct hostent *hptr;while (--argc > 0) {ptr = *++argv;if ( (hptr = gethostbyname(ptr)) == NULL) {printf("gethostbyname error for host: %s: %s\n",ptr, hstrerror(h_errno));continue;}printf("official hostname: %s\n", hptr->h_name);for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)printf("\talias: %s\n", *pptr);switch (hptr->h_addrtype) {case AF_INET:pptr = hptr->h_addr_list;for ( ; *pptr != NULL; pptr++)printf("\taddress: %s\n",inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));break;default:printf("unknown address type\n");break;}}exit(0);
}
golang中的dns问题相关推荐
- linux mint 修改dns,如何在Ubuntu和LinuxMint中刷新DNS缓存
本篇文章给大家介绍的内容是关于如何在Ubuntu和LinuxMint中刷新DNS缓存,下面我们来看具体的内容. 域名系统(DNS)是互联网的主干网之一.你无法想象在全球数百万用户的公共网络上运行网站. ...
- Golang中Buffer高效拼接字符串以及自定义线程安全Buffer
本文原创文章,转载注明出处,博客地址 https://segmentfault.com/u/to... 第一时间看后续精彩文章.觉得好的话,顺手分享到朋友圈吧,感谢支持. Go中可以使用"+ ...
- 如何在golang中关闭bufio.reader_Golang 并发模型系列:1. 轻松入门流水线模型
Go语言中文网,致力于每日分享编码.开源等知识,欢迎关注我,会有意想不到的收获! Golang作为一个实用主义的编程语言,非常注重性能,在语言特性上天然支持并发,它有多种并发模型,通过流水线模型系列文 ...
- go语言的iota是什么意思_关于Golang中的iota
快速一览 iota是Golang中提供的一个简化常量和枚举编程的标识符,合理的使用这个标识符可以让代码变得更简洁,省去大量的不必要的代码. 比如下面的这个常量定义 const ( a = 1 b = ...
- Golang中的panic和recover(捕获异常)
Golang中的panic和recover(捕获异常) 参考文章: (1)Golang中的panic和recover(捕获异常) (2)https://www.cnblogs.com/zhzhlong ...
- golang 中string和int类型相互转换
总结了golang中字符串和各种int类型之间的相互转换方式: string转成int: test_int, err := strconv.Atoi(test_string) if err != ni ...
- golang中并发sync和channel
golang中并发sync和channel chenbaoke · 2014-12-08 13:00:01 · 19151 次点击 · 预计阅读时间 5 分钟 · 不到1分钟之前 开始浏览 这是一个创 ...
- golang中的sync.WaitGroup
golang中的sync.WaitGroup Posted on 2015/04/09刚才看golang的sync的包,看见一个很有用的功能.就是WaitGroup. 先说说WaitGroup的用途: ...
- 初步解读Golang中的接口相关编写方法
初步解读Golang中的接口相关编写方法 概述如果说goroutine和channel是Go并发的两大基石,那么接口是Go语言编程中数据类型的关键.在Go语言的实际编程中,几乎所有的数据结构都围绕接口 ...
- golang中utf8和汉字互转
golang中utf8和汉字互转 package mainimport ("fmt""strconv""strings" )func mai ...
最新文章
- redis主从复制、高可用和集群
- Dtree【树形下拉框】
- 缓存之EHCache
- codeforces1438 E.Yurii Can Do Everything
- Javascript 自定义输出
- Linux系统编程25:基础IO之亲自实现一个动静态库
- vitamio 缓冲一部分时,loading还没消失,直接点击播放,loading未能消失
- 大数据技术包含哪些结构层次
- h5算命php源码,H5付费算命PHP源码那么火_付费算命源码如何下载
- 【操作系统】进程间通信 — 消息队列
- Oracle重建索引
- UE4_Stereo Panoramic Movie Capture_合并左右眼为一张图片
- [css文字单行省略与多行省略]
- 五种提前还款方式那种更划算
- 通信感知一体化技术思考
- XeLaTeX+xeCJK中文字体设置
- 莲香·沙罗花飞逝的梦境
- JPA之EntityManager踩坑笔记:更改PersistenceContext
- java 图片格式转化 wmf,emf -> svg,png
- REST API 最佳实践 – REST 端点设计示例
热门文章
- I03 403-(Python+mysql) 飞机票销售系统
- representation learning的理解
- lol服务器不稳定领皮肤,LOL免费领“源代码娜美”皮肤 官方致歉游戏卡顿问题...
- hotmail手机端_Hotmail邮箱客户端下载-Hotmail手机版下载 v2.48.0-pc6下载
- WIN7+LR11+IE8无法打开的问题解决方法
- 站内信通知数据表设计
- 程序员最喜欢的五大神器
- 【PBR系列五】镜面反射BRDF模型(Specular BRDF)及实现效果
- 在 Linux 中安装 Firefox
- 软件测试面试智商题,程序员面试时遇智商测试题:15分钟内答对这3题,你的IQ可以碾压大多数人!...