K8S内部服务调⽤域名解析超时
前⾔
近期线上 k8s 时不时就会出现⼀些内部服务间的调⽤超时问题,通过⽇志可以得知超时的原因都是出现在域名解析上,并且都是 k8s 内部的域名解析超时,于是直接先将内部域名替换成 k8s service 的 IP,观察⼀段时间发现没有超时的情况发⽣了,但是由于使⽤ service IP 不是长久之计,所以还要去解决办法。
复现
⼀开始运维同事在调⽤⽅ pod 中使⽤ab⼯具对⽬标服务进⾏了多次压测,并没有发现有超时的请求,我介⼊之后分析ab这类 http 压测⼯具应该都会有 dns 缓存,⽽我们主要是要测试 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", "-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 容器中进⾏压测:
# 200个并发,持续30秒
./dns -host {service}.{namespace} -c 200 -d 30
这次可以发现最⼤耗时有5s多,多次测试结果都是类似:
⽽我们内部服务间 HTTP 调⽤的超时⼀般都是设置在3s左右,以此推断出与线上的超时情况应该是同⼀种情况,在并发⾼的情况下会出现部分域名解析超时⽽导致 HTTP 请求失败。
原因
起初⼀直以为是coredns的问题,于是运维升级了下coredns版本再进⾏压测,发现问题还是存在,说明不是版本的问题,难道是coredns本⾝的性能就差导致的?想想也不太可能啊,才 200 的并发就顶不住了那性能也未免太弱了吧,结合之前的压测数据,平均响应都挺正常的(82ms),但是就有个别请求会延迟,⽽且都是 5 秒左右,所以就⼜带着k8s dns 5s的关键字去 google 搜了⼀下,这不搜不知道⼀搜吓⼀跳啊,原来是 k8s ⾥的⼀个⼤坑啊(其实和 k8s 没有太⼤的关系,只是 k8s 层⾯没有提供解决⽅案)。
5s 超时原因
linux 中glibc的 resolver 的缺省超时时间是 5s,⽽导致超时的原因是内核conntrack模块的 bug。
Weave works 的⼯程师 Martynas Pumputis 对这个问题做了很详细的分析:
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 请求
通过f的use-vc选项来开启 TCP 协议
测试
1. 修改/f⽂件,在最后加⼊⼀⾏⽂本:
options use-vc
2. 进⾏压测:
# 200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
结果如下:
结论
确实没有出现5s的超时问题了,但是部分请求耗时还是⽐较⾼,在4s左右,⽽且平均耗时⽐ UPD 协议的还⾼,效果并不好。
⽅案(⼆):避免相同五元组 DNS 请求的并发
通过f的single-request-reopen和single-request选项来避免:
single-request-reopen (glibc>=2.9) 发送 A 类型请求和 AAAA 类型请求使⽤不同的源端⼝。这样两个请
求在 conntrack 表中不占⽤同⼀
个表项,从⽽避免冲突。
single-request (glibc>=2.10) 避免并发,改为串⾏发送 A 类型和 AAAA 类型请求,没有了并发,从⽽也避免了冲突。
测试 single-request-reopen
1. 修改/f⽂件,在最后加⼊⼀⾏⽂本:
options single-request-reopen
2. 进⾏压测:
# 200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
结果如下:
测试 single-request
1. 修改/f⽂件,在最后加⼊⼀⾏⽂本:
options single-request
2. 进⾏压测:
# 200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
结果如下:
结论
通过压测结果可以看到single-request-reopen和single-request选项确实可以显著的降低域名解析耗时。
关于⽅案(⼀)和⽅案(⼆)的实施步骤和缺点
实施步骤
其实就是要给容器的/f⽂件添加选项,⽬前有两个⽅案⽐较合适:
1. 通过修改 pod 的 postStart hook 来设置
lifecycle:
postStart:
exec:
command:
- /bin/sh
- -c
- "/bin/echo 'options single-request-reopen' >> /f"
1. 通过修改 pod 的 template.spec.dnsConfig 来设置
template:
spec:
dnsConfig:
options:
- name: single-request-reopen
注: 需要 k8s 版本>=1.9
缺点
不⽀持alpine基础镜像的容器,因为apline底层使⽤的musl libc库并不⽀持这些 f 选项,所以如果使⽤alpine基础镜像构建的应⽤,还是⽆法规避超时的问题。
⽅案(三):本地 DNS 缓存
⼤概原理就是:
本地 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 "github/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\/f/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
#
# /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 {nodeselector
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 . /f {
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: 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
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论