作者:谢于宁(予栖)
前言
本议题主要介绍如何在 Kubernetes 集群中实现 DNS 故障的可观测性以及疑难问题的诊断,我会将从以下几方面展开:
- Kubernetes 集群中 DNS 如何工作?
- 常见的 DNS 故障原因
- 如何诊断 DNS 服务端异常 - CoreDNS 内置的可观测性能力
- 如何诊断客户端异常 - 基于 BPF 的客户端 DNS 异常定位
- 如何处理一起真实的 DNS 故障?
Kubernetes 中 DNS 如何工作?
Kubernetes 集群中 DNS 无处不在:
- 当你的微服务架构中一个 APP,想要跟另外一个 APP 进行通信的时候,它依赖于 DNS 做服务发现。
- 当你部署了一个分布式数据库,数据库节点之前利用 DNS 发现彼此。
- 当你的 Kubernetes 集群中日志组件、监控组件需要去跟 APIServer 进行通信、更新数据的时候,DNS 告诉它 APIServer 是哪个 IP 地址。
我们以一个 web APP 应用想要访问 database 数据库为例,表面上看起来就是一个数据库链接被建立了,但底下发生了许多事:
- 应用被配置了 database:6379 作为数据库链接串
- 应用启动后,通过 DNS Server 查询 database 的实际位置,即 IP 地址
- DNS Server 返回 database 这个 Kubernetes Service 的 IP 地址
- 应用访问 Service IP 地址,负载均衡 IPVS 模块对 IP 地址做了目的地址转换 DNAT,流量被转发到了真实的数据库 Pod IP 172.20.1.7
那么,web APP 应用是如何找到 DNS Server 的呢?我们观察 Pod 容器中 /etc/resolv.conf 文件可以发现里面配置了一个 DNS Server 的 IP 地址,172.21.0.10。这个 IP 地址其实就是集群中 kube-system 命名空间下的 kube-dns ClusterIP 服务的 IP 地址,集群中的 CoreDNS 通过这个 ClusterIP 服务提供 DNS 解析的服务。同时从图中可以看到,集群中其实部署有多个 CoreDNS 的容器副本,这些副本彼此地位相同,客户端访问这些副本时,也是通过 IPVS 做负载均衡的。
除了 IP 地址之外,/etc/resolv.conf 还通常配置有 search path,它由多个域名后缀组成。在请求一个域名时,如果域名包含的 dots(点)的数量小于 ndots,就会被拼接上 search path,组成一个完整的域名(FQDN)再发出域名查询。本例子中,web app 请求 database 时,由于 database 包含的点数量(ndots)为 0,小于设定的 ndots 5,就会被拼接上 default.svc.cluster.local 这个后缀,组成 database.default.svc.cluster.local 这个完整域名(FQDN)再发出。search path 和 ndots 的设置需要非常小心,不合理的配置可能会让应用的解析域名耗时成倍的增加。
在真实的 Kubernetes 集群中,我们的应用经常会访问一些外部域名,以 www.aliyun.com 为例,应用请求这个域名的 IP 地址的过程中,实际会依次请求 www.aliyun.com.default.svc.cluster.local、www.aliyun.com.svc.default.cluster.local、www.aliyun.com.cluster.local、www.aliyun.com,前三个域名通通不存在,CoreDNS 均会返回域名不存在(NXDOMAIN)的报错,直到最后这一次请求 CoreDNS 才会返回正确(NOERROR)的结果。
除此之外,大部分的现代应用都支持 IPv6 了,其解析的时候都会并发地请求 A 和 AAAA(IPv6)记录,因此对于一个集群外部域名,Pod 容器内往往需要花费 8 次 DNS 解析。这带来了成倍的 DNS 解析压力。
常见的 DNS 故障根因
DNS 历史悠久,协议简单可靠,但从上面我们发现 Kubernetes 中 DNS 服务发现并非那么简单:
- 爆炸半径大:几乎所有数据面、管控平面都会依赖于 DNS
- 请求链路长:作为 Kubernetes Service,访问 DNS 涉及内核 IPVS、iptables 多个模块
- 请求 QPS 高:应用本身请求域名就会非常频繁,加上 search 搜索域的设计,这个 QPS 会翻倍
我们在日常的运维和工单处理过程中,我们发现 DNS 故障经常发生,且每次的原因并不一样。
CPU 限制
CoreDNS Server 能提供的 QPS 是与 CPU 正相关的,当 CPU 明显出现瓶颈或业务高峰期来临时,CoreDNS 在响应 DNS 请求时会出现一些明显的延迟或者解析失败。因此,我们应维持副本数在一个合适的水位。一般建议每个副本的峰值 CPU 使用量小于一核,如果其在业务峰值期间占用 CPU 大于一核,建议对 CoreDNS 进行副本扩容。默认情况下,可以采用副本数和集群节点数一比八的比值来部署,即每扩容八个集群节点,增加一个 CoreDNS 副本。
Conntrack 表项限制
另外一个常见的异常是 Conntrack Table 表项满。默认情况下,Linux 中每个 TCP、UDP 类型的链接都会占用掉一个 Conntrack 表项,这个表的数目是有限制的。当业务使用短链接请求特别多的时候,很容易把表打满,报出 Conntrack table full 的异常。针对这种异常,可以扩大 Conntrack Table 或者尽可能使用一些长链接请求。除了 Conntrack 限制以外,ARP Table、Socket 数目限制都会导致类似的问题。
Conntrack 中源端口复用竞态问题
还有一些场景是由于冲突导致的。通常我们在高 QPS 场景下,客户端会复用源端口请求 DNS。比如当我们去使用 Alpine 作为容器基础镜像时,其内置的 Musl 运行库会并发地采用同一个源端口请求 A 和 AAAA 域名记录。恰巧这种并发触发了内核中 Conntrack 的 Race Condition 的缺陷,这个缺陷会导致后进来的 DNS 报文被直接丢弃。
IPVS 后端变更后,报文源端口复用导致丢包
我们从内核网络栈的设计来看,DNS 由于 UDP 的天然设计的问题,就是往往特别容易丢包。再举一个例子就是 IPVS 的场景,当集群使用 IPVS 作为负载均衡时,如果 CoreDNS 有单个副本重启、或者副本缩容了,此时如果有一个报文采用了一个几分钟前刚用过的源端口发起请求,这个报文会被丢弃,导致域名解析的延迟。以上两个源端口冲突的问题,都可以通过 Kernel 升级来解决。
CoreDNS 软件 Bug
早期版本的 CoreDNS 存在诸多问题,包含 APIServer 断连时 Panic 重启、AutoPath 插件异常退出等问题,通常会导致一些偶现的解析失败问题,但如果 APIServer 持续不能恢复可能会带来整个集群的域名完全无法解析。想要避免这些问题,建议将 CoreDNS 升级到最新的稳定版本。
CoreDNS 内置的可观测性能力
前面我们知道了 Kubernetes 集群中 DNS 故障的常见根因,如何去发现他们呢?值得庆幸的是,CoreDNS 是一款天生插件化的 DNS 服务器软件,它内置了非常丰富的可观测性能力。以下我们介绍几个常用的插件,以及使用方式。
Log
CoreDNS 每接收到一个请求并完成响应时,会打印一行包含请求域名内容、来源 IP、响应状态码在内的日志,这一行日志类似于 Nginx 的 Access Log,可以帮我们我们快速地定位一些问题发生的位置,完成问题的整个定界过程。
此外我们可以将日志上传到云端,做日志持久化、绘出趋势图等等,甚至我们可以做一些域名访问的审计,例如识别到集群内某一个 Pod 访问了非法的域名等等。在右图的这个例子中采用了 SLS 日志服务的仪表盘,可以将每天的趋势,CoreDNS 响应状态码的以可视化的方式展示出来,方便问题的分析。当然,除了可视化以外,日志仪表盘支持告警规则配置[1]。
Dump
Dump 插件和 Log 插件类似,也会打印一行日志,差异在于其会在 CoreDNS 接收到客户端请求的时候就开始打印。如果你确认解析请求到了 CoreDNS 但 Log 日志没有输出时,可以打开 Dump 插件排查。
Debug
Debug 插件顾名思义就是用于排错。当网络链路异常或上游 DNS 服务器返回异常时,CoreDNS 可能会收到一个异常的报文,此时 Debug 插件就可以将报文以完整 16 进展的方式打印出来。后期就可以用 Wireshark 之类的工具分析这个报文,看下报文在哪里出了问题。
DNSTap
DNSTap 是一个非常有意思的插件。DNSTap 本身是一种灵活的二进制日志的协议,专门为 DNS 报文所设计的。左图中共有四条报文链路:
- 客户端请求 CoreDNS
- CoreDNS 请求上游 DNS 服务器
- 上游 DNS 服务器响应 CoreDNS
- CoreDNS 响应客户端
这四条报文均可以被 CoreDNS dnstap 插件以该二进制日志的方式投递到远程的 dnstap server 中,dnstap server 可以完成存储、分析、上报异常等动作。dnstap 在发送这些报文的过程中采用了异步 IO 设计,避免了本地磁盘日志写入和后续处理过程中报文反序列化流程,可以实现低性能开销、高效的报文采集和异常诊断流程。
那么我们如何在 dnstap server 中实现报文的异常诊断呢?dnstap server 是可以解析出原始的 DNS 报文的,从图中的 DNS 报文的 RCODE 状态码字段可以提取出 DNS 解析请求的响应状态码。不同的状态码提现了不同的异常类型,在实际问题排查过程中可以根据右表进行问题定位。
除了报文本身的 RCODE 字段外,我们可以根据 dnstap 报文的 Message Type 做一些异常判断。当一个相同的 DNS Message ID 的报文仅产生了 ClientQuery 和 ForwarderQuery 时,说明上游并没有响应该 DNS 请求,CoreDNS 也没有响应给客户端。针对这样不同 Message Type 出现的组合,可以诊断出不同的场景。我们利用这个思路,在阿里云 ACK 也提供了一个开箱即用的方案,可以快速的做一些问题的定界[2]。
Trace
Trace 插件实现了 OpenTracing 的能力,可以将一个请求从接收,到逐个插件何时开始处理、何时结束处理,都记录在一个外部的 OpenTracing 服务器中。通常我们怀疑某个 DNS 请求耗时太久,想要去定位延迟点位、优化耗时的话,可以使用这个插件快速发现瓶颈所在。
Prometheus
Prometheus 插件就提供了暴露 CoreDNS 服务端侧各种运行指标的能力。通常我们需要将 metrics 接口配置到 Prometheus 集群的采集对象列表中,每隔一定时间,Prometheus 就可以将 CoreDNS 的数据取走并存到时序数据库中。利用 Grafana 仪表盘可以将这些指标展示出来,我们可以从图表中观察到不同 RCODE 状态码的返回趋势、请求的 QPS 趋势等。针对这些趋势设置合理的阈值就可以做实时告警了。右图时在阿里云 ACK 集群使用 ARMS Prometheus 服务默认配置的 CoreDNS 监控仪表盘示例。
基于 BPF 的客户端 DNS 异常定位
很多 DNS 异常发生在客户端这一侧,CoreDNS 根本没有接收到请求。这类问题根因通常会较难定位,除了观察内核日志、CPU 负载情况等等,我们还经常使用 tcpdump 抓包的方式分析报文的流向,在哪个地方产生了丢失。当一个 DNS 异常发生的概率比较低,tcpdump 抓包这种方式的成本就变得非常高。
好消息是,我们可以利用 BPF 工具去观测一些常见的内核侧的报文异常。图中是一个 BPF 示例工具 trace_dns_drops.bt[3],这个工具启动后会监测常见的两个 DNS 报文丢失的内核函数,根据函数的返回值判断该报文是否会丢失,如果丢失则打印出当前访问的报文源 IP 源端口。在这个例子里,我们模拟了 IPVS 后端变更时源端口冲突导致的报文丢失和错误的 iptables 把 DNS 报文丢弃的两个场景。除了源端口和源 IP 以外,我们也可以再对工具做一些改写,从报文内提取 DNS 报文的字段,例如记录类型、请求的域名等。
BPF 技术可以帮我们精准定位一个报文在内核的流转流程,通过提取时间、报文五元组、经过的函数,就可以知晓报文具体的丢包原因,继而进一步优化配置。当前开源社区也有一些非常好用的工具可以辅助定位,例如 BCC 的 gethostlatency.py、Cilium 的 pwru 项目等。
如何应对一个 DNS 故障
上面我们介绍了如何观测 DNS 服务端或客户端侧的异常,但当一个真实的线上环境中遇到了 DNS 故障,我们应该怎么处理呢?
明确问题
- 不要过早地下定论:Kubernetes 中 DNS 解析链路很长,不要盲目地认定某一侧出了问题
- 明确报错原因:不同客户端在 DNS 异常时有不同类型的报错,有时候是连不上 DNS 服务器,有时候是连上了 DNS 服务器但域名不存在
- 域名是否拼写正确,Pod DNS 配置和 CoreDNS 配置是否符合预期
- 最近有没有做过任何变更,比如扩容机器了以后,新的机器不一定在一个安全组
收集信息
- 向客户端、客户端所在集群节点、CoreDNS 所在集群节点、CoreDNS 服务端要日志,看异常时间内请求是否有相关明显异常的日志
- 查询 Kubernetes 事件、集群节点负载监控等等,有无异常事件、异常流量峰值等
验证猜想
- 从客户端、客户端所在集群节点、同集群节点其它容器分别手动发起 dig 测试,可以测试到 kube-dns ClusterIP 的 IP、或者不同 CoreDNS 副本的 IP,验证网络上有无问题
- 启用 CoreDNS 的可观测性插件,或是运行 BPF、tcpdump 等工具,验证报文是否异常
修复问题
- 针对找到的根因,寻求解决方案
- 逐步地执行修复,并做好回滚措施
- 执行验证问题恢复的测试流程,通过各类仪表盘全面地观察修复效果
以上步骤中,从收集完信息到验证猜想之间,我们往往需要问自己几个问题,例如:
- 异常出现的频次如何?一直出现/高峰时期/时不时的
- 异常出现的范围如何?整个集群/固定的几个 Pod/随机 Pod/随机节点上任意 Pod
这些问题的答案指向了非常多可能的异常原因。我们在阿里云 ACK 产品文档中提供了一系列 Kubernetes 域名解析异常问题排查[4]的文档,可供辅助排查。
总结
本文介绍了 CoreDNS 服务器、客户端侧的常见 DNS 异常、故障根因,异常观测方案和故障处理流程,希望对大家的问题诊断有所帮助。DNS 服务对于 Kubernetes 集群是至关重要的,除了观测异常之外,我们在架构设计之初就应充分考虑 DNS 服务的稳定性,采纳一些例如 DNS 本地缓存之类的最佳实践。谢谢大家!
参考文档
[1] DNS 最佳实践
https://help.aliyun.com/document_detail/172339.html
[2] 本议题 PPT 下载地址
[3] Linux /etc/resolv.conf 配置文档
https://man7.org/linux/man-pages/man5/resolv.conf.5.html
相关链接
[1]日志仪表盘
https://help.aliyun.com/document_detail/213461.html
[2]问题的定界
https://help.aliyun.com/document_detail/268638.html
[3]BPF 示例工具 trace_dns_drops.bt
https://gist.github.com/xh4n3/61d8081b834d7e21bff723614e07777c
[4]异常问题排查
https://help.aliyun.com/document_detail/404754.html
点击文末“此处”,即可查看容器服务 ACK 产品详情!
#云原生与云未来的新可能#
复制并前往下方链接,即可免费下载电子书
https://developer.aliyun.com/topic/download?id=8265