近期線上 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
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 的延時。
經過resolv.conf
的use-vc
選項來開啓 TCP 協議緩存
/etc/resolv.conf
文件,在最後加入一行文本:
確實沒有出現5s
的超時問題了,可是部分請求耗時仍是比較高,在4s
左右,並且平均耗時比 UPD 協議的還高,效果並很差。併發
經過resolv.conf
的single-request-reopen
和single-request
選項來避免:
/etc/resolv.conf
文件,在最後加入一行文本:
/etc/resolv.conf
文件,在最後加入一行文本:
經過壓測結果能夠看到single-request-reopen
和single-request
選項確實能夠顯著的下降域名解析耗時。
其實就是要給容器的/etc/resolv.conf
文件添加選項,目前有兩個方案比較合適:
lifecycle: postStart: exec: command: - /bin/sh - -c - "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"
template: spec: dnsConfig: options: - name: single-request-reopen
注
: 須要 k8s 版本>=1.9
不支持alpine
基礎鏡像的容器,由於apline
底層使用的musl libc
庫並不支持這些 resolv.conf 選項,因此若是使用alpine
基礎鏡像構建的應用,仍是沒法規避超時的問題。
其實 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 衝突)
kube-dns service
的 clusterIP# kubectl -n kube-system get svc kube-dns -o jsonpath="{.spec.clusterIP}" 10.96.0.10
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
# 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 能夠看到幾個細節:
DaemonSet
,即在每一個 k8s node 節點上運行一個 dns 服務hostNetwork
屬性爲true
,即直接使用 node 物理機的網卡進行端口綁定,這樣在此 node 節點中的 pod 能夠直接訪問 dns 服務,不經過 service 進行轉發,也就不會有 DNATdnsPolicy
屬性爲Default
,不使用 cluster DNS,在解析外網域名時直接使用本地的 DNS 設置169.254.20.10
和10.96.0.10
IP 上,這樣節點下面的 pod 只須要將 dns 設置爲169.254.20.10
便可直接訪問宿主機上的 dns 服務。/etc/resolv.conf
文件中的 nameserver:
經過壓測發現並無解決超時的問題,按理說沒有conntrack
衝突應該表現出的狀況與方案(二)相似纔對,也多是我使用的姿式不對,不過雖然這個問題還存在,可是經過DaemonSet
將 dns 請求壓力分散到各個 node 節點,也能夠有效的緩解域名解析超時問題。
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 官方能早日將此功能直接集成進去。