去哪儿 Kube DNS 架构优化
一、问题背景
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 进行解析,相关不足如下:
1. 故障域较大,牵一发动全身(虽然节点较多、dns 实例也较多,并有配置自动扩缩,但访问链路上是 pod→ svc→某 coredns pod 的。这种在出现单点异常时,高并发量下可能很多 pod 在瞬时仍会请求到该 dnsserver 上,错误感知性较强。)
2. DNS 策略默认为 ClusterFirst,并且 ClusterDNS 默认只有一个 kube-dns 这个 svc 的 IP 地址(kubelet config 中可修改,支持多个)。因仅指向一个 kube-svc,出现问题时需要应用具备重试能力,以访问其他 endpoints;填多个的情况下,在第一个不响应至超时请求下一个 nameserver 时,也会有一定时间(默认 5s),多数业务请求是不能够容忍的。
3. 缓存命中率较低等(因访问链路为 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 内部域名的反解析支持。
改造后方案对比原生方案的优势
1. 隔离故障域:每 pod 访问首选 DNS 为所在宿主 q-dnsmasq,宿主相关问题导致不可用时仅会影响当前宿主存在的 pod,且会存在第二个 nameserver 进行兜底,在 q-dnsmasq 挂掉时由 kube-svc(coredns)进行服务。将可能存在的应用大面积解析问题控制在应用单点解析问题,便于快速隔离或做自动隔离(后续 K8S 单点自愈场景会支持该问题场景)。
2. 提升解析效率:q-dnsmasq 收到请求会并发请求多个 coredns 或多个 localdns,取最快响应结果,在提升效率的同时,也提供了较高的对 localdns 的故障容忍高可用性。
3. 提升缓存命中率,不再分散通过 svc 分配处理请求的 dns,每个 pod 访问的首选 dns 可做到按节点分散、并确保 pod 二次请求解析访问的 dns 一致性,提升缓存命中率。
4. 两条解析链路,最多 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 的原因主要如下:
1. 公司服务器节点(物理机或 KVM 等),在装机时均有安装 q-dnsmasq,是在容器化之前就存在、用于缓解 Localdns 压力。
2. dnsmasq 支持将所有 DNS 查询发送给所有配置的 DNS 服务器(--all-servers),可以很大提高 DNS 查询的性能以及可靠性。
3. 相比较更符合业务需求,能够确保快速且可靠地得到响应。
三、测试记录
配置展示
示例信息注明,如下信息均为虚拟
Cluster : abc
Cluster Domain:abc.k8s.xxx.qunar.com
1. q-dnsmasq
2.Node resolv.conf
3.Kubelet config
Pod 使用测试
测试场景
1. 将测试用 pod 调度至调整后的节点,并确认 Dns policy: ClusterFirst。
2. 进入 pod 内,查看其/etc/resolv.conf 是否符合预期(kubelet clusterDNS 指定的 2 个 dns 地址)。
3. pod 内部,使用 dig 工具进行测试,并在宿主节点查看 q-dnsmasq 解析日志,同时开始抓包查看走向,确保解析链路符合预期。
集群外部域名: www.qunar.com
集群内部域名:
kubernetes.default.svc.abc.k8s.xxx.qunar.com
集群内部域名(search domain):
kubernetes.default
kubernetes.default.svc
${svc} # 当前 namespace 下
4. 宿主节点,使用 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 小结-观察确认
变更过程中,中间态的必要理解:
1. kube-dns svc 下的 endpoints 列表,会新增 coredns daemonset 的实例(为 hostip)。
2. 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 来实现。
版权声明: 本文为 InfoQ 作者【Qunar技术沙龙】的原创文章。
原文链接:【http://xie.infoq.cn/article/cba9b95d21f6f459e02c93efa】。文章转载请联系作者。
评论