本文发布时间已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。
kube-proxy 的微妙之处:调试间歇性连接重置
我最近遇到一个导致间歇性连接重置的 Bug。经过一番深入研究,我发现它是由几个不同的网络子系统微妙组合引起的。这帮助我更好地理解了 Kubernetes 网络,我认为有必要与对此主题感兴趣的更广泛的受众分享。
症状
我们收到用户报告,声称在使用 ClusterIP 类型的 Kubernetes 服务向同一集群中运行的 Pod 提供大型文件时,他们遇到了连接重置。对集群的初步调试没有发现任何有趣的问题:网络连接正常,下载文件也没有遇到任何问题。但是,当我们在多个客户端并行运行工作负载时,我们能够重现该问题。更令人费解的是,当使用没有 Kubernetes 的虚拟机运行工作负载时,无法重现该问题。这个问题可以通过一个简单的应用程序轻松重现,显然与 Kubernetes 网络有关,但究竟是什么原因呢?
Kubernetes 网络基础
在深入研究这个问题之前,让我们先谈谈 Kubernetes 网络的一些基础知识,因为 Kubernetes 处理来自 Pod 的网络流量的方式因目标地址的不同而有很大差异。
Pod 到 Pod
在 Kubernetes 中,每个 Pod 都有自己的 IP 地址。好处是,在 Pod 内运行的应用程序可以使用其规范端口,而不是重新映射到不同的随机端口。Pod 之间具有 L3 连接。它们可以互相 ping,并互相发送 TCP 或 UDP 数据包。CNI 是解决在不同主机上运行的容器的此问题的标准。有许多不同的插件支持 CNI。
Pod 到外部
对于从 Pod 到外部地址的流量,Kubernetes 只是使用 SNAT。它的作用是将 Pod 的内部源 IP:端口替换为主机的 IP:端口。当返回数据包返回到主机时,它将 Pod 的 IP:端口重写为目标地址,并将其发送回原始 Pod。整个过程对原始 Pod 是透明的,它根本不知道地址转换。
Pod 到服务
Pod 是易逝的。大多数情况下,人们需要可靠的服务。否则,它几乎毫无用处。因此,Kubernetes 有一个叫做 “服务” 的概念,它只是 Pod 前面的一个 L4 负载均衡器。服务有几种不同的类型。最基本的一种类型称为 ClusterIP。对于这种类型的服务,它具有一个唯一的 VIP 地址,该地址只能在集群内部路由。
Kubernetes 中实现此功能的组件称为 kube-proxy。它位于每个节点上,并编程复杂的 iptables 规则来执行 Pod 和服务之间的各种过滤和 NAT。如果转到 Kubernetes 节点并键入 iptables-save
,您将看到 Kubernetes 或其他程序插入的规则。最重要的链是 KUBE-SERVICES
、KUBE-SVC-*
和 KUBE-SEP-*
。
KUBE-SERVICES
是服务数据包的入口点。它的作用是匹配目标 IP:端口,并将数据包分派到相应的KUBE-SVC-*
链。KUBE-SVC-*
链充当负载均衡器,并将数据包平均分配到KUBE-SEP-*
链。每个KUBE-SVC-*
的KUBE-SEP-*
链数量与它后面的端点数量相同。KUBE-SEP-*
链表示一个服务端点。它只是执行 DNAT,将服务 IP:端口替换为 Pod 的端点 IP:端口。
对于 DNAT,conntrack 会启动并使用状态机跟踪连接状态。需要状态的原因是,它需要记住它更改的目标地址,并在返回数据包返回时将其更改回来。Iptables 还可以依赖 conntrack 状态 (ctstate) 来决定数据包的命运。这 4 个 conntrack 状态尤其重要
- NEW: conntrack 对此数据包一无所知,这发生在接收到 SYN 数据包时。
- ESTABLISHED: conntrack 知道该数据包属于已建立的连接,这发生在握手完成后。
- RELATED: 该数据包不属于任何连接,但它与另一个连接相关联,这对于 FTP 等协议特别有用。
- INVALID: 数据包有问题,并且 conntrack 不知道如何处理它。此状态在此 Kubernetes 问题中起着核心作用。
以下是 Pod 和服务之间 TCP 连接的工作原理图。事件的顺序是
- 左侧的客户端 Pod 向服务发送数据包:192.168.0.2:80
- 数据包正在通过客户端节点中的 iptables 规则,并且目标地址更改为 Pod IP,10.0.1.2:80
- 服务器 Pod 处理数据包并发送回一个目标地址为 10.0.0.2 的数据包
- 数据包返回到客户端节点,conntrack 识别数据包并将源地址重写回 192.169.0.2:80
- 客户端 Pod 接收到响应数据包

良好的数据包流
是什么导致了连接重置?
背景知识就讲到这里,那么到底发生了什么,导致了意外的连接重置呢?
如下图所示,问题出在数据包 3。当 conntrack 无法识别返回数据包时,并将其标记为 INVALID。最常见的原因包括:conntrack 无法跟踪连接,因为它已超出容量,数据包本身超出了 TCP 窗口等。对于那些被 conntrack 标记为 INVALID 状态的数据包,我们没有 iptables 规则来丢弃它,因此它将被转发到客户端 Pod,而源 IP 地址不会被重写(如数据包 4 所示)!客户端 Pod 无法识别此数据包,因为它具有不同的源 IP,即 Pod IP,而不是服务 IP。结果,客户端 Pod 说,“等一下,我不记得曾经与此 IP 建立过连接,为什么这家伙一直向我发送此数据包?” 基本上,客户端所做的只是向服务器 Pod IP 发送一个 RST 数据包,即数据包 5。不幸的是,这是一个完全合法的 Pod 到 Pod 数据包,可以传递给服务器 Pod。服务器 Pod 不知道客户端发生的所有地址转换。从它的角度来看,数据包 5 是一个完全合法的数据包,就像数据包 2 和 3 一样。服务器 Pod 只知道,“好吧,客户端 Pod 不想与我对话,所以让我们关闭连接!” 砰!当然,为了使这一切发生,RST 数据包也必须是合法的,具有正确的 TCP 序列号等。但是当它发生时,双方都同意关闭连接。

连接重置数据包流
如何解决?
一旦我们了解了根本原因,修复并不困难。至少有两种方法可以解决它。
- 使 conntrack 在数据包上更加宽松,并且不要将数据包标记为 INVALID。在 Linux 中,您可以通过
echo 1 > /proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_be_liberal
来实现。 - 专门添加一个 iptables 规则来丢弃标记为 INVALID 的数据包,这样它就不会到达客户端 Pod 并造成损害。
该修复程序在 v1.15+ 中可用。但是,对于受此 Bug 影响的用户,有一种方法可以通过在集群中应用以下规则来缓解该问题。
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: startup-script
labels:
app: startup-script
spec:
template:
metadata:
labels:
app: startup-script
spec:
hostPID: true
containers:
- name: startup-script
image: gcr.io/google-containers/startup-script:v1
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
env:
- name: STARTUP_SCRIPT
value: |
#! /bin/bash
echo 1 > /proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_be_liberal
echo done
总结
显然,该 Bug 几乎一直存在。我很惊讶直到最近才被注意到。我认为原因可能是:(1) 这在服务大型负载的拥塞服务器中更常见,这可能不是常见的用例;(2) 应用程序层处理重试以容忍这种重置。无论如何,无论 Kubernetes 的发展速度有多快,它仍然是一个年轻的项目。除了密切倾听客户的反馈、不认为任何事情是理所当然的而是深入挖掘之外,没有其他秘密,我们可以使其成为运行应用程序的最佳平台。