Kubernetes DNS查询和优化

Posted by     "Pyker" on Friday, April 16, 2021

TOC

CoreDNS是Kubernetes集群中负责DNS解析的组件,能够支持解析集群内部自定义服务域名和集群外部域名。CoreDNS具备丰富的插件集,在集群层面支持自建DNS、自定义hosts、CNAME、rewrite等需求。K8S集群中默认部署形态在DNS QPS较高场景下,可能会出现解析压力,导致部分查询失败。本文介绍如何优化集群DNS性能。

合理的dns副本数

调整CoreDNS副本数与集群节点数到合适比率有助于提升集群服务发现的性能,该比值推荐为1:8,即一个CoreDNS Pod支撑8个集群节点。

  • 当集群节点无需大规模扩缩容时,执行以下命令调整目标副本数到目标值。

kubectl scale –replicas={target} deployment/coredns -n kube-system

  • 集群节点需要大规模扩缩容时,推荐部署以下YAML模板,使用集群水平伸缩器cluster-proportional-autoscaler动态调整副本数量。不建议使用针对QPS、CPU或MEM指标的水平伸缩器,实测效果较差.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dns-autoscaler
  namespace: kube-system
  labels:
    k8s-app: dns-autoscaler
spec:
  selector:
    matchLabels:
       k8s-app: dns-autoscaler
  template:
    metadata:
      labels:
        k8s-app: dns-autoscaler

    spec:
      serviceAccountName: admin
      containers:
      - name: autoscaler
        image: registry.cn-hangzhou.aliyuncs.com/ringtail/cluster-proportional-autoscaler-amd64:v1.3.0
        resources:
            requests:
                cpu: "200m"
                memory: "150Mi"
        command:
          - /cluster-proportional-autoscaler
          - --namespace=kube-system
          - --configmap=dns-autoscaler
          - --target=Deployment/coredns
          - --default-params={"linear":{"coresPerReplica":2,"nodesPerReplica":1,"min":2,"max":100,"preventSinglePointFailure":true}}
          - --logtostderr=true
          - --v=2

上述使用线程伸缩策略中,CoreDNS副本数的计算公式为replicas = max (ceil (cores × 1/coresPerReplica), ceil (nodes × 1/nodesPerReplica) ),且CoreDNS副本数受到max,min限制。 线程伸缩策略参数如下。

{
     "coresPerReplica": 2,                # 每个coredns pod可以处理的cpu核心数,和nodesPerReplica比较取最大值。
     "nodesPerReplica": 1,                # 每个coredns pod 可以处理的node节点数,和coresPerReplica比较取最大值。
     "min": 2,
     "max": 100,
     "preventSinglePointFailure": false,  # 设置为true时,如果有多个节点,controller会确保至少有2个副本。
     "includeUnschedulableNodes": false   # 设置为true时,副本将根据节点总数进行伸缩。否则,副本只会根据可调度节点的数量进行扩展
}

例如,给定的群集具有4个节点和13个核心。 使用上述参数,每个副本可以处理1个节点。 因此,我们需要4/1 = 4个副本来照顾所有4个节点。 每个副本可以处理2个核心。 我们需要ceil(13/2)= 7个副本来处理所有13个内核。 结果,控制器将选择较大的一个,即7。

部署NodeLocaldns

NodeLocal DNSCache在集群的上运行一个dnsCache daemonset来提高clusterDNS性能和可靠性。在K8S集群上的一些测试表明:相比于纯coredns方案,nodelocaldns + coredns方案能够大幅降低DNS查询timeout的频次,提升服务稳定性,能够扛住1倍多的QPS。

nodelocaldns通过添加iptables规则能够接收节点上所有发往169.254.20.10的dns查询请求,把针对集群内部域名查询请求路由到coredns;把集群外部域名请求直接通过host网络发往集群外部dns服务器。

架构图

部署模版:

# 保存以下脚本 命名为install-nodelocaldns.sh
#!/env/bin/bash
localDnsIp="169.254.20.10"
upstreanDnsIp=$(kubectl get svc -n kube-system | grep kube-dns | awk '{ print $3 }')

yaml=$(cat <<-END
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
---
apiVersion: v1
kind: Service
metadata:
  name: kube-dns-upstream
  namespace: kube-system
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
    kubernetes.io/name: "KubeDNSUpstream"
spec:
  ports:
  - name: dns
    port: 53
    protocol: UDP
    targetPort: 53
  - name: dns-tcp
    port: 53
    protocol: TCP
    targetPort: 53
  selector:
    k8s-app: kube-dns
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
data:
  Corefile: |
    cluster.local:53 {
        errors
        cache {
                success 9984 300
                denial 9984 5
        }
        reload
        loop
        bind $localDnsIp $upstreanDnsIp
        forward . __PILLAR__CLUSTER__DNS__ {
                force_tcp
        }
        prometheus :9253
        health $localDnsIp:8080
        }
    in-addr.arpa:53 {
        errors
        cache 30
        reload
        loop
        bind $localDnsIp $upstreanDnsIp
        forward . __PILLAR__CLUSTER__DNS__ {
                force_tcp
        }
        prometheus :9253
        }
    ip6.arpa:53 {
        errors
        cache 30
        reload
        loop
        bind $localDnsIp $upstreanDnsIp
        forward . __PILLAR__CLUSTER__DNS__ {
                force_tcp
        }
        prometheus :9253
        }
    .:53 {
        errors
        cache 30
        reload
        loop
        bind $localDnsIp $upstreanDnsIp
        forward . __PILLAR__UPSTREAM__SERVERS__
        prometheus :9253
        }
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    k8s-app: node-local-dns
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
spec:
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 10%
  selector:
    matchLabels:
      k8s-app: node-local-dns
  template:
    metadata:
      labels:
        k8s-app: node-local-dns
      annotations:
        prometheus.io/port: "9253"
        prometheus.io/scrape: "true"
    spec:
      priorityClassName: system-node-critical
      serviceAccountName: node-local-dns
      hostNetwork: true
      dnsPolicy: Default  # 不使用cluster DNS解析.
      tolerations:
      - key: "CriticalAddonsOnly"
        operator: "Exists"
      - effect: "NoExecute"
        operator: "Exists"
      - effect: "NoSchedule"
        operator: "Exists"
      containers:
      - name: node-cache
        image: k8s.gcr.io/dns/k8s-dns-node-cache:1.17.0
        resources:
          requests:
            cpu: 25m
            memory: 5Mi
        args: [ "-localip", "$localDnsIp,$upstreanDnsIp", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream" ]
        securityContext:
          privileged: true
        ports:
        - containerPort: 53
          name: dns
          protocol: UDP
        - containerPort: 53
          name: dns-tcp
          protocol: TCP
        - containerPort: 9253
          name: metrics
          protocol: TCP
        livenessProbe:
          httpGet:
            host: $localDnsIp
            path: /health
            port: 8080
          initialDelaySeconds: 60
          timeoutSeconds: 5
        volumeMounts:
        - mountPath: /run/xtables.lock
          name: xtables-lock
          readOnly: false
        - name: config-volume
          mountPath: /etc/coredns
        - name: kube-dns-config
          mountPath: /etc/kube-dns
      volumes:
      - name: xtables-lock
        hostPath:
          path: /run/xtables.lock
          type: FileOrCreate
      - name: kube-dns-config
        configMap:
          name: kube-dns
          optional: true
      - name: config-volume
        configMap:
          name: node-local-dns
          items:
            - key: Corefile
              path: Corefile.base
---
# A headless service is a service with a service IP but instead of load-balancing it will return the IPs of our associated Pods.
# We use this to expose metrics to Prometheus.
apiVersion: v1
kind: Service
metadata:
  annotations:
    prometheus.io/port: "9253"
    prometheus.io/scrape: "true"
  labels:
    k8s-app: node-local-dns
  name: node-local-dns
  namespace: kube-system
spec:
  clusterIP: None
  ports:
    - name: metrics
      port: 9253
      targetPort: 9253
  selector:
    k8s-app: node-local-dns

END
)

echo "$yaml" > nodelocaldns-ds.yaml
kubectl apply -f nodelocaldns-ds.yaml
$ sh install-nodelocaldns.sh

此时已经完成了nodelocaldns的部署。通过查看日志会发现nodelocaldns已经正常参与解析。

$ kubectl logs node-local-dns-8x3o5 -n kube-system
[INFO] 172.31.3.217:41770 - 20309 "A IN www.baidu.com.default.svc.cluster.local. udp 57 false 512" NXDOMAIN qr,aa,rd 150 0.002431573s
[INFO] 172.31.3.217:41770 - 20650 "AAAA IN www.baidu.com.default.svc.cluster.local. udp 57 false 512" NXDOMAIN qr,aa,rd 150 0.002533574s
[INFO] 172.31.3.217:58177 - 3493 "AAAA IN www.baidu.com.svc.cluster.local. udp 49 false 512" NXDOMAIN qr,aa,rd 142 0.001126744s
[INFO] 172.31.3.217:58177 - 3232 "A IN www.baidu.com.svc.cluster.local. udp 49 false 512" NXDOMAIN qr,aa,rd 142 0.001107933s
[INFO] 172.31.3.217:51909 - 6463 "AAAA IN www.baidu.com.cluster.local. udp 45 false 512" NXDOMAIN qr,aa,rd 138 0.001056258s
[INFO] 172.31.3.217:51909 - 6211 "A IN www.baidu.com.cluster.local. udp 45 false 512" NXDOMAIN qr,aa,rd 138 0.00108577s
[INFO] 172.31.3.217:60395 - 42129 "A IN www.baidu.com.ap-southeast-1.compute.internal. udp 63 false 512" NXDOMAIN qr,rd,ra 186 0.003109582s
[INFO] 172.31.3.217:60395 - 42426 "AAAA IN www.baidu.com.ap-southeast-1.compute.internal. udp 63 false 512" NXDOMAIN qr,rd,ra 186 0.004827469s
[INFO] 172.31.3.217:54473 - 18014 "A IN www.baidu.com. udp 31 false 512" NOERROR qr,rd,ra 181 0.001185881s
[INFO] 172.31.3.217:54473 - 18249 "AAAA IN www.baidu.com. udp 31 false 512" NOERROR qr,rd,ra 207 0.001337411s

解析search域影响查询响应

目前,在ClusterFirst模式下,2次(1次IPv4,1次IPv6)集群外部域名查询产生10次(5次IPv4,5次IPv6)查询请求。(这里有aws域名,如果裸机部署k8s集群将为4次)例如,解析www.baidu.com域名,会先分别携带三个集群主域名后缀,产生八次无效查询请求,这样会导致集群DNS QPS放大四倍,影响性能。

查看pod里search域

$ kubectl exec test-pod -- cat /etc/resolv.conf
nameserver 10.100.0.10
search default.svc.cluster.local svc.cluster.local cluster.local ap-southeast-1.compute.internal
options ndots:5

通过在pod中cat /etc/resolv.conf可以发现有4个search域,默认还有一个.,故有5次,nodots:5表示要经过5次查询。这将严重影响dns解析效果。 我们将Pod的search改为(这里直接在pod里修改,后续通过后可通过dnsConfig修改):

search default.svc.cluster.local
options ndots:2

deployment的dnsConfig配置:

  dnsPolicy: None
  dnsConfig:
    nameservers:
    - 169.254.20.10      # 启用谁也nodelocaldns解析
    searches:
    - svc.cluster.local  # 这样设置,要求服务名称调用时,严格svcname.namespace格式
    options:
    - name: ndots
      value: "2"
    - name: single-request-reopen
    - name: timeout
      value: "1"

在配置生效后,我们验证:

$ curl -s -IL https://www.ipyker.com && curl -s -IL helloworld.hw:5000/hello
HTTP/2 200
Content-Length: 60
...

$ kubectl logs node-local-dns-8x3o5 -n kube-system
[INFO] 172.31.3.217:47392 - 38526 "AAAA IN www.ipyker.com. udp 32 false 512" NOERROR qr,rd,ra 76 0.000413585s
[INFO] 172.31.3.217:47392 - 38307 "A IN www.ipyker.com. udp 32 false 512" NOERROR qr,rd,ra 204 0.000494763s
[INFO] 172.31.3.217:37782 - 60764 "AAAA IN helloworld.hw.svc.cluster.local. udp 49 false 512" NOERROR qr,aa,rd 142 0.000102695s
[INFO] 172.31.3.217:37782 - 60470 "A IN helloworld.hw.svc.cluster.local. udp 49 false 512" NOERROR qr,aa,rd 96 0.000050289s

可以发现不管内外地址,请求一次就命中。

为什么ndots:5会对应用程序性能产生负面影响?

如果您的应用程序进行了大量外部流量,则对于已建立的每个TCP连接(或更具体而言,对于每个已解析的名称),它将在正确解析名称之前发出5个DNS查询,因为它将通过4个DNS查询。 首先是本地搜索域,最后将发布绝对名称解析查询。

single-request-reopen 选项

since glibc 2.9中有说明:在_res.options中设置RES_SNGLKUPREOP。 解析程序对A和AAAA请求使用相同的套接字。 某些硬件错误地仅发送回一个答复。 发生这种情况时,客户端系统将等待第二次答复。 启用此选项将更改此行为,以便如果未正确处理来自同一端口的两个请求,它将在发送第二个请求之前关闭套接字并打开一个新请求。

timeout 选项

一般/etc/resolv.conf 还有两个默认的值至关重要,一个是超时的 timeout,一个是重试的 attempts,默认情况下,前者是 5s 后者是 2 次。理解该两个字段:每个 nameserver 等待 timeout 的时间,如果存在多个namespace都没结果,resolver 会重复上面的步骤 (attempts – 1) 次。设置的目的是加快nodelocaldns不能处理的解析快速转发到coredns。

ipv6解析影响服务查询时间

通过上面的记录,可以发现每一次请求都会有AAAA解析,即ipv6,我们可以根据自己业务性质是否有使用到ipv6特性来关闭该查询。如果K8S集群宿主机没有关闭IPV6内核模块的话,容器请求coredns时的默认行为是同时发起IPV4和IPV6解析。 此处暂时不涉及主机层面禁用ipv6的步骤。

DNS缓存命中和延时

首先,我们环境当前存在部分延迟,所以本次直接演示:

  • 准备一个curl请求文本
cat > reuqest-time.txt <<EOF
time_namelookup: %{time_namelookup}\n
time_connect: %{time_connect}\n
time_appconnect: %{time_appconnect}\n
time_redirect: %{time_redirect}\n
time_pretransfer: %{time_pretransfer}\n
time_starttransfer: %{time_starttransfer}\n
----------\n
time_total: %{time_total}\n
EOF
  • 测试请求,可多执行几次 发现dns解析非常耗时,216ms,总耗时才391ms。
$ curl -w "@reuqest-time.txt" -o /dev/null -s -L helloworld.hw:5000/hello
time_namelookup: 0.216190
time_connect: 0.217433
time_appconnect: 0.233136
time_redirect: 0.000000
time_pretransfer: 0.233272
time_starttransfer: 0.390998
----------
time_total: 0.391311

修改Coredns的配置文件,修改缓存和ttl时间

apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        log
        errors
        health
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           upstream
           fallthrough in-addr.arpa ip6.arpa
           ttl 300  # 修改ttl的值为300秒,默认为5秒
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache {
                success 9984 300 120
                denial 9984 5
        }        
        loop
        reload
        loadbalance  # 随机A,AAAA,MX解析的顺序
    }    
  • 插件cache的语法为cache [TTL] [ZONES...],最大TTL,单位为秒。如果未指定,则使用最大TTL, NOERROR响应为3600,不存在响应为1800。 例如:设置TTL为300:意思为缓存300将缓存记录长达300秒。ZONES应该缓存的区域。如果为空,则使用配置块中的区域。(一个xxx:53{}块即为一个zone) cache {success CAPACITY TTL MINTTL}: 覆盖用于缓存成功响应的设置。 “CAPACITY”表示开始驱逐(随机)之前缓存的最大数据包数。 TTL会覆盖缓存的最大TTL。 MINTTL会覆盖缓存的最小TTL(默认值为5),这对于将查询限制到后端很有用。 cache {denial CAPACITY TTL MINTTL}: 覆盖用于缓存拒绝存在响应的设置。 “CAPACITY”表示开始驱逐(LRU)之前缓存的最大数据包数。 TTL会覆盖缓存的最大TTL。 MINTTL会覆盖缓存的最小TTL(默认值为5),这对于将查询限制到后端很有用。 第三类(错误),但是这些响应永远不会被缓存。

  • 插件Kubernetes TTL字段允许您为响应设置自定义TTL。 默认值为5秒。 允许的最小TTL为0秒,最大为3600秒。 将TTL设置为0将防止记录被缓存。

「真诚赞赏,手留余香」

云原生与运维

真诚赞赏,手留余香

使用微信扫描二维码完成支付


comments powered by Disqus