在线上部署的一个程序,在某天突然在一天内出现多次 “dial tcp: lookup xxxx.com on 223.x.x.x:53: read udp 180.x.x.x:7792->223.x.x.x:53: i/o timeout” 的问题,导致线上告警触发了多次。后面查找问题,发现 go 每次发起 http 请求都会发起一个 dns 请求来进行域名解析,而我们服务器的dns服务器可能由于网络抖动问题,请求超时,最终导致请求失败触发了告警服务。
然后在网上进行了发现在 github 上有别人写的一个 dns cache程序,该库github 地址。https://github.com/viki-org/dnscache/blob/master/dnscache.go。
下面对该程序进行代码解析
(1)存储的结构体。
type Resolver struct {
lock sync.RWMutex // 使用sync的读写锁来进行控制
cache map[string][]net.IP // 使用map 来进行存储 ,一个域名可以存储多个ip地址。
}
(2)新建一个 dns cache 对象
// 传入一个时延,表示ip的有效时间,过了这个时间则重新进行ip解析
func New(refreshRate time.Duration) *Resolver {
resolver := &Resolver { // 对该结构体进行初始化,
cache: make(map[string][]net.IP, 64),
}
if refreshRate > 0 {
// 使用一个协程来进行更新ip
go resolver.autoRefresh(refreshRate)
}
return resolver
}
(3) Fetch(string) 函数
// 传入一个域名,返回ip数组
func (r *Resolver) Fetch(address string) ([]net.IP, error) {
r.lock.RLock()
ips, exists := r.cache[address] // 先查看缓存中是否有该域名的ip,
r.lock.RUnlock()
if exists { return ips, nil }
return r.Lookup(address) // 进行域名的解析并存入到map数组中
}
(4)FetchOne(string) 获取一个域名对应的ip
func (r *Resolver) FetchOne(address string) (net.IP, error) {
// 调用 Fetch(string) 函数
ips, err := r.Fetch(address)
if err != nil || len(ips) == 0 { return nil, err}
return ips[0], nil
}
(5) FetchOneString(string) 返回一个 ip字符串
// 根据域名并返回一个 ip字符串
func (r *Resolver) FetchOneString(address string) (string, error) {
ip, err := r.FetchOne(address)
if err != nil || ip == nil { return "", err }
return ip.String(), nil
}
(6)Refresh() 刷新域名ip
unc (r *Resolver) Refresh() {
i := 0
r.lock.RLock()
addresses := make([]string, len(r.cache))
for key, _ := range r.cache { // 把cache里的域名赋值到一个[]string中
addresses[i] = key
i++
}
r.lock.RUnlock()
// 为什么先赋值到一个中间[]string中呢?因为域名解析需要时间,如果直接操作
// for key, _ := range r.cache {
// r.Lookup(key)
// 则会锁住 r.cache 很长时间,不利于并发操作
//}
for _, address := range addresses {
r.Lookup(address) // 循环重新解析cache中的域名
time.Sleep(time.Second * 2)
}
}
(7)Loopup(string) 重新解析域名
func (r *Resolver) Lookup(address string) ([]net.IP, error) {
// 先解析域名,再赋值给 cache,减少锁的粒度,提高并发
ips, err := net.LookupIP(address)
if err != nil { return nil, err }
r.lock.Lock()
r.cache[address] = ips
r.lock.Unlock()
return ips, nil
}
(8) autoRefresh(time.Duration) 定时刷新域名
func (r *Resolver) autoRefresh(rate time.Duration) {
for {
// 独立的协程来运行该函数,每隔 rate 时间进行一次域名的刷新
time.Sleep(rate)
r.Refresh()
}
}
使用了 dnschache.go 封装的dns cache,在go 语言层上进行了cache缓存,避免每次请求都进行dns解析。而一般来说 linux 服务器本地的 dns服务器也会进行 dns 缓存,避免每次都去远程的 dns 服务器请求。这样在两个层面上都进行了 dns 缓存,从而减少了 dns 查询失败导致的请求失败的概率。