Jason Pan

Prometheus DNS 解析失败的网络分析

黄杰 / 2022-01-18


一、问题描述

需要从通过域名采集两个部署中的指标,两个部署分别对应两个域名和若干个容器:

异常现象: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字节,服务端也不会进行截断:

image-20220118155416338

2.2 复习知识点

image-20220118194642728

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 的左右两侧。

image-20220118164612107image-20220118164641566

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 的协议作为配置加入。