阅读 148

记一次Go net库DNS问题排查

本文作者: 刘浩

协议声明: 知识共享署名 4.0 国际许可协议 转载请注明出处.

背景

那是一个风和日丽的早晨,我像往常一样背着炸药包来到了公司。最近一段时间一直在维护公司的基础推送服务。由于推送服务是异步的通常在业务方调用接口过后无法及时的感知到错误日志以及发送状态,通过日志查询又存在一定的延迟。于是针对这个问题专门做了一个调试工具 pushuppushup 可以准确的感知发送信息几乎0延迟,并对错误日志做出一定的排查建议,提升接入效率。大概长这个样子:

image.png

然后某业务线客户端同学在自行接入过程中反馈华为的推送消息存在间歇性推送消息丢失问题。我心中先是一愣,转念一想肯定是你使用有问题, 华为的推送不仅限制少而且还比较稳定。不过还是以严谨的态度准备查一查,然后我一个箭步以迅雷不及掩耳盗铃之势自己尝试了进行了一下,发现确实存在丢失问题[狗头] [狗头][狗头]

Trace 定位

推送服务涉及的服务模块较多,由于这个问题比较容易复现便使用故障的Trace ID进行了查询,问题原因如下图所示:

image.png

小结

看起来故障的原因是Go服务中DNS 解析超时了。

在机器上测试一下

本着发现问题首先确认是不是自己的问题的原则。DNS 解析超时了,最简单直接的方法是测试一下DNS解析是否是正常。于是进行了下面两个步骤的测试。

dig

dig 是一个常用的DNS解析测试工具,相信大家都非常熟悉。但是测试结果却出人意料,毫秒级响应,多次测试无异常case. 于是不甘心的我随手curl了一下发现也是毫秒级的响应。就有点一脸mb.....

tcpdump

dig 没有发现问题,那就抓下包看看。不抓不知道,一抓发现这个域名在测试环境上有大量的 ServeFail状态码返回。

小结

curl 响应正常,dig 响应正常, 该域名解析时会出现 DNS ServeFail 状态码。但是观察dig的耗时也不是特别严重,但是net库中的DNS解析耗时超过了5秒甚至某些测试中超过了10s。(这时候 call 了SRE同学一起排查)

源码调试

nslookup 什么的都很快,但是net解析却很慢,看起来一头雾水。迷茫中我选择对net库的源码进行调试。源码调试一般常用两种办法,一种是通过dlv gdb 等 debug 工具 进行断点调试,一种是printf调试法.

由于该问题只涉及到dns解析,于是我剥离了业务逻辑,简单得写了2行调试代码, 代码如下:

import (
	"log"
	"net"
	"os"
	"runtime"

	"github.com/pkg/profile"
)

func main() {
	log.Printf("dig %s\n", os.Args[1])
	runtime.SetCPUProfileRate(5000)
	p := profile.Start()
	defer p.Stop()
	log.Println(net.LookupHost(os.Args[1]))
}
复制代码

这段代码主要有两个测试目的,一是剥离业务逻辑测试go net库的 DNS解析是否有问题,二是分析运行耗时。由于这段代码运行时间太短,需要使用runtime.SetCPUProfileRate(5000) 调节一下cpu的采样率,不然你会发现profile文件中是空的。

此外,由于我使用的是 m1 芯片的 Mac 于是我使用了交叉编译在我本机编译了可运行文件:

 GOOS=linux GOARCH=386 go build
复制代码

运行了一下发现确实有问题,不过更令人吃惊的是这次运行居然超过了10s的时间才返回了解析到的ip。 于是我下下载了运行产生的 cpu.pprof 文件,并使用 go tool 工具打开后看到了如下的结果(这个图可能不是第一次的结果了,但是不影响分析问题):

image.png 此图虽然可以看到大致的耗时,但是还是较难以推断问题, 于是我使用了prinf调试法。

prinf 大法好

prinf调试 是一种上古时期的魔法,一直流传到现在,在一些问题诊断过程中仍然是一种非常有用的方法,甚至有人极为推崇。说人话就是在运行过程中打印一些关键日志,按照日志推断程序运行过程的方法。在排查这个问题的过程中我需要对 net库的DNS解析做出日志插入。

注意

要修改 net库代码,鬼知道会改成什么样子。由于官方库一般大家都是使用intaller 安装的,所以源码中并不是git仓库,所以一定要去源码中(GOROOT) git initcheckout 出来一个分支,防止你调试完回来骂我[逃]

print 什么内容

调试日志主要是要详细, 并注意关键信息。应该主要包含:执行耗时,函数名称,关键参数。还要注意格式排版,事实上,一个整齐的格式通常更容易发现问题。

print 在什么地方

首先 pprof 已经将关键的耗时函数统计出来了,可以作为一个参考;其次,在定位问题时使用二分法打印也是个推荐的方法; 最后,暴力打印法虽然不构成犯罪,但是不提倡 [狗头]

打印结果

在使用日志定位到问题在 dnsPacketRoundTrip 方法中后,我对该方法的运行过程做了详细的日志输出(对不起我鲁莽了,给大家道歉)。 DF3C43E7-04A8-45E3-B04E-541A5C003543.png 执行结果出来了,可以看到的是对A记录的解析非常快,但是对于 AAAA记录的解析却耗时,多次测试过程中有的甚至出现了timeout.

image.png

image.png 咦,不知道你注意到没,有些请求很快返回了但是有些请求姗姗来迟,还犹抱琵琶半遮面。对没错说的就是AAAA!

DNS 的几个关键概念

如果你配置过DNS你经常可能听到以下几个记录大家一起复习下:

  • A: 指向ipv4的地址
  • CNAME: 指向另一个域名的记录
  • AAAA: ipv6地址
  • SOA: Start of Authority,起始授权服务器

域名支持 IPv6 吗?

所以按照上面的信息只有AAAA记录的解析出现了问题。那么该域名支持v6么?域名官方的回答是不支持。

ServeFail 会有什么问题?

正常情况下 比如小米的 api.xmpush.xiaomi.com 在不存在 AAAA记录时会返回 NOERROR 并附带一个SOA,这时候服务器会将空记录缓存SOA中携带的TTL秒,但是返回ServeFail 之后,服务器默认缓存1秒,这时候很容易造成缓存击穿的情况。这个和我们平时服务开发过程中要注意对空记录进行缓存的道理有些类似。

谁返回了ServeFail ?

教室里边坐满了阿里云DNS,114.114,8.8 以及 腾讯DNS,A同学问了一圈,发现大家纷纷举起了自己的🙋. 这时候A同学注意到,8.8 同学 你为什么这么优秀,你居然没有举手! 然后A同学就追问阿里的同学,人家没有fail你为什么fail了,你fail的原因是什么(同九义,何汝秀)? 阿里的同学回去翻了翻他的小本子回来说: "是8.8.8.8服务器直接向huawei.com拿的soa记录,而我们的dns是向上一级.cloud.huawei.com 拿的。这是local dns处理方式不同爱好" 所以,大家都没有错!于是又向厂商反馈了这个问题,但是并没有得到答复。

那么,可以不使用ipv6解析吗?

从上面的调试可以看到主要是 AAAA记录 也就是 ipv6 地址在解析时遇到了问题,使用dig 在 114.114.114.114进行 AAAA 域名解析测试发现华为没有配置 AAAA记录。但是看到go源码中(1.16.3)对解析类型是写死的早期版本就更不用说了(不过我去查了master的代码对这个地方略有修改,但还是写死的)。

func (r *Resolver) goLookupIPCNAMEOrder(ctx context.Context, name string, order hostLookupOrder) (addrs []IPAddr, cname dnsmessage.Name, err error) {
	// ...
	qtypes := [...]dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
	// ...
}
复制代码

小结

net库在解析时会同时解析 A记录和 AAAA记录,但是在解析 AAAA记录时Client 端迟迟得不到DNS的响应,并且收到了ServeFail的响应。按照 RFC 1035 中的定义,ServeFail 一般是一种异常情况,如果记录不存在不应该返回这个状态码。

image.png

测试了一下 114.114.114.114 以及 阿里云的DNS 会返回 ServeFail, 但是 8.8.8.8 返回了NOERROR 并附带一个SOA。

image.png

公司内部的DNS服务有问题?

SRE同学说自建的DNS服务器在解析时如果发现是外部域名会直接forward到阿里的DNS服务上,但是我们测试过程中发现虽然阿里的服务器AAAA记录返回ServeFail 但是响应速度不慢。于是SRE同学继续在DNS服务上抓包发现 当收到ServeFail的请求时,本地递归服务器会触发迭代查询。本来是有问题的,再去试一次只能是浪费时间。

如何解决?

问题到这里已经基本清晰。剩下的就比较好解决:

  • SRE同学对 cloud.huawei.com 这个域 进行了禁止迭代查询的配置,也就是说forward服务返回什么就直接响应,什么该域名不再做迭代尝试。
  • 由于go中每次请求都会查询DNS,可以考虑对DNS进行本地缓存的尝试。
  • 向厂商反馈DNS AAAA存在解析问题(事实上一直在反馈).

总结

本文通过记录总结整个过程一来分享给大家自己查询问题的方式和方法,二是更多的与大家交流,成长。回顾过程,分析优劣点,更多的积累问题经验以期在处理问题的时候选取最优的途径,提升Troubleshooting的效率。

引用

文章分类
后端
文章标签