根据 CNCF 历年发布的 Kubernetes 生态报告,安全、存储、网络始终是开发者最关注的三大槽点。即便在虚拟网络和请求路由方面有丰富经验,很多开发者在处理 Kubernetes 集群网络时还是很混乱。

本文将以带有两个 Linux 节点的标准 Google Kubernetes Engine(GKE)集群为例,通过跟踪 HTTP 请求被传送到集群服务的整个过程,深度拆解 Kubernetes 网络的复杂性。

请求的旅程

当一个人在浏览网页时,他首先单击一个链接,发生了一些事,之后目标页面就被加载出来。这让人不免好奇,从单击链接到页面加载,中间到底发生了什么?

对于这个问题,我们可以这样理解。如下图所示,用户请求通过 Internet 被发送给一个非常大的云提供商,然后再被发送到该云提供商基础架构中托管的 Kubernetes 集群。

如果进一步放大 Kubernetes 集群,我们可以看到云提供商正向 KubernetesService资源(svc)发送请求,然后将请求路由到 Kubernetes ReplicaSet(rs)中的 Pod。

为了更直观,我们可以部署 YAML 来创建 Kubernetes Service 和 ReplicaSet:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: hello-world
labels:
app: hello-world
spec:
selector:
matchLabels:
app: hello-world
replicas: 2
template:
metadata:
labels:
app: hello-world
spec:
containers:
- name: hello-world
image: gcr.io/google-samples/node-hello:1.0
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP

---
apiVersion: v1
kind: Service
metadata:
name: hello-world
spec:
selector:
app: hello-world
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: LoadBalancer
externalTrafficPolicy: Cluster

现在我们已经在hello-world ReplicaSet下创建了两个 Pod,还创建了一个带有负载均衡器的服务资源hello-world(如果云提供商和集群网络支持),以及一个在host:port中有两个条目的 KubernetesEndpoint资源,每个 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,我们可以看到没有进程正在监听该端口。