Prometheus DNS 解析失败的网络分析
潘忠显 / 2022-01-18
一、问题描述
需要从通过域名采集两个部署中的指标,两个部署分别对应两个域名和若干个容器:
- DOMAIN-A: 对应 200 个 IP
- DOMAIN-B: 对应 280 个 IP
异常现象:DOMAIN-A 可以正常采集,DOMAIN-B 不能够正常采集,并且打印错误日志如下:
warn component=“discovery manager scrape” discovery=dns msg=“DNS resolution failed” server={DNS-SERVER-IP} name=DOMAIN-B. err=“dns: overflow unpacking uint16” error component=“discovery manager scrape” discovery=dns msg=“Error refreshing DNS targets” err=“could not resolve "DOMAIN-B": all servers responded with errors to at least one search domain”
Prometheus 部分配置如下:
scrape_configs:
- job_name: query_mark_state
honor_timestamps: true
metrics_path: /metrics
scheme: http
relabel_configs:
- source_labels: [__meta_dns_name]
target_label: service
dns_sd_configs:
- names:
- DOMAIN-A # <<<< 第一个域名 <<<<<
refresh_interval: 1s
type: A
port: 8080
- job_name: get_task_list
...
dns_sd_configs:
- names:
- DOMAIN-B # <<<< 第二个域名 <<<<<
...
二、问题排查
2.1 抓包分析
DNS可以采用 UDP 或者 TCP,Prometheus 使用的也是 UDP 协议,两个域名从抓包看都能正常解析:
Prometheus在解析域名时将 TC 位置零,这意味着如果返回长度超过512字节,服务端也不会进行截断:
2.2 复习知识点
- DNS 使用 53 端口,可以用 TCP 或 UDP,常用 UDP
- DNS UDP 使用 TC 标识时,长度超过 512 会截断
- UDP 最大载荷长度 65535,IP 分片
- 以太网 MTU 1500B,IP 头 20B,前两个 IP 回包有效载荷各 1480B,第三个 1181 - 16 - 20 = 1145B,因此 UDP 在 IP 分片前 大小为 4105B
- UDP 包头 8B,UDP消息大小为 4097 B
2.3 Prometheus DNS库分析
Prometheus 使用的库为 miekg/dns,Prometheus 对其进行了简单的封装在 discovery/dns/dns.go 中,主要作用是读取 Yaml 配置来创建 dns.Client
、增加对应的日志上报和指标统计。
通过在 Prometheus 和 miekg/dns 中添加日志,发现实际的问题在于 msg_helpers.go 中的 func unpackHeader(msg []byte, off int)
函数报错,原因是在对输入的 buffer 解包时,传入的数据只有 4096 字节,造成解包失败。
而我们两个域名对应的 UDP 返回包分别是 3297B 和 4097B,恰好落在了 4096 的左右两侧。
miekg/dns 有几个常量:
const (
// DefaultMsgSize is the standard default for messages larger than 512 bytes.
DefaultMsgSize = 4096
// MinMsgSize is the minimal size of a DNS packet.
MinMsgSize = 512
// MaxMsgSize is the largest possible DNS packet.
MaxMsgSize = 65535
)
而 Prometheus 的封装中直接使用了这个 UDP + DefaultMsgSize:
client := &dns.Client{}
msg.SetQuestion(dns.Fqdn(name), queryType)
if edns {
msg.SetEdns0(dns.DefaultMsgSize, false)
}
2.4 小结
使用Prometheus时,用户侧是无法通过配置来改变 协议 或 消息长度大小,就是指定的 UDP + 4096 Buffer 的,而这种设置在 IP 数量巨大时会造成解析失败。
三、问题解决
根据上边问题分析,无法通过配置来解决返回 IP 太多的问题,需要直接修改 Prometheus 对 DNS 的封装,以下方式二者选其一均可解决。
改用 TCP 协议,意味着默认 Buffer 大小是 65535:
client := &dns.Client{
Net: "tcp", // << 新增
}
依然使用 UDP 协议,指定 Buffer 大小为 UDP 包的最大值:
if edns {
msg.SetEdns0(dns.MaxMsgSize, false) // DefaultMsgSize -> MaxMsgSize
}
需要给 Prometheus 提 ISSUE 将 DNS 的协议作为配置加入。