使用node local dns来提升ClusterDNS服务质量
在 Kubernetes 集群会碰到这个间歇性 5 延迟的问题,Weave works 发布了一篇博客 Racy conntrack and DNS lookup timeouts 详细介绍了问题的原因。
简单来说,由于 UDP 是无连接的,内核 netfilter 模块在处理同一个 socket 上的并发 UDP 包时就可能会有三个竞争问题。以下面的 conntrack 和 DNAT 工作流程为例: 由于 UDP 的 connect 系统调用不会立即创建 conntrack 记录,而是在 UDP 包发送之后才去创建,这就可能会导致下面三种问题: 两个 UDP 包在第一步 nf*conntrack*in 中都没有找到 conntrack 记录,所以两个不同的包就会去创建相同的 conntrack 记录(注意五元组是相同的)。 一个 UDP 包还没有调用 get*unique*tuple 时 conntrack 记录就已经被另一个 UDP 包确认了。 两个 UDP 包在 ipt*do*table 中选择了两个不同端点的 DNAT 规则。 所有这三种场景都会导致最后一步 _*nf*conntrack_confirm 失败,从而一个 UDP 包被丢弃。由于 GNU C 库和 musl libc 库在查询 DNS 时,都会同时发出 A 和 AAAA DNS 查询,由于上述的内核竞争问题,就可能会发生其中一个包被丢掉的问题。丢弃之后客户端会超时重试,超时时间通常是 5 秒。
简介: NodeLocal DNSCache在集群的上运行一个dnsCache daemonset来提高clusterDNS性能和可靠性。在ACK集群上的一些测试表明:相比于纯coredns方案,nodelocaldns + coredns方案能够大幅降低DNS查询timeout的频次,提升服务稳定性,能够扛住1倍多的QPS。
NodeLocal DNSCache通过在集群上运行一个dnsCache daemonset来提高clusterDNS性能和可靠性。在ACK集群上的一些测试表明:相比于纯coredns方案,nodelocaldns + coredns方案能够大幅降低DNS查询timeout的频次,提升服务稳定性。
本文将介绍如何在ACK集群上部署node local dns。
部署nodelocaldns
nodelocaldns通过添加iptables规则能够接收节点上所有发往169.254.20.10
的dns查询请求,把针对集群内部域名查询请求路由到coredns;把集群外部域名请求直接通过host网络发往集群外部dns服务器。
# 下载部署脚本 $ curl https://node-local-dns.oss-cn-hangzhou.aliyuncs.com/install-nodelocaldns.sh # 部署。确保kubectl能够连接集群 $ bash install-nodelocaldns.sh
定制业务容器dnsConfig
为了使业务容器能够使用nodelocaldns,需要将nameserver配置为169.254.20.10
,而不是ClusterDNS。定制dnsConfig有以下几点需要注意到:
- dnsPolicy: None。不使用ClusterDNS。
- 配置searches,保证集群内部域名能够被正常解析。
- 适当降低ndots值。当前ACK集群ndots值默认为5,降低ndots值有利于加速集群外部域名访问。如果业务容器没有使用带多个dots的集群内部域名,建议将值设为2。
apiVersion: v1 kind: Pod metadata: name: alpine namespace: default spec: containers: - image: alpine command: - sleep - "10000" imagePullPolicy: Always name: alpine dnsPolicy: None dnsConfig: nameservers: ["169.254.20.10"] searches: - default.svc.cluster.local - svc.cluster.local - cluster.local options: - name: ndots value: "2"
如何避免这个问题
要避免 DNS 延迟的问题,就要设法绕开上述三个问题,所以就有下面几种方法:
①. 禁止并发 DNS 查询,比如在 Pod 配置中开启 single-request-reopen
选项强制 A 查询和 AAAA 查询使用相同的 socket:
dnsConfig:
options:
- name: single-request-reopen
②. 禁用 IPv6 从而避免 AAAA 查询,比如可以给 Grub 配置 ipv6.disable=1
来禁止 ipv6(需要重启节点才可以生效)。
通过大神写go 测试dns解析时间脚本进行了测试
package main import ( "context" "flag" "fmt" "net" "sync/atomic" "time" ) var host string var connections int var duration int64 var limit int64 var timeoutCount int64 func main() { // os.Args = append(os.Args, "-host", "www.baidu.com", "-c", "200", "-d", "30", "-l", "5000") flag.StringVar(&host, "host", "", "Resolve host") flag.IntVar(&connections, "c", 100, "Connections") flag.Int64Var(&duration, "d", 0, "Duration(s)") flag.Int64Var(&limit, "l", 0, "Limit(ms)") flag.Parse() var count int64 = 0 var errCount int64 = 0 pool := make(chan interface{}, connections) exit := make(chan bool) var ( min int64 = 0 max int64 = 0 sum int64 = 0 ) go func() { time.Sleep(time.Second * time.Duration(duration)) exit <- true }() endD: for { select { case pool <- nil: go func() { defer func() { <-pool }() resolver := &net.Resolver{} now := time.Now() _, err := resolver.LookupIPAddr(context.Background(), host) use := time.Since(now).Nanoseconds() / int64(time.Millisecond) if min == 0 || use < min { min = use } if use > max { max = use } sum += use if limit > 0 && use >= limit { timeoutCount++ } atomic.AddInt64(&count, 1) if err != nil { fmt.Println(err.Error()) atomic.AddInt64(&errCount, 1) } }() case <-exit: break endD } } fmt.Printf("request count:%d\nerror count:%d\n", count, errCount) fmt.Printf("request time:min(%dms) max(%dms) avg(%dms) timeout(%dn)\n", min, max, sum/count, timeoutCount) }
使用方法为:
./dns -host {service}.{namespace} -c 200 -d 30 比如 ./dns -host redis.senyint -c 200 -d 30
阿里云的方式内部解析还是5秒左右,没用, 使用①+nodelocaldns方式解析时间控制在800ms左右
参考摘自:
https://developer.aliyun.com/article/709471
https://blog.csdn.net/qq_23435961/article/details/106659069
https://zhuanlan.zhihu.com/p/145127061