記一次 K8S 內部服務調用域名解析超時排坑經歷

前言

近期線上 k8s 時不時就會出現一些內部服務間的調用超時問題,經過日誌能夠得知超時的緣由都是出如今域名解析上,而且都是 k8s 內部的域名解析超時,因而直接先將內部域名替換成 k8s service 的 IP,觀察一段時間發現沒有超時的狀況發生了,可是因爲使用 service IP 不是長久之計,因此還要去找解決辦法。node

復現

一開始運維同事在調用方 pod 中使用ab工具對目標服務進行了屢次壓測,並無發現有超時的請求,我介入以後分析ab這類 http 壓測工具應該都會有 dns 緩存,而咱們主要是要測試 dns 服務的性能,因而直接動手擼了一個壓測工具只作域名解析,代碼以下:linux

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)
}

編譯好二進制程序直接丟到對應的 pod 容器中進行壓測:git

# 200個併發,持續30秒
./dns -host {service}.{namespace} -c 200 -d 30

此次能夠發現最大耗時有5s多,屢次測試結果都是相似:github

而咱們內部服務間 HTTP 調用的超時通常都是設置在3s左右,以此推斷出與線上的超時狀況應該是同一種狀況,在併發高的狀況下會出現部分域名解析超時而致使 HTTP 請求失敗。express

緣由

起初一直覺得是coredns的問題,因而找運維升級了下coredns版本再進行壓測,發現問題仍是存在,說明不是版本的問題,難道是coredns自己的性能就差致使的?想一想也不太可能啊,才 200 的併發就頂不住了那性能也未免太弱了吧,結合以前的壓測數據,平均響應都挺正常的(82ms),可是就有個別請求會延遲,並且都是 5 秒左右,因此就又帶着k8s dns 5s的關鍵字去 google 搜了一下,這不搜不知道一搜嚇一跳啊,原來是 k8s 裏的一個大坑啊(其實和 k8s 沒有太大的關係,只是 k8s 層面沒有提供解決方案)。apache

5s 超時緣由

linux 中glibc的 resolver 的缺省超時時間是 5s,而致使超時的緣由是內核conntrack模塊的 bug。json

Weave works 的工程師 Martynas Pumputis 對這個問題作了很詳細的分析: https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts

這裏再引用下https://imroc.io/posts/kubernetes/troubleshooting-with-kubernetes-network/文章中的解釋:api

DNS client (glibc 或 musl libc) 會併發請求 A 和 AAAA 記錄,跟 DNS Server 通訊天然會先 connect (創建 fd),後面請求報文使用這個 fd 來發送,因爲 UDP 是無狀態協議, connect 時並不會發包,也就不會建立 conntrack 表項, 而併發請求的 A 和 AAAA 記錄默認使用同一個 fd 發包,send 時各自發的包它們源 Port 相同(由於用的同一個 socket 發送),當併發發包時,兩個包都尚未被插入 conntrack 表項,因此 netfilter 會爲它們分別建立 conntrack 表項,而集羣內請求 kube-dns 或 coredns 都是訪問的 CLUSTER-IP,報文最終會被 DNAT 成一個 endpoint 的 POD IP,當兩個包剛好又被 DNAT 成同一個 POD IP 時,它們的五元組就相同了,在最終插入的時候後面那個包就會被丟掉,若是 dns 的 pod 副本只有一個實例的狀況就很容易發生(始終被 DNAT 成同一個 POD IP),現象就是 dns 請求超時,client 默認策略是等待 5s 自動重試,若是重試成功,咱們看到的現象就是 dns 請求有 5s 的延時。

解決方案

方案(一):使用 TCP 協議發送 DNS 請求

經過resolv.confuse-vc選項來開啓 TCP 協議緩存

測試

  1. 修改/etc/resolv.conf文件,在最後加入一行文本:
    options use-vc
  2. 進行壓測:
    # 200個併發,持續30秒,記錄超過5s的請求個數 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
    結果以下:

結論

確實沒有出現5s的超時問題了,可是部分請求耗時仍是比較高,在4s左右,並且平均耗時比 UPD 協議的還高,效果並很差。併發

方案(二):避免相同五元組 DNS 請求的併發

經過resolv.confsingle-request-reopensingle-request選項來避免:

  • single-request-reopen (glibc>=2.9) 發送 A 類型請求和 AAAA 類型請求使用不一樣的源端口。這樣兩個請求在 conntrack 表中不佔用同一個表項,從而避免衝突。
  • single-request (glibc>=2.10) 避免併發,改成串行發送 A 類型和 AAAA 類型請求,沒有了併發,從而也避免了衝突。

測試 single-request-reopen

  1. 修改/etc/resolv.conf文件,在最後加入一行文本:
    options single-request-reopen
  2. 進行壓測:
    # 200個併發,持續30秒,記錄超過5s的請求個數 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
    結果以下:

測試 single-request

  1. 修改/etc/resolv.conf文件,在最後加入一行文本:
    options single-request
  2. 進行壓測:
    # 200個併發,持續30秒,記錄超過5s的請求個數 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
    結果以下:

結論

經過壓測結果能夠看到single-request-reopensingle-request選項確實能夠顯著的下降域名解析耗時。

關於方案(一)和方案(二)的實施步驟和缺點

實施步驟

其實就是要給容器的/etc/resolv.conf文件添加選項,目前有兩個方案比較合適:

  1. 經過修改 pod 的 postStart hook 來設置
lifecycle:
  postStart:
    exec:
      command:
        - /bin/sh
        - -c
        - "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"
  1. 經過修改 pod 的 template.spec.dnsConfig 來設置
template:
  spec:
    dnsConfig:
      options:
        - name: single-request-reopen
: 須要 k8s 版本>=1.9

缺點

不支持alpine基礎鏡像的容器,由於apline底層使用的musl libc庫並不支持這些 resolv.conf 選項,因此若是使用alpine基礎鏡像構建的應用,仍是沒法規避超時的問題。

方案(三):本地 DNS 緩存

其實 k8s 官方也意識到了這個問題比較常見,給出了 coredns 以 cache 模式做爲 daemonset 部署的解決方案: https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns/nodelocaldns

大概原理就是:

本地 DNS 緩存以 DaemonSet 方式在每一個節點部署一個使用 hostNetwork 的 Pod,建立一個網卡綁上本地 DNS 的 IP,本機的 Pod 的 DNS 請求路由到本地 DNS,而後取緩存或者繼續使用 TCP 請求上游集羣 DNS 解析 (因爲使用 TCP,同一個 socket 只會作一遍三次握手,不存在併發建立 conntrack 表項,也就不會有 conntrack 衝突)

部署

  1. 獲取當前kube-dns service的 clusterIP
# kubectl -n kube-system get svc kube-dns -o jsonpath="{.spec.clusterIP}"
10.96.0.10
  1. 下載官方提供的 yaml 模板進行關鍵字替換
wget -O nodelocaldns.yaml "https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml" && \
sed -i 's/__PILLAR__DNS__SERVER__/10.96.0.10/g' nodelocaldns.yaml && \
sed -i 's/__PILLAR__LOCAL__DNS__/169.254.20.10/g' nodelocaldns.yaml && \
sed -i 's/__PILLAR__DNS__DOMAIN__/cluster.local/g' nodelocaldns.yaml && \
sed -i 's/__PILLAR__CLUSTER__DNS__/10.96.0.10/g' nodelocaldns.yaml && \
sed -i 's/__PILLAR__UPSTREAM__SERVERS__/\/etc\/resolv.conf/g' nodelocaldns.yaml
  1. 最終 yaml 文件以下:
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

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 30
                denial 9984 5
        }
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . 10.96.0.10 {
                force_tcp
        }
        prometheus :9253
        health 169.254.20.10:8080
        }
    in-addr.arpa:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . 10.96.0.10 {
                force_tcp
        }
        prometheus :9253
        }
    ip6.arpa:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . 10.96.0.10 {
                force_tcp
        }
        prometheus :9253
        }
    .:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . /etc/resolv.conf {
                force_tcp
        }
        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
    spec:
      priorityClassName: system-node-critical
      serviceAccountName: node-local-dns
      hostNetwork: true
      dnsPolicy: Default # Don't use cluster DNS.
      tolerations:
        - key: "CriticalAddonsOnly"
          operator: "Exists"
      containers:
        - name: node-cache
          image: k8s.gcr.io/k8s-dns-node-cache:1.15.7
          resources:
            requests:
              cpu: 25m
              memory: 5Mi
          args:
            [
              "-localip",
              "169.254.20.10,10.96.0.10",
              "-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: 169.254.20.10
              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

經過 yaml 能夠看到幾個細節:

  1. 部署類型是使用的DaemonSet,即在每一個 k8s node 節點上運行一個 dns 服務
  2. hostNetwork屬性爲true,即直接使用 node 物理機的網卡進行端口綁定,這樣在此 node 節點中的 pod 能夠直接訪問 dns 服務,不經過 service 進行轉發,也就不會有 DNAT
  3. dnsPolicy屬性爲Default,不使用 cluster DNS,在解析外網域名時直接使用本地的 DNS 設置
  4. 綁定在 node 節點169.254.20.1010.96.0.10IP 上,這樣節點下面的 pod 只須要將 dns 設置爲169.254.20.10便可直接訪問宿主機上的 dns 服務。

測試

  1. 修改/etc/resolv.conf文件中的 nameserver:
    nameserver 169.254.20.10
  2. 進行壓測:
    # 200個併發,持續30秒,記錄超過5s的請求個數 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
    結果以下:

結論

經過壓測發現並無解決超時的問題,按理說沒有conntrack衝突應該表現出的狀況與方案(二)相似纔對,也多是我使用的姿式不對,不過雖然這個問題還存在,可是經過DaemonSet將 dns 請求壓力分散到各個 node 節點,也能夠有效的緩解域名解析超時問題。

實施

  • 方案(一):經過修改 pod 的 template.spec.dnsConfig 來設置,並將dnsPolicy設置爲None
template:
  spec:
    dnsConfig:
      nameservers:
        - 169.254.20.10
      searches:
        - public.svc.cluster.local
        - svc.cluster.local
        - cluster.local
      options:
        - name: ndots
        value: "5"
    dnsPolicy: None
  • 方案(二):修改默認的cluster-dns,在 node 節點上將/etc/systemd/system/kubelet.service.d/10-kubeadm.conf文件中的--cluster-dns參數值修改成169.254.20.10,而後重啓kubelet
systemctl restart kubelet
:配置文件路徑也多是 /etc/kubernetes/kubelet

最終解決方案

最後仍是決定使用方案(二)+方案(三)配合使用,來最大程度的優化此問題,而且將線上全部的基礎鏡像都替換爲非apline的鏡像版本,至此問題基本解決,也但願 K8S 官方能早日將此功能直接集成進去。

相關文章
相關標籤/搜索