一次 k8s coredns 通信故障分析实录(conntrack 篇)

avatar
@🕳️/3

最近在边缘部署的单机 K8s 环境中出现了间歇性的 DNS 解析失败,服务跑一段时间就出现了。问题的原因之前没有出现过,因此做了一个记录。

部署架构

  • 单节点 Kubernetes 集群
  • CoreDNS 以主机网络模式运行
  • 通过 iptables 规则将 DNS 查询请求转发至虚拟 IP (10.43.0.10:53)

coredns-arch

问题现象

在排查过程中发现了一个有趣的现象:

  • 正常解析场景:使用 dig 命令手动发送 DNS 查询包均能获得正确响应
  • 异常解析场景:Redisson 客户端的 DNSMonitor 代码(基于 Netty 实现)每 5 秒进行的域名解析会出现失败(用来判断 DNS 解析是否失效)

将 Netty 发送的 DNS 数据包保存下来,使用 nc 工具重放相同的 payload 能够成功获得响应。测试命令如下:

nc -u 10.43.0.10 53 < dns_pkt

这就有点奇怪了,几乎所有的条件都一致。后面发现一个奇怪的地方,出问题的时候,在 DNS 解析失败的情况下,Netty 客户端始终使用同一个源端口进行 DNS 查询。这个特征与正常情况下应该随机选择源端口的行为不符。

通过检查 conntrack NAT 表项,conntrack 表项的反向 SRC 和 DST 地址都是不符合预期的(后面会分析),又由于 Redisson 的 DNSMonitor 组件配置了 5 秒的监控间隔,这导致:DNS 查询请求每 5 秒发送一次 UDP,conntrack 表项的默认过期时间为 30 秒,频繁的 DNS 查询导致 conntrack 表项无法正常老化和更新。

udp      17 173 src=10.42.0.129 dst=10.43.0.10 sport=52421 dport=53 src=10.43.0.10 dst=172.20.112.199 sport=53 dport=52421 [ASSURED] mark=0 use=1

当我手动把这个记录删除时,解析立马可以恢复正常。

为什么 conntrack 记录会跑着跑着就不对了。我写了一个脚本监控 conntrack 异常记录出现的时间,

DNS 解析异常与凌晨的定时任务存在关联。运维定时任务导致 CoreDNS 发生非预期的每日重启。

为什么 coredns 重启会导致 conntrack 异常

在 CoreDNS 重启过程中会出现一个临时状态,NAT 规则变更:

  • CoreDNS Pod 终止时,相关的 NAT 规则会被删除
  • 新的 CoreDNS Pod 启动后,会重新添加 NAT 规则

在 NAT 规则被删除到重新添加的这段时间内,如果容器与 CoreDNS(10.43.0.10)发生通信,由于特定的 NAT 规则缺失,流量会被错误地导向默认 NAT 规则处理。

我做了一个重启期间的 iptable 的 NAT 表变化记录,重启操作前的 iptables 的 NAT 表规则:

Chain KUBE-SERVICES (2 references)
 pkts bytes target     prot opt in     out     source               destination    
 2838  262K KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  any    any     anywhere             10.43.0.10           /* kube-system/kube-dns:dns cluster IP */ udp dpt:domain
    0     0 KUBE-NODEPORTS  all  --  any    any     anywhere             anywhere             /* kubernetes service nodeports; NOTE: this must be the last rule in this      chain */ ADDRTYPE match dst-type LOCAL
 
 
Chain KUBE-SVC-TCOU7JCQXEZGVUNU (1 references)
 pkts bytes target     prot opt in     out     source               destination         
 2838  262K KUBE-SEP-2NOFLIOVOWN7BA6Y  all  --  any    any     anywhere             anywhere             /* kube-system/kube-dns:dns */

Chain KUBE-SEP-2NOFLIOVOWN7BA6Y (1 references)
 pkts bytes target     prot opt in     out     source               destination         
  162 15174 KUBE-MARK-MASQ  all  --  any    any     seewo-ac-server      anywhere             /* kube-system/kube-dns:dns */
 2838  262K DNAT       udp  --  any    any     anywhere             anywhere             /* kube-system/kube-dns:dns */ udp to:172.20.112.199:53

在重启过程中,这几条规则被删除了。

conntrack 前后对比如下:

$ conntrack -L

# 异常
udp      17 177 src=10.42.0.33 dst=10.43.0.10 sport=52421 dport=53 src=10.43.0.10 dst=172.20.112.199 sport=53 dport=52421 [ASSURED] mark=0 use=1

# 正常
udp      17 28  src=10.42.0.33 dst=10.43.0.10 sport=59515 dport=53 src=172.20.112.199 dst=10.42.0.33 sport=53 dport=59515 mark=0 use=1

NAT 规则不存在时:

当 NAT 规则存在时:

错误的 NAT 规则会导致 conntrack 表项记录异常,这些异常的 conntrack 记录会持续存在,直到 UDP conntrack 表项自然过期(默认 30 秒),影响后续的 DNS 查询请求的正常处理。

如何复现

写一段 Go 程序,使用同一个端口,不停的与 10.43.0.10 的 53 端口通信

package main

import (
	"fmt"
	"github.com/miekg/dns"
	"net"
	"os"
	"strconv"
	"time"
)

const DnsAddr = "10.43.0.10:53"

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Please provide domain name")
		return
	}

	domain := os.Args[1]
	sleepTime := 1000
	if len(os.Args) > 2 {
		if t, err := strconv.Atoi(os.Args[2]); err == nil {
			sleepTime = t
		}
	}

	fmt.Printf("dns_fk!, dns: %s, domain: %s, sleep: %dms\n", DnsAddr, domain, sleepTime)

	conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 52001})
	if err != nil {
		fmt.Printf("Failed to bind: %v\n", err)
		return
	}
	defer conn.Close()

	dnsAddr, err := net.ResolveUDPAddr("udp", DnsAddr)
	if err != nil {
		fmt.Printf("Failed to resolve DNS address: %v\n", err)
		return
	}

	packet := buildDNSPacket(domain)
	count := 0
	lastPrint := time.Now()

	for {
		count++
		if time.Since(lastPrint) > 3*time.Second {
			fmt.Printf("send count: %d\n", count)
			lastPrint = time.Now()
		}

		_, err := conn.WriteToUDP(packet, dnsAddr)
		if err != nil {
			fmt.Printf("Failed to send packet: %v\n", err)
			continue
		}

		if sleepTime > 0 {
			time.Sleep(time.Duration(sleepTime) * time.Millisecond)
		}
	}
}

func buildDNSPacket(domain string) []byte {
	m := new(dns.Msg)
	m.SetQuestion(dns.Fqdn(domain), dns.TypeA)
	m.RecursionDesired = false

	m.Id = dns.Id()

	data, _ := m.Pack()
	return data
}

./udp_fk rocketmq-nameserver.xeewo-edge-base.svc.cluster.local 10

此时会创建一条 conntrack 记录

$ conntrack -E -s 10.42.0.33  -p udp

 [NEW] udp      17 30 src=10.42.0.33 dst=10.43.0.10 sport=52001 dport=53 [UNREPLIED] src=172.20.112.199 dst=10.42.0.33 sport=53 dport=52001
 [UPDATE] udp      17 30 src=10.42.0.33 dst=10.43.0.10 sport=52001 dport=53 src=172.20.112.199 dst=10.42.0.33 sport=53 dport=52001
 [UPDATE] udp      17 120 src=10.42.0.33 dst=10.43.0.10 sport=52001 dport=53 src=172.20.112.199 dst=10.42.0.33 sport=53 dport=52001 [ASSURED]

然后重启 coredns

kubectl delete pod coredns-65b5854f7-lkwlf -n kube-system

观察 conntrack 变化过程:

$ conntrack -E -s 10.42.0.33  -p udp
// 重启前
[NEW] udp      17 30 src=10.42.0.33 dst=10.43.0.10 sport=52001 dport=53
[UNREPLIED] src=172.20.112.199 dst=10.42.0.33 sport=53 dport=52001
[UPDATE] udp      17 30 src=10.42.0.33 dst=10.43.0.10 sport=52001 dport=53 src=172.20.112.199 dst=10.42.0.33 sport=53 dport=52001
[UPDATE] udp      17 120 src=10.42.0.33 dst=10.43.0.10 sport=52001 dport=53 src=172.20.112.199 dst=10.42.0.33 sport=53 dport=52001 [ASSURED]
 
// 重启后

// 旧的 conntrack 记录被删除
[DESTROY] udp      17 src=10.42.0.33 dst=10.43.0.10 sport=52001 dport=53 src=172.20.112.199 dst=10.42.0.33 sport=53 dport=52001 [ASSURED] 

// 新增了一条新的 conntrack 记录(异常)
[NEW] udp      17 30 src=10.42.0.33 dst=10.43.0.10 sport=52001 dport=53 [UNREPLIED] src=10.43.0.10 dst=172.20.112.199 sport=53 dport=42285

通过抓包就可以发现,此时的 DNS 通信就是失败的。

k8s 的问题?

事后,经过查看 k8s 的 issue,还真这是一个老 bug 了(kube-proxy/iptables, client continuous to send packets, udp server pod restart cause UDP conntrack entry error )链接地址 github.com/kubernetes/…

k8s 的新版本可以缓解这一问题。

修复

k8s 版本是升不动的,改业务代码倒是比较容易,本质问题是 redisson 的 DNSMonitor 特性,目前看是使用同一个 UDP 端口去查询 DNS,解决一下就可以了,具体的措施就不展开。

小结

这次问题出现有两个条件:

  • coredns 每天定期重启,加速了问题的出现
  • redisson 使用固定的端口,持续的请求 coredns,导致错误的 conntrack 记录无法过期清理

正经人谁用 redisson。。。