一、问题背景
DNS是Kubernetes集群中非常重要的基础服务,在客户端设置不合理、集群规模较大等情况下比较容易出现解析超时、解析失败等现象,严重时可能会对业务造成相关影响。
尤其在节点异常宕机,或集群整体负载、coredns所分布在的节点负载较大时,该情况会非常常见,我们不定期会收到相关解析异常报警和业务线同学反馈,虽然每次持续时间较短,但在高峰期频繁出现也非常折磨。经过多种方案对比考量,我们最终决定对原生DNS架构做下改动:
将原架构集中请求pod→kube-dns svc→coredns pod的方式改造为:各node节点上的pod首选请求所在node本地的dns服务(后续称为q-dnsmasq),在q-dnsmasq服务不可用时再去请求kube-svc;
并将q-dnsmasq启用all-servers模式,让其识别请求域名是K8S内部(如cluster.local)还是外部,内部则同时转发多个Coredns并取最快响应结果;
如为K8S外部域名则在转发请求至公司dns-server时,也通过并发的方式同时请求3台公司dns-server(后续称为localdns),取最快响应结果;并启用缓存。通过此来实现较高效率、较大限度的可用性。
二、方案对比
原生方案
K8S原生方案为coredns deployment,并通过kube-dns service转发至endpoints列表中的某个coredns pod进行解析,相关不足如下:
-
故障域较大,牵一发动全身(虽然节点较多、dns实例也较多,并有配置自动扩缩,但访问链路上是pod→ svc→某coredns pod的。这种在出现单点异常时,高并发量下可能很多pod在瞬时仍会请求到该dnsserver上,错误感知性较强。)
-
DNS策略默认为ClusterFirst,并且ClusterDNS默认只有一个kube-dns这个svc的IP地址(kubelet config中可修改,支持多个)。因仅指向一个kube-svc,出现问题时需要应用具备重试能力,以访问其他endpoints;填多个的情况下,在第一个不响应至超时请求下一个nameserver时,也会有一定时间(默认5s),多数业务请求是不能够容忍的。
-
缓存命中率较低等(因访问链路为pod → svc → 某coredns pod,在比较大的coredns服务器规模下,缓存命中率有限)。
改造方案
改造后k8s dns架构图
释意:
- IDC Pod,即我们本地机房的Pod。
- Cloud Pod,即我们弹性到公有云上的Pod(此次改造方案并不涉及云上Pod解析链路变化,会在后续进行改造)。
- q-dnsmasq,在每个宿主都有运行,改造后的kube-dns方案,业务Pod首选DNS即为该服务,监听地址为HostIP:53。
- kube-dns svc,即coredns的service,不过存在变化的是该svc监听udp/tcp:53,但targetPort为xx53。
- Coredns,为了将K8S内外请求全都拆分为节点级别完成,将其由Deployment改造为了DaemonSet,同时因Pod IP非固化,虽然有方案但对有新增节点时也并不灵活,所以最终选择将其改造为hostNetwork模式,监听HostIP:xx53(53为q-dnsmasq监听)。
- LocalDNS,公司的DNS服务器,每机房各N台。
解析链路简述
-
在Pod内
正常链路:q-dnsmasq存活可正常提供解析时
K8S内部域名:IDC Pod → q-dnsmasq(cache) → Coredns:xx53 & kube-dns:53
K8S外部域名:IDC Pod → q-dnsmasq(cache) → All LocalDNS:53
异常链路:q-dnsmasq挂掉不可正常提供解析时
K8S内部域名:IDC Pod → kube-dns:53 → Coredns:xx53(cache)
K8S外部域名:IDC Pod → kube-dns:53 → Coredns:xx53(cache) → localdns
-
在宿主上
K8S内部域名:Node → q-dnsmasq(cache) → Coredns:xx53 & kube-dns:53
K8S外部域名:Node → q-dnsmasq(cache) → All LocalDNS:53
K8S的反解析
在q-dnsmasq上,也做了对K8S内部域名的反解析支持。
改造后方案对比原生方案的优势
-
隔离故障域:每pod访问首选DNS为所在宿主q-dnsmasq,宿主相关问题导致不可用时仅会影响当前宿主存在的pod,且会存在第二个nameserver进行兜底,在q-dnsmasq挂掉时由kube-svc(coredns)进行服务。将可能存在的应用大面积解析问题控制在应用单点解析问题,便于快速隔离或做自动隔离(后续K8S单点自愈场景会支持该问题场景)。
-
提升解析效率:q-dnsmasq收到请求会并发请求多个coredns或多个localdns,取最快响应结果,在提升效率的同时,也提供了较高的对localdns的故障容忍高可用性。
-
提升缓存命中率,不再分散通过svc 分配处理请求的dns,每个pod访问的首选dns可做到按节点分散、并确保pod二次请求解析访问的dns一致性,提升缓存命中率。
-
两条解析链路,最多2级,提供降级保障的同时,不影响可追溯性。
5. 全局具备首次选nameserver,并均为并发请求多nameserver。
-
pod resolv,首选q-dnsmasq,次选kube-dns
-
pod解析K8S内部域名, IDC Pod → q-dnsmasq(cache) → Coredns:xx53 & kube-dns:53
-
pod解析K8S外部域名, IDC Pod → q-dnsmasq(cache) → All LocalDNS:53
-
-
宿主 resolv,首选q-dnsmasq,次选2个localdns
-
宿主解析K8S内部域名,Node → q-dnsmasq(cache) → Coredns:xx53 & kube-dns:53
-
宿主解析K8S外部域名,Node → q-dnsmasq(cache) → All LocalDNS:53
-
为什么会选用DNSMasq?
在方案选型过程中,有关注到过Nodelocal DNSCache方案, 它的做法简单来说是通过在集群中运行一个DaemonSet CoreDNS运行DNS缓存代理,以此来到达提高集群DNS性能和稳定性;
但最终未使用该方案而采用DnsMasq的原因主要如下:
-
公司服务器节点(物理机或KVM等),在装机时均有安装q-dnsmasq,是在容器化之前就存在、用于缓解Localdns压力。
-
dnsmasq支持将所有 DNS 查询发送给所有配置的DNS服务器(--all-servers),可以很大提高 DNS 查询的性能以及可靠性。
-
相比较更符合业务需求,能够确保快速且可靠地得到响应。
三、测试记录
配置展示
示例信息注明,如下信息均为虚拟
Cluster : abc
Cluster Domain:abc.k8s.xxx.qunar.com
1. q-dnsmasq
2.Node resolv.conf
3.Kubelet config
Pod 使用测试
测试场景
-
将测试用pod调度至调整后的节点,并确认Dns policy: ClusterFirst。
-
进入pod内,查看其/etc/resolv.conf是否符合预期(kubelet clusterDNS指定的2个dns地址)。
-
pod内部,使用dig工具进行测试,并在宿主节点查看q-dnsmasq解析日志,同时开始抓包查看走向,确保解析链路符合预期。
-
集群外部域名: www.qunar.com
-
集群内部域名:
kubernetes.default.svc.abc.k8s.xxx.qunar.com
-
集群内部域名(search domain):
kubernetes.default
kubernetes.default.svc
${svc} # 当前namespace下
- 宿主节点,使用dig工具进行测试。
-
集群外部域名: www.qunar.com
-
集群内部域名:
kubernetes.default.svc.abc.k8s.xxx.qunar.com
测试过程
因信息敏感性与篇幅限制,实测记录相关信息不再贴出,简述整体过程;测试场景要覆盖如下:
- Pod - 集群外部域名
- Pod - 集群内部域名
- Pod - 内外域名+search
- Dnsmasq 缓存有效性
1. 配置确认
检查改造节点的如下配置文件是否修改符合预期。
- /etc/q-dnsmasq.conf
- /etc/dnsmasq.d/q-ns.conf
- /etc/q-kubedns.server
2. Pod调度
确认测试的Pod通过nodeName或affinity等方式定向调度至了"改造后的目标节点"。
3. Pod内环境确认
确认/etc/resolv.conf配置文件是否符合预期。
4. 集群外部域名测试
4.1 域名解析
Pod内dig www.qunar.com确认是否可正常解析。
4.2 抓包记录
在"改造后的目标节点"通过抓包观察,确认链路为如下走向:
- pod ip → Node q-dnsmasq → localdns allserver
4.3 解析日志
如果开启了dnsmasq的log-queries,也可以通过其日志,确认链路为如下走向:
- pod → Node q-dnsmasq → localdns allserver
5. 集群内部域名测试
参照[ 4. 集群外部域名测试 ]过程,正常解析链路表现应当如下:
- pod → Node q-dnsmasq → Coredns:xx53 & kube-dns:53
6. 缓存测试
30s内连续dig两次,关注第二次是否直接由q-dnsmasq返回结果即可。
7. 宿主节点测试
K8S内部域名
- 非完整fqdn - UnknownHost(宿主resolv.conf中,不存在K8S相关search domain)
- 完整fqdn - 正常解析(node → node q-dnsmasq → coredns:xx53 & kube-dns:53)
K8S外部域名
- 正常解析(node → Node q-dnsmasq → localdns allserver)
8. q-dnsmasq/coredns pod 故障测试
- pod ip → node q-dnsmasq → 异常,转而请求 → kube-dns svc
- node → node q-dnsmasq → 异常,转而请求localdns-1
在这里我们环境有些特殊,之前为了方便可直接使用svc(Qunar的容器网络是打通的,pod ip/svc均可直接通信)访问相关应用,我们在公司localdns服务器上做了forward,它会将.abc.k8s.xxx.qunar.com全部转发至对应集群的kube-dns。*
*也就是说,在node上执行dig时,哪怕q-dnsmasq挂掉了直接请求到了localdns上,也是可以正常提供解析的;这个场景应对多数公司都用不到,可以忽略。
8.1 停止q-dnsmasq
8.2 pod内分别测试内、外部域名
四、上线方案
配置q-dnsmasq
配置Kubelet
截止此处,Pod的首选DNS已经指向为q-dnsmasq,调度到该节点上的Pod不会再去访问kube-svc进行解析。
配置kube-dns
1. 准备Coredns Daemonset
1.1 创建新的Coredns ConfigMap - coredns-2, 端口配置为xx53
1.2 创建Coredns DaemonSet
- 确认引用的configmap为coredns-2(改配置coredns启动为xx53端口)
- 确认启用了hostNetwork: true
- 也可以保守些,通过nodeName: xxx-node 指定仅部署1个进行测试,确认无问题再放开调度
2. 测试加入Coredns Daemonset实例后的kube-dns svc是否正常
2.1 小结-观察确认
变更过程中,中间态的必要理解:
-
kube-dns svc下的endpoints列表,会新增coredns daemonset的实例(为hostip)。
-
kube-dns svc的port为53,转发endpoints的目标端口也为53(这里理解下,新增进去的coredns daemonset端口为xx53,理论上来讲其实转发到coredns daemosnet 53端口时,因coredns daemonset pod未监听53端口,应当是要出问题的;但是并不会。因coredns daemonset为hostip,虽然coredns daemonset pod未监听53端口,但所在节点上的q-dnsmasq是监听着53端口的)。
所以,这里的新增coredns daemonset pod加入kube-dns svc的endpoints列表后不受影响的详细,如下图所示:
kube-dns svc: ${kube-dns-svc_IP]
kube-dns endpoints: pod-1_ip:53,pod-2_ip:53,...,host-1_ip:53,host-2_ip:53,...
3. 将Coredns Daemonset投入使用
缩容cordons deployment
下线coredns deployment
- 缩容deployment creodns 副本数为0,将其进行下线;
- 缩容时,kube-dns svc会自动将deployment corends实例从endpoints移除,不会再去请求到这些实例上,只会请求daemonset coredns:53(实际上为q-dnsmasq),如此完成Coredns Deployment → Coredns DaemonSet的转换。
缩容coredns deployment
$ sudo kubectl scale --replicas=0 deployment -n kube-system coredns |
|---|
修改kube-svc service
- kube-dns svc tcp port存在不可修改情况,需要recreate才能成功修改生效。
- 在kube-dns svc修改完成后,此时,直接访问kube-dns svc的链路,将会由变更时中间态(pod → kube-dns svc:53 → q-dnsmasq:53)修正为 pod → kube-dns svc:53 → coredns daemonset pod:xx53(不再是q-dnsmasq)。
- 至此,coredns deployment将彻底完成下线,整个架构转换彻底完成。
4. 调整Coredns配置
在如上已经完成了变更。该步骤可忽略,此处为为了方便统一进行的额外调整。
五、总结与后续规划
文章介绍了在Qunar的业务需求场景下,kube-dns调整方案的优势以及原生k8s dns方案的不足,并提供可实践、可复刻的方案,以及测试验收、无损上线方式。
希望对在大规模场景下kube-dns,对解析有较大依赖性的团队能够提供一种优化思路或优化方案候选项。
另外,Qunar是有使用到公有云作为弹性资源的,DNS解析相关问题在云上也会存在,因公有云virtual-kubelet并不允许Daemonset调度,所以文中方案对公有云并不适用,但我们对云上环境也已有相关解决思路,在Q3会逐步验证和实施:
- 封装q-dnsmasq镜像,由初始化脚本读取Pod Annotation获取自己所在机房,来完成各机房弹云后的转发差异化配置。
- 在调度至云上时,通过对调度至公有云节点的Pod进行Sidecar注入dnsmasq容器,以及dnsConfig配置改写云上Pod的首选DNS为127.0.0.1、次选对应集群的kube-dns svc来实现。