Kubernetes DNS 超时

最近有很多Kubernetes的用户报告说从Pod中查找DNS有时需要5秒甚至更多的时间:
weave#3287
kubernetes#56903
在这篇文章中,我将解释这种延迟的根本原因,讨论一些缓解措施并给出内核修复。

背景

在Kubernetes中,Pod访问DNS服务器(kube-dns)的最常见方式是通过服务抽象。因此,在尝试解释这个问题之前,了解Kubernetes Service是如何工作的以及目标网络地址转换(DNAT)是如何在Linux内核中实现的非常重要。

注意:本文中的所有示例都基于Kubernetes v1.11.0和Linux内核v4.17。

Service 如何工作

iptables模式中,节点上的kube-proxy在主机网络名称空间的nat表中为每个Service创建一些iptables规则。

让我们考虑一个集群中有两个DNS服务器实例的kube-dns服务。有关规定如下:

(1) -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
<...>
(2) -A KUBE-SERVICES -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
<...>
(3) -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LLLB6FGXBLX6PZF7
(4) -A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -j KUBE-SEP-LRVEW52VMYCOUSMZ
<...>
(5) -A KUBE-SEP-LLLB6FGXBLX6PZF7 -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.32.0.6:53
<...>
(6) -A KUBE-SEP-LRVEW52VMYCOUSMZ -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.32.0.7:53

在我们的示例中,每个Pod的 /etc/resolv.conf 中都填充了名称服务器10.96.0.10条目。因此,一个来自Pod的DNS查询请求将被发送到10.96.0.10,这是kube-dns对应的Service的一个ClusterIP(一个虚拟IP)。

  • 基于规则1 请求进入KUBE-SERVICE链
  • 匹配到规则2, 进入规则3
  • 进入规则3以后有个随机分配,即要么转到规则5要么转到规则6 (两个rs实例,每个命中的概率为50%)
  • 规则5或者6 修改请求的目的IPv4地址UDP数据包的“真实”IPv4地址DNS服务器

这种修改是由DNAT完成的

10.32.0.6和10.32.0.7是Weave网络中Kubernetes DNS服务器容器的IPv4地址。

DNAT in Linux Kernel

如上所述,服务的基础(在iptables模式下)是DNAT,由内核执行。
DNAT的主要职责是更改发送包的目的地、应答包的来源,同时确保对所有后续包进行相同的修改。
后者严重依赖于连接跟踪机制,也称为conntrack,它是作为内核模块实现的。顾名思义,conntrack跟踪系统中正在进行的网络连接。
以一种简化的方式,conntrack中的每个连接都由两个元组表示

  • 一个用于原始请求(IP_CT_DIR_ORIGINAL)
  • 另一个用于应答(IP_CT_DIR_REPLY)

对于UDP,每个元组:

  • 源IP地址
  • 源端口
  • 目标IP地址
  • 目标端口

应答元组包含存储在src字段中的目标的实际地址。

例如,如果一个IP地址为10.40.0.17的Pod向kube-dns的ClusterIP发送一个请求,该请求被翻译为10.32.0.6,那么将创建以下元组:

Original: src=10.40.0.17 dst=10.96.0.10 sport=53378 dport=53
Reply: src=10.32.0.6 dst=10.40.0.17 sport=53 dport=53378

通过这些条目,内核可以相应地修改任何相关信息包的目标地址和源地址,而不需要再次遍历DNAT规则。此外,它还知道如何修改回复以及应该发送给谁。
当一个conntrack条目被创建时,它首先未经确认。稍后,如果没有使用相同的原始元组或应答元组确认的conntrack条目,内核将尝试确认条目。

conntrack的创建和DNAT的简化流程如下所示:

conntrack
  1. 如果给定数据包不存在,则为其创建一个conntrack
    IP_CT_DIR_REPLY是IP_CT_DIR_ORIGINAL元组的反转,所以应答元组的src还没有改变。
  2. 查找匹配的规则
  3. 根据DNAT规则更新应答元组src部分,使其不被任何已确认的连接使用。
  4. Mangle根据应答元组对包的目的端口和地址进行操作。
  5. 如果没有与相同的原始或应答元组确认的连接,则确认连接;递增insert_failed计数器,如果包存在,则删除它。

问题

当来自不同线程的两个UDP数据包同时通过同一个socket发送时,就会出现问题

UDP是一种无连接的协议:

  • 不会因为connect(2) syscall(与TCP相反)而发送包,因此在调用后不会创建conntrack条目
  • 只有在发送包时才创建条目
    这导致了以下可能的竞争:
  1. 两个包都没有在上图步骤1 nf_conntrack_in中找到已确认的连接, , 对于这两个包,将创建具有相同元组的两个conntrack条目。
  2. 与上面的情况相同,但是其中一个包的conntrack条目在另一个调用3之前被确认get_unique_tuple另一个包得到一个不同的应答元组,通常会改变源端口
  3. 与第一种情况相同,但是在步骤2中选择了两个具有不同端点的不同规则。ipt_do_table。
竞争的结果是一样的:

其中一个包在上图步骤5 __nf_conntrack_confirm中被丢弃`

这正是DNS的情况。GNU C库和musl libc都并行执行A和AAAA DNS查找。其中一个UDP数据包可能会由于竞争被内核丢弃,所以客户端会在超时(通常为5秒)后尝试重新发送它。

值得一提的是,这个问题不仅针对Kubernetes—任何并行发送UDP数据包的Linux多线程进程都容易出现这种竞争情况。

而且,即使您没有任何DNAT规则,也可能发生第2次争用——这足以加载nf_nat内核模块来支持调用get_unique_tuple。

可以使用conntrack -S获得的insert_failed计数器是一个很好的指示器,可以用来判断您是否遇到了这个问题

解决办法

有很多的解决办法:

  • 禁用并行查找
  • 禁用IPv6以避免AAAA查找
  • 使用TCP进行查找
  • 在Pod的解析器配置文件中设置DNS服务器的实际IP地址
    有关详细信息,请参阅文章开头的链接问题。不幸的是,由于通常使用的容器基础映像Alpine Linux所使用的musl libc中的限制,它们中的许多不能工作。

对Weave用户来说,最可靠的一种方法是使用tc延迟DNS数据包, 看Quentin Machu's write-up

另外,您可能想知道ipvs模式下的kube-proxy是否可以绕过这个问题。答案是否定的,因为conntrack也在此模式下启用。此外,当使用rr调度程序时,第3个竞争可以很容易地在低DNS流量的集群中复制。

内核修复

不管有什么变通方法,我决定修复内核中的根本原因。

结果如下:
"netfilter: nf_conntrack: resolve clash for matching conntracks"

fixes the 1st race (accepted)

"netfilter: nf_nat: return the same reply tuple for matching CTs"

fixes the 2nd race (waiting for a review)

这两个补丁修复了仅运行一个DNS服务器实例的集群的问题,同时降低了其他服务器的超时命中率。

为了完全消除所有情况下的问题,第三轮比赛需要解决

  • 一种可能的解决方案是在第5步中合并来自同一套接字的不同目的地的冲突conntrack条目__nf_conntrack_confirm 但是,这将使之前针对包的iptables规则遍历的结果无效,在该步骤中更改了包的目标。

  • 另一种可能的解决方案是在每个节点上运行一个DNS服务器实例,并创建一个Pod来查询在本地节点上运行的DNS服务器,这是我的同事的建议链接

原文

https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts

https://blog.quentin-machu.fr/2018/06/24/5-15s-dns-lookups-on-kubernetes/

https://tech.xing.com/a-reason-for-unexplained-connection-timeouts-on-kubernetes-docker-abd041cf7e02

你可能感兴趣的:(Kubernetes DNS 超时)