编辑推荐: |
本文将以带有两个
Linux 节点的标准 Google Kubernetes Engine(GKE)集群为例,通过跟踪
HTTP 请求被传送到集群服务的整个过程,深度拆解 Kubernetes 网络的复杂性。
本文来自于K8s技术圈,由Alice编辑、推荐。 |
|
请求的旅程
当一个人在浏览网页时,他首先单击一个链接,发生了一些事,之后目标页面就被加载出来。这让人不免好奇,从单击链接到页面加载,中间到底发生了什么?
对于这个问题,我们可以这样理解。如下图所示,用户请求通过 Internet 被发送给一个非常大的云提供商,然后再被发送到该云提供商基础架构中托管的
Kubernetes 集群。
如果进一步放大 Kubernetes 集群,我们可以看到云提供商正向 Kubernetes Service
资源(svc)发送请求,然后将请求路由到 Kubernetes ReplicaSet(rs)中的 Pod。
为了更直观,我们可以部署 YAML 来创建 Kubernetes Service 和 ReplicaSet:
现在我们已经在 hello-world ReplicaSet 下创建了两个 Pod,还创建了一个带有负载均衡器的服务资源
hello-world(如果云提供商和集群网络支持),以及一个在 host:port 中有两个条目的
Kubernetes Endpoint 资源,每个 Pod 对应一个,以 Pod IP 作为主机值和端口
8080。
在 GKE 集群上,我们 kubectl 一下会返回以下内容:
集群 IP 网络信息:
Node - 10.138.15.0/24
Cluster - 10.16.0.0/14
Service - 10.19.240.0/20
已知服务在集群 CIDR 中的虚拟 IP 地址(VIP)是 10.19.240.1。现在,我们可以从负载均衡器开始,深入跟踪请求进入
Kubernetes 集群的整个“旅程”。
负载均衡器
Kubernetes 通过本地控制器和 Ingress 控制器提供了很多公开服务的方法,但这里我们还是使用
LoadBalancer 类型的标准 Service 资源。
我们的 hello-world 服务需要 GCP 网络负载均衡器。每个 GKE 集群都有一个云控制器,它在集群和
API 端点之间进行接口,以自动创建集群资源所需的 GCP 服务,包括我们的负载均衡器(不同云提供商的负载均衡器在类型、特性上都有不同)。
通过从不同的角度观察集群,我们可以查看外部负载均衡器的位置:
kube-proxy
每个节点都有一个 kube-proxy 容器进程(在 Kubernetes 参考框架中,kube-proxy
容器位于 kube-system 命名空间的 Pod 中),它负责把寻址到集群 Kubernetes
服务对象虚拟 IP 地址的流量转发到相应后端 Pod。kube-proxy 当前支持三种不同的实现方式:
User space:即用户空间,服务路由是在用户进程空间的 kube-proxy 中进行的,而不是内核网络堆栈。这是kube-proxy
的最初版本,较为稳定,但是效率不太高;
iptables:这种方式采用 Linux 内核级 Netfilter 规则为 Kubernetes
Services 配置所有路由,是大多数平台实现 kube-proxy 的默认模式。当对多个后端 Pod
进行负载均衡时,它使用未加权的循环调度;
IPVS:IPVS 基于 Netfilter 框架构建,在 Linux 内核中实现了 L4 负载均衡,支持多种负载均衡算法,连接最少,预期延迟最短。它从
Kubernetes v1.11 中开始普遍可用,但需要 Linux 内核加载 IPVS 模块。它也不像
iptables 那样拥有各种 Kubernetes 网络项目的广泛支持。
在我们的 GKE 集群中,kube-proxy 以 iptables 模式运行,所以我们后续主要研究该模式的工作方式。
如果查看创建好的 hello-world 服务,我们可以发现它已经被分配了一个节点端口 30510。节点网络上动态分配的端口允许其中托管的多个
Kubernetes 服务在其端点中使用相同的面向 Internet 的端口。
如果服务已被部署到标准 Amazon EKS 集群,它将由 Elastic Load Balance
提供服务,该服务会将传入的连接发送到相应 Pod 节点上我们服务的节点端口。但是,Google Cloud
Platform 网络负载均衡器只会将流量转发到与负载均衡器的传入端口位于同一端口的目标,例如,到负载均衡器上的端口
80 的流量会被发送到目标后端实例上的端口 80。
我们的 hello-world pods 绝对没有在节点的端口 80
上监听。所以如果在节点上运行 netstat,我们可以看到没有进程正在监听该端口。
那么,通过负载均衡器的请求是如何成功建立连接的呢?如果 kube-proxy
在用户空间模式下运行,它实际上是将连接代理到后端 Pod。但是,在 iptables 模式下,kube-proxy
配置了 Netfilter 链,因此该连接被节点的内核直接路由到了后端容器的端点。
iptables
在我们的 GKE 集群中,如果登录到其中一个节点并运行 iptables,我们可以看到这些规则。
根据规则注释,我们可以获得与来自服务的负载均衡器到 hello-world 服务的传入连接相匹配的过滤器链的名称,并遵循该链的规则(在没有规则注释的情况下,我们仍然可以将规则的源
IP 地址与服务的负载均衡器进行匹配)。
我们还可以可视化网络堆栈中用于评估和修改数据包的链和规则,查看我们在集群中创建的服务是怎么把流量定向到副本集成员的。
KUBE-FW-33X6KPGSXBPETFQV 链有三个规则,每个规则都添加了另一个链来处理数据包。
KUBE-MARK-MASQ 向发送到 hello-world 服务的包(来自集群网络外部)添加一个
Netfilter 标记。带有此标记的数据包将按照 POSTROUTING 规则进行更改,以使用源网络地址转换(SNAT),并将节点
IP 地址作为其源 IP 地址;
KUBE-SVC-33X6KPGSXBPETFQV 链适用于所有与 hello-world 服务相关的流量(与源无关),并且对每个服务端点(在本例中为两个
Pod)提供规则。使用哪个端点链是完全随机确定的:
KUBE-SEP-ALRUKLHE5DT3R34X:如果需要,KUBE-MARK-MASQ 会再次向数据包中添加一个
Netfilter 标记用以 SNAT;DNAT 规则使用 10.16.0.11:8080 端点作为目标来设置目标
NAT
KUBE-SEP-X7DMMHFVFOT4JLHD:如果需要,KUBE-MARK-MASQ 会再次为数据包添加一个
Netfilter 标记用以 SNAT;DNAT 规则使用 10.16.1.8:8080 端点作为目标来设置目标
NAT
KUBE-MARK-DROP 向此时尚未启用目标 NAT 的数据包添加 Netfilter 标记。这些数据包将在
KUBE-FIREWALL 链中被丢弃。
需要注意的是,尽管我们的集群有两个节点,每个节点都有一个 hello-world Pod,但这种路由方法并不存在优先级。如果我们将服务规范中的
externalTrafficPolicy 更改为 Local,那么情况就会改变。假设此时存在请求,这个请求不仅会转到接收请求的节点上的
Pod,还会导致没有服务 Pod 的节点拒绝连接。
因此,Local 策略通常需要与 Kubernetes daemon
sets 一起使用,后者会在集群中的每个节点上调度一个 Pod。虽然前者能明显降低请求的平均网络延迟,但它也可能导致服务
Pods 之间的负载不均衡。
Pod 网络
本文不会深入介绍 Pod 网络,但是在我们的 GKE 集群中,Pod
网络有自己的 CIDR 块,与节点网络分开。Kubernetes 网络模型要求集群中的所有 Pod
能够直接相互寻址(无视其主机节点)。GKE 群集使用 kubenet CNI,它在每个节点上创建到
Pod 网络的网桥接口,为每个节点提供自己的 Pod IP 地址专用 CIDR 块,以简化分配和路由。Google
Compute Engine(GCE)网络可以在 VM 之间路由该 Pod 网络流量。
请求
以下是是我们获取 HTTP 200 响应代码的方式:
本文提到了许多改变路由的方法,它们由不同 Kubernetes 平台提供,下面是一个简单的清单:
容器网络接口(CNI)插件:每个云提供商默认使用与其 VM 网络模型兼容的 CNI 实现。本文以默认设置的
GKE 集群为例,但如果是 Amazon EKS,那会很不一样,因为 AWS VPC CNI 把容器直接放在节点的
VPC 网络上;
Kubernetes Network Policy:Calico 是实施网络策略最受欢迎的 CNI
插件之一,它在节点上为每个 Pod 创建一个虚拟网络接口,并使用 Netfilter 规则来实施其防火墙规则;
尽管大多数情况下仍然使用 Netfilter,但 kube-proxy IPVS 路由模式通常会把服务路由和
NAT 移出 Netfilter 规则;
外部负载均衡器或其他可以将流量直接发送到服务节点端口的源将匹配 iptables 中的不同链(KUBE-NODEPORTS);
Kubernetes Ingress 控制器可以通过多种方式更改边缘服务路由;
诸如 Istio 之类的服务网格可能会绕过 kube-proxy,直接连接服务容器之间的内部路由。
保护服务
Kubernetes 网络需要大量可移动部件,它非常复杂,但如果开发者对集群中发生的事有基本了解,这会有助于开发者更有效地监控、保护它。
第一,对于 Kubernetes 服务资源创建的云负载均衡器,添加防火墙限制的通用方法是不存在的。一些云提供商会支持服务规范中的
loadBalancerSourceRanges 字段,这个字段允许开发者提供可以连接到负载均衡器的
IP CIDR 块白名单。如果云提供商不支持此字段,它就会被忽略,因此开发者需要验证外部负载均衡器的网络配置。
而对于不支持 loadBalancerSourceRanges 字段的云提供商,除非已经在云提供商级别采取措施锁定了负载均衡器和运行它们的云网络,开发者还是应该假定负载均衡器上的服务端点是对全世界开放的。由于各种因素,云提供商负载均衡器产品的默认防火墙设置千差万别,一些云提供商可能还支持对
Service 对象的注释,以配置负载均衡器的安全性。
其次,请注意,我们没有通过在 GKE 集群中启用 Kubernetes 网络策略支持来安装 Calico
CNI,因为 Calico 创建了大量其他 iptables 规则,这给可视化跟踪到 Pod 的虚拟路由时增加了额外步骤。尽管如此,我们还是建议开发者在生产集群中实现
NetworkPolicy API 的 CNI,并创建限制 Pod 流量的策略。
第三,启用 HostNetwork 属性创建的 Pod 将共享节点的网络空间。虽然存在一些这样做的例子,但通常情况下,大多数
Pod 不需要在主机网络上,尤其是对于有 root 特权的 Pod,这可能会导致受攻击的容器可以查看网络流量。如果开发者需要在节点网络上公开容器端口,而使用
Kubernetes Service 节点端口无法满足需求,一个稳妥的选择是可以在 PodSpec
中为容器指定 hostPort。
最后,使用主机网络的 Pod 不应使用 NET_ADMIN 功能运行,这将使它们能够读取和修改节点的防火墙规则。
原文:https://www.stackrox.com/post/2020/01/kubernetes-networking-demystified/ |