本文整理自灵雀云的专家工程师刘梦馨,在《蓝鲸 X DeepFlow 可观测性 Meetup》 中的分享实录,从一个毫无头绪的 K8s DNS 故障出发,分享问题的排查思路,详解排查过程中遇到的 DNS 服务、Alpine 镜像、业务代码逻辑、CNI 插件等各个层面的异常现象。整个排查过程基于 DeepFlow 的持续观测能力,实现了对故障现场的高清还原。刘老师同时也从资深用户的角度,对 DeepFlow Dashboard 提出了宝贵的易用性改善建议。
我主要是负责我们这边(灵雀云)容器网络的事情,我们有一个开源项目叫 Kube-OVN,可能有的人知道,但我今天不讲那块儿,做容器网络的话,会知道名义上我们是开发,但是可能一多半的时间都在排查问题。今天的话我就给大家介绍一下,我们利用 DeepFlow 来帮助我们排查了一个比较困难、困扰我们比较长时间问题的一个案例,希望对大家有一些启发。
我讲的过程主要就分为这四个部分,第一个部分是介绍一下我们这个故障具体的场景是什么样,第二步是我们的过程中不断地迭代的排查过程,然后我们得到的一些优化方案,还有最后我们的一些思考和总结。
故障场景
我先简单介绍一下这个故障的场景,在每天晚上,我们的整个软件系统会跑一个集中性的自动化的测试,在这个自动化的测试中,有一部分是跑 DevOps 的 CI,在跑 CI 的时候会很偶发地发生一个请求超时的问题,但是它这个请求超时应用侧并没有报一些额外的错误,它只报了一个请求超时的错误。它是一个 git clone 的一个操作,git clone 一个代码仓库,返回的一个是 Couldn’t resolve XXX,然后 Request timeout 这样的一个问题。有时候是域名解析失败,有的时候是超时,都会有,但是这个问题困难的一个地方在于它是没有办法稳定复现的,可能一周的话就出现那么两三次,而且每次的都是凌晨跑,可能三四点会出现这样一个错误,可能等一周没有,等一周突然又有了是这样的一个情况。还有一个比较让我们难以排查的原因是因为我们都是一个纯容器化的(环境),它 CI 很多都是 Job 类型的一个应用的场景,并没有一个长久的现场给我们来用, Job 跑完了、失败了就直接退出了,等我们第二天早上起来发现这个问题再去想去排查的话,就没有任何的线索了。
从应用侧留下来的线索就是第一行的一句话,剩下的我们就什么都没有了,对我们来说是很头疼的,因为负责 CI 的团队经常跟我们抱怨,他们的 CI 失败了测试会报他们的故障,他们发现是网络的故障就报到我们这边说是我们的故障,我们又说你们又没有给我现场怎么查?就来回扯皮,这个事情就扯了很长的时间,最后解决的过程,就是这个事。
就这么一个 git clone 失败的事情,前后我们排查了大概 3 个月的时间,这么简单的一个错误的背后,其实有很多很多的各种各样的坑,各种各样的问题都会引发这个问题。在这个过程中,我们除了这一句话什么都没有了,我们就开始找一些第三方的一些工具来帮助我们去协查这个问题。当时我们就尝试去使用 DeepFlow 来帮我们去做这个问题的协查,我第一次用 DeepFlow 的时候会发现 DeepFlow 有很多的面板,可能我不需要做什么,装好了之后都可以看那个面板就有什么 DNS 的记录,有 Request 的记录,有 Flow 的记录,所以我希望通过这个东西能帮助我们去排查问题。
这是在我们环境里第一次部署 DeepFlow,然后看 DNS 的一个面板,其实看 DNS (的 dashboard ) 我是蒙的,因为第一眼他告诉我这个 Client Error 是有 58%,我这个环境里其实没有什么问题的,是我一个测试的环境,刚把它部署上去,我想看一下这个面板怎么用,它就告诉我这个环境里超过一半的 DNS 请求都是错误的,当时直接就把我给打蒙了。
我给大家简单说一下,这是 K8s 里面一个机制导致的问题,这个问题导致了接下来我们很多的问题,导致我们的排查都变得很困难。比如说我们这个从客户端它是一个简单的 git clone 一个域名,在 K8s 里面它其实是要做很多的事情的,因为 K8s 自己有自己的服务发现的一些机制,它有一个自己的 CoreDNS,它会先尝试在集群内解析的时候,每个 Pod 在 K8s 中启动的时候,K8s 会把 Pod 的 /etc/resolve.conf 给改掉,会给它拼很多的后缀。可以看到下面这个图,kube-system.svc.cluster.local 这个是最完整的,然后可能有 .svc 的,把前面的 namespace 去掉,然后再把 svc 的去掉,然后再把集群的 cluster domain 给你拼下来。导致的结果就是容器里面去做一个 DNS 请求,而且如果你很不幸请求的是一个外网的,因为我们访问是个外网的仓库,是需要把后面的 5 个域名全都匹配一遍,它会把这个拼成完整的域名,然后去请求,请求如果失败的话他会走下一个,失败的话再走下一个,直到最后一次失败他才会真正的去访问你的真实想访问的域名。
而且还有一个问题是他不是发一次(请求),它是每一个请求 IPV4 和 IPV6 同时发,所以它一次的话是发两个请求,把这个请求发给 CoreDNS, CoreDNS 收到这个请求之后,它会根据你的这个后缀来判断是不是做集群内解析。如果集群内解析的话,它会尝试,如果没有的话,它其实还是会往上游发。所以理论上来说,你这个前面发了 10 多次请求,CoreDNS 因为本地不匹配,所以还会再去往上游再去发 10 多次的请求。
在这个过程中,利用 DeepFlow 的面板,看 DNS (的Request Log),这不是失败的记录,而是一个成功请求的记录,在 Client 端是需要发送 12 次 DNS 请求才最后一次才能返回。Success 之前所有的它都会匹配,你可以看到它前面有一个匹配了什么, .kube-system,匹配了.svc,匹配了.cluster,匹配我集群内的后缀,都是返回的 Non-Existent 那么一个错误,他这样不断的迭代,最后终于发了我想要的请求。这样的话才解析到我最终预期得到的 IP。
K8s 的这个机制是为了方便 Service 的访问,如果你是访问一个 Service Name 的话,它一次就匹配到了,但是如果你很不幸访问的是一个外网域名的话,那就要走这样一个过程,而且这 12 次是你 Client 到 CoreDNS, CoreDNS 再往外可能还有 12 次这样的请求。所以这个 DNS 的放大是很厉害的,它会带来很多额外的资源,开销可能还好,但是它可能会有一些并发处理,这样的一些不断重试的过程,可能会带来一些很多额外的问题。
看一下这个(Dashboard 的结果),我们这个集群里面其实不全是访问外网了,大部分还是内网的,但是总体相当于有将近 60% 的这个 DNS 请求都是无效的,这是 K8s 本身的机制,会带来这样的一个问题。
这个问题其实是有一些调整的方法的,从业务侧改是最简单的方法就是可以请求完整的域名,如果在域名后面加了一个 . 进行一个补全, alauda.cn 后面再加一个 . 的话,那么 DNS 解析器就会认为是一个完整的域名,这样的话它就不会再去进行补全,就可以避免这个问题去发生。
因为你的概率降低了,你原来要可能 20 多次请求,现在只要一次就直接解析到了,我们跟业务侧的当时的反馈就是这个问题也挺难查的,又不是必现的,我也不可能天天去盯着,你先通过这个方式绕过去行不行?但是业务侧就把我们给怼了一顿,说你为什么要让我们去了解这么奇怪的一个域名的写法?而且我们有几百个这样的应用,就为了你这一个偶现的问题,我们要去做那么多的改造,而且原则上来说你们并没有排除网络侧的问题,你们没有洗清你们的怀疑的理由,就让我们去做调整,所以都被怼的比较狠。
DNS 上游网络排查
我们要做的事情就是需要进一步的去定位这个问题,看一下究竟是哪导致的这个问题。由于他是请求一个外网的域名,当时我们内网域名其实还没有发现这样的问题,所以我们怀疑是有可能是 CoreDNS 的上游解析出问题了。这个事情我们需要找的一个证据, CoreDNS 到我们的上游的 DNS 会不会有这样的一个错误?
吐槽一下 DeepFlow 的 DNS 面板,它其实是没有 CoreDNS (向上游)的解析,只有集群内(请求 CoreDNS)解析的数据。
比较好的一点就是 DNS 相当于是个专门的一个 DNS 优化过的页面,可以查 CoreDNS 的原始页面,我以应用的视角去看 CoreDNS,我直接去看它的 Flow Log 和它的 Request Log,这样的话我再去过滤UDP,再去过滤53,其实在流日志(Request Log dashbaord)里面也是可以匹配到这些 DNS 的记录的。
也不知道幸运还是不幸吧,我们第一次看这个表的时候就发现问题了,正好是在集群内解析外网失败的时候,这个集群去访问上游的时候,出现了一个失败,我们就把这个锅甩给底层的基础设施了,说这肯定是你们到上游的网络出问题了嘛。但是之后这个问题还是出现了,等再次出现的时候我们就发现访问外网的时候就没有任何的问题了,访问外网的记录都是 Success 了,但是内网去访问 alauda.cn 的时候还是出错了,基本上可以定位到外网是没有问题的,那问题怀疑是 Pod 到 CoreDNS 这个路径上出现了一些问题。
我们就在这个过程中进一步的去看(Request Log)日志,发现有一些很奇怪的一个现象,理论上来说,我们大部分的域名解析都是失败,因为他又补全了一个那个奇怪的域名是解析不到的。但是我们在有一次的失败过程中,我们把这个就 DNS 解析的日志拉出来的时候,发现那个很奇怪的现象,请求 alauda.cn 时,补全了一个错误的域名,但是不知道出现了一些什么样的情况,上游返回了一个 Success,但是返回 Success 的时候它有没有返回域名,Response 里面是空的,当天的上游返回有一个异常的现象,从 DNS 的角度,它应该返回一个 NX DOMAIN,说这个域名不存在,但他不知道那天是抽了什么风,他返回了一个就是成功,但是里面是没有任何数据记录的,我们认为你这个解析是没有这个类型的一个记录,相当于解析列表是空的,它返回了这样的一种情况。
再往上查的话,会发现这个就涉及到了一些镜像里的这个 libc 库的一个问题,我们会发现就在那个他们 CI 的那个过程中,如果是出现了这种 no data 的情况,会导致 DNS 的请求提前中断,就直接返回了一个失败,告诉你这个域名不存在。
Musl Libc 库错误处理排查
但是这个其实是跟 libc 的库很相关的一个行为,就是如果你是用 glibc 的话,就是简单跟大家说一下,如果你的应用去请求 DNS 的话,一般情况下你不会去手写那个 DNS 的,一般都是通过调一些库嘛?如果是你是 C 语言的话,基本上那个标准的 C 库是支持你的 DNS 这个调用的,但是这个标准的 C 库是有不同实现的,就是我们常用的一般都是 glibc 的这种,如果是 glibc 的话,他碰到这种 no data 的情况,会认为我没有请求到结果,我会在补全的过程继续往下走。如果是 glibc 的话,它在这种情况下是正常工作的。
但是我们的 CI 很多都是用的 Alpine 的镜像, Alpine 默认带的是 musl 这个 C 库,这个 C 库它在 DNS 处理的一个行为是如果我处理到了这种 Success,但是 Success 返回是空的话,那我就认为有错,我就提前结束了,我就不会去再往下去请求。
最终排查到最后是很诡异的一个现象,是 Alpine 的那个 libc 库的一个行为。这样的话理论上所有用 Alpine 的应用都有可能受到影响,但是我们实际发现可能其他的库、其他的应用受的就并没有那么明显的情况,如果你触发到这个 musl 库的这个行为的话,前提就是你必须是用 C 写的一个程序,但是我们很多大部分整体都是 Golang 这样写的,可能因为 Golang 有自己的 DNS 解析器,所以他绕过了这个问题。
但是在那个 DevOps,尤其是 CI,他们可能比如说用 Git 用这种的东西的大部分都是 C 写的,所以就是很倒霉。只有 DevOps 那个团队的 git 拉镜像的程序有可能会被这个问题影响。本质上来说它可能是受上游 DNS 的一个不稳定的影响,但是如果是 glibc 的话,它其实屏蔽掉这个问题了,但是如果是 musl 的话,会让这个故障会更明显一些。我下面贴了几个链接,有包括 GitLab 还有 K8s 他们自己本身社区对这个问题的讨论,而且我甚至还看到了有些人专门给musl 库打了一个 patch 来处理这个行为,让它变得和 glibc 的行为更像,也是为了规避这种问题。
这个问题解决的时候,我们当时就是和应用侧沟通,就建议他们去换基础镜像,因为 Alpine 本身还有一些别的问题, 有些其他的 DNS 的解析的问题,还有他的一些分配器的一些性能的问题。反正就是我们这边其实是强烈不推荐他用 musl 的,如果是不行的话,可能会建议他们去 patch 一下那个 musl 的库,把这个补丁带上,或者就是你去尝试一些新版本的 musl,可能是这周吧, musl 发了一个新版本,更新日志里面他去强调了这个事情,说他最新的这个版本的这个 DNS 行为会跟 glibc 的行为尽可能的类似。估计也是他们那个社区就碰到了很多用了 K8s 之后这样的问题。
低内核版本 Bug 导致 conntrack 创建冲突
我们本来以为这个问题解决掉了,我们 DevOps 里面的业务团队,他们换了一批镜像,就把一部分的镜像换成了那个 Ubuntu,但是换完了之后会发现依然存在 DNS 解析的问题。
他们就又过来又把我们怼了一遍,说我们听你的把镜像都换了,结果你们这个问题还是有,我们这次再去看的时候就发现这个现象就跟之前又不一样了,我们通过查看 Client 的 DNS,发现 DNS 的返回记录都是正常的,我们观察到一些异常的情况,有些 DNS 的请求返回的时间会比较长,就是可能会 5 秒、 10 秒、 15 秒这个样子,有的时候就 15 秒才返回,这个时候就可能会触发它应用侧的一个超时,因为应用侧的那个是一个 10 秒的超时。
但是很奇怪的是他们应用侧报的是一个,他没有报超时,他报的是没有办法解析域名,所以当时我们也很奇怪为什么会是这样的一个行为。
回来之后,我们发现当时的判断稍微有一些问题,其实并不是说它最终成功了,而是 DeepFlow 的 DNS 面板稍微有点小问题,它展示的是 Client Error 的一些情况。但是对于这种超时的情况,它其实是没有显示出来的,可能跟这个机制有关,因为 Client Error 是很明确你上游返回的一个错误。但是超时的话,尤其是 UDP 这种情况,可能就是你发这个包出去没有人回来,所以他那个面板里面我们其实一开始是没有找到那个中间的一些过程的,后来我们还是到了那个 Request Log 里面,把所有的 DNS 记录,所有的 UDP 记录都拉出来,我们会发现这边有很规律的这个 5 秒一次。比如说这 25-30、30-35、35-40 这样的一些情况,可以很明显地看到中间会有隔 5 秒的这个情况。
这个是 K8s 社区里面一个比较古老的问题了,如果定位到了这样的 5 秒的现象,去任何一个搜索引擎搜什么 DNS 5 秒或者是 K8s,5 秒都会有一个有很多的文章去介绍这个问题,它的原因我们没有细查,当时我们认为就是这个问题了,按照这个帖子或者很多人讨论的说的其实是一个内核 conntrack 的一个 Bug。尤其由于你是 UDP 的,而且是短时间的并发性地发送了大量的 DNS 请求,导致 conntrack 创建冲突了。如果 conntrack 创建冲突的时候,会等 5 秒超时之后它再去重试,如果它发现冲突的话,会间隔 5 秒,如果它第二次不冲突了,那就是只差 5 秒,如果是第二次又冲突的话,可能就再加 5 秒,再冲突的话就再加 5 秒,就是会有这样的一个现象。
这个问题社区有很多的解决方法,但是对于我们当时来说都不太好去实现,最本质的一个解决方法是去升级内核,应该是在五点几的内核去修 conntrack 冲突的 bug 的,如果你用到是 redhat 支持的 4.19 这样的一些内核,或者是 4 的内核,它 Backport 回来应该也是可以。但是由于操作系统不受我们控制的,而且我们客户的操作系统我们也控制不了,所以这个内核的问题我们当时就没有办法去推这个事情。
再有就是社区比较推荐的是用那个 Local DNS Cache,这个是在每台机器上起了一个 DNS 的 Daemon,会绕过这个 conntrack,就不需要做 NAT 的一些事情了,也能够去解决这个 conntrack 冲突的问题,相当于把 conntrack 绕过去了,但是这个问题也比较麻烦,因为我们只是负责网络的,如果你改这个 Local DNS Cache 的话,还需要去和 K8s 的团队沟通,因为 K8s 底下配置也要改,所以当时就变成了 DevOps 团队、网络团队还有 K8s 团队这样三个团队的事情,也很难去推进这个事情。
再有的话就是利用 TCP 就是或者是一些串行化,就是不并发发送的方式,但这个都需要应用去调整,所以这几个方法也不行。最后我们当时临时商量的一个方式,就是这是一个概率性的这个 5 秒超时,那么你们应用侧那边是不是可以加一些超时和加一些重试?你多重复重试几次,或者多加一些超时,对吧?你们不能假设网络肯定是稳定的,对不对?那网络肯定会丢包嘛?我们就那么跟他们说,反正那个业务团队那边骂骂咧咧的就又把我们骂了一顿,但是还是捏着鼻子又去改了。
ARP 缓存未更新导致 Mac 地址错误
但是很不幸的是他们把超时调整到了 30 秒,发现这个 DNS 解析的问题依然存在,还是会存在解析失败,你可以想象那个团队当时就已经炸了,对吧?天天让我们改,你们这个问题到底怎么回事?这个这次我们发现的问题就是他加了 30 秒的重试,我们当时的假设就是你个冲突不会连续发生,你连续发生三四次就差不多了,你不可能连续五六次还在那冲突,还在那触发这个 Bug,但是其实还是存在,会发现这个 5 秒的超时,那段时间就没有好过,就 5 秒就一直到它 30 秒超时。我们这时候再去抓包,当时我们没用 DeepFlow,就只能去抓包了,把所有的机器都开启抓包,用 tcpdump 把包都存下来了,其实也没有发现一些特别特殊的问题,但是我们就过滤那些包的时候就很偶尔的发现了一个现象,我们发现这个 Pod 的 IP去请求这个 CoreDNS 的时候,这个 IP 在半个小时之前曾经被另一个 Pod 使用过。
顺着这个现象再去摸,我们去把数据包的 Mac 地址打开,我们发现这两个就是这个 IP 相同,但是 Mac 不同。有一个问题,就是客户端发送的请求,服务端就该发响应,这些 IP 都是对的。但是这个服务端就是 CoreDNS 在回这个 UDP 包的时候,他用的 Mac 地址是前一个 Pod IP 对应的 Mac 地址,就我不清楚大家有没有理解过来。从顺序上来说的话,比如说半个小时前有一个 Pod 起来了,它的 IP 比如说是 1.1,它去请求 CoreDNS 都是正常的,过了一段时间这个 Pod 销毁了。半个小时之后又一个 Pod 起来了,它的 IP 也是 1.1,它也去请求 CoreDNS,但是 CoreDNS 那边记录的是你前一个 Pod 的那个 Mac 地址,所以它在往回发这个包的时候,你的第二个 Pod 就收不到了,它其实是这么一个情况,触发的情况就是你的机器内短时间内就是有比较大量的 Pod 在创建、删除。
正好 Devops git clone 的 Pod 用到了那个 IP 地址,那个时候 CoreDNS 内的 ARP 缓存,它又没有更新,所以在这种情况下导致的这个问题。我们最终定位到这儿,就相对来说会好解决一点,因为这个时候才真正涉及到我们团队真正干的事情,我们团队是写网络插件的,所以这个事情就相对就好改一点了。就是我们在 Pod 创建之后,我们会发送一个那个 GARP 的那个广播,我们会主动的去通知其他 Pod 去更新自己的 ARP 缓存,这样的话这个问题就基本上就没有再出现了。
我前两天我还跟云杉的人说了我们排查这个的过程,云杉的人跟我说,你这个在我们的那个 Flow Log 里也可以看,我们都是记了那个 Mac 地址什么的,我还专门去弄了一下,但挺困难的。你我就是一个做网络的,我得去看 Clickhouse,我得改 Grafana,我才能把那个 Mac 地址去那个面板上显示出来。
而且这个事情就很诡异,我如果知道了这个问题是这样,我就我要把 Mac 地址显示出来,但是我在知道这个问题之前,我怎么知道是那个 Mac 地址出问题呢?所以我的感觉就是确实 Flow Log 里面所有的信息都包含了,但是我在这个时间点我其实很难的去想到我应该去看那个 Mac 相关的事情,就我在想是不是 DeepFlow,我看那个向阳老师最早也说他们想做那个 trace 的那个 completion 嘛,我觉得你其实可以帮我做一下这个丢包的 completion。比如说我就说我这在这个时间点,我这个 DNS 出了问题,那你是不是就把这一段时间点的那个 DNS 的日志,还有它关联上下游的日志,还有它的 Flow Log、 Request Log 这些都拉出来。
比如说这种的话,其实我现在说下来,大家可能感觉这个好像排查的还比较顺利,但其实挺困难的啊,就是我怎么就突然看到这差 5 秒了?是不是这个事情就很困难?还有就是这个我怎么就正好发现了那边是一个成功的,但是返回 0 这个其实都很困难的,就是你的信息这些东西都有了,但是真的就有时候靠经验或者是靠运气,我才正好的发现了这条数据是异常的,所以我还挺希望就是 DeepFlow 如果我就告诉他一个时间段,那就告诉我: “诶这条数据好像挺异常的”,那我其实就就会好受很多,就不会天天挨骂,对不对?
简单总结一下这个我们的排查过程,前后比较长的时间,都是同一个现象,都是命令行的一句话出了一个错,但是可能是存在多个诱因的,我们排查了很多的诱因,有操作系统层面的问题,有可能是就最开始的那个 libc 库的问题,也有可其实也有可能是应用侧。我们其实也碰到应用侧,它可能就是自己代码写的问题,比如说把链接耗光了或者怎么样导致的一些问题,也有可能就是你 CNI 本身就是像我们这种情况下的问题,所以就是可能同一个现象,但背后真的问题的原因是千奇百怪的,
对于我们来说,我们可能总结的几个要点就是我们需要可能针对这种特定类型的应用流量进行一个特定特殊的一个监控。就比如说 DNS,在 K8s 里面就是一个比较典型的一个场景,因为它本身 DNS 的机制就很复杂,而且 DNS 又容易出问题,而且这个你会发现 K8s 所有的 DNS 问题全是概率性的。可能需要对 DNS 专门的有一些监控,专门的一些视图,还有的话就是需要有这种 Flow 级别的监控。大家可以看我的排查过程中,其实后期很多的,就那个 DNS 面板,我可能就是过一下发现那段时间那可能有什么,但是可能更多的话我可能会需要 Flow 级别,我需要看更多更详细的信息,因为可能就是就 DeepFlow 其实采的数据很多很多,在那它面板里其实展示的比较有限,但是你真正排查问题的时候其实可能还是需要更多 Flow 级别的一些东西来帮助我们,或看一些现场的一些信息。
还有的话因为这个问题我们排查了很长时间,我就想是不是其实还是缺了一些东西?我看 Cilium有个工具是可以就探测,发数据包,从这个数据包到另一端,你这个链路是不是会出现丢包?如果是丢包的话会出现在哪?它有这么样的一个工具,但是那个其实不太适合做长期的这个监控,因为它只是临时性的一个命令行。
其实 DeepFlow 有很多 eBPF的能力,有这个 eBPF 能力,我们就能做跟 Cilium 类似的事情,去 hook 看这个内核里面这个丢包的一些事件,如果我这个丢包事件正好和这段时间内上面的流出现了一些问题的话,那可能这个问题对于我们来说可能就会更清晰的定位一点。就比如像之前说的那个内核 conntrack 的冲突,那我可能这边如果能抓到一个 conntrack 失败的事件,跟我的这个 DNS 的这个失败的事件能够关联上的话,其实对我们就有很多的帮助。
声明:
本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
学到了,666