大家好,我是二哥。
今天我们来聊一个有意思的话题:当我们向一个K8s service发起请求后,这个请求是如何到达这个服务背后的Pod上的?
为了便于讨论,我们把范围限定在:当我们从一个K8s cluster的Pod里面向位于同cluster的另一个service发起请求这样的场景。
1. 基础知识
为什么二哥说这个话题有意思呢?它其实是一个包含多个基础知识的综合题。想要找到答案,得需要理解几个与之相关的重要基础知识:iptables、package flow和路由。我们先来依次过一下这几个基础概念。
1.1 Service和Pod关系
首先我们先来复习一下Service和Pod之间的关系。Service存在的意义是将背后的Pod聚合在一起,以单一入口的方式对外提供服务。这里的外部访问者既可能是K8s cluster内部的Pod,也可以是K8s外部的进程。
我们都知道service有一个可以在K8s内部访问到的虚拟IP地址(Cluster IP),这个地址你可以在kubedns里面找到,所以请求端通常通过类似下面这个FQDN prometheus-service.LanceAndCloudnative.svc.cluster.local
来访问一个服务。但如果你K8s Node上无论是执行 ip a
还是 netstat -an
都无法找到这个虚拟地址。
另外我们还知道一个service背后会站着若干个Pod,每个Pod有自己的IP地址。如图1所示。
图 1:Service和Pod之间的关系
1.2 netfilter package flow
Linux 在内核网络组件中很多关键位置处都布置了 netfilter 过滤器。Netfilter 框架是 Linux 防火墙和网络的主要维护者罗斯迪·鲁塞尔(Rusty Russell)提出并主导设计的,它围绕网络层(IP 协议)的周围,埋下了五个钩子(Hooks),每当有数据包流到网络层,经过这些钩子时,就会自动触发由内核模块注册在这里的回调函数,程序代码就能够通过回调来干预 Linux 的网络通信。
图2所示的PREROUTING、INPUT、OUTPUT、FORWARD、POSTROUTING即为这里提到的5个钩子。每个钩子都像珍珠项链一样串联着若干规则,从而形成一个链。这些规则散落在五个表中,它们分布是NAT、Mangle、RAW、Security和Filter。其中Security表不常用,除去它,我们把其它部分合起来简称五链四表。
下面这张图能比较好地阐述链和表的关系。图片来自公众号:开发内功修炼。
图 2:五链四表对照表
了解了 netfilter之后,我们再来看看图3。这张图估计很多同学都不陌生。它非常清晰地展示了内核收到网络包后,netfilter和路由对这个包在数据内容修改和传输路径方面的影响。
为了突出本文的重点,我把流量从service转到Pod过程中涉及到的钩子和路由画出来了。你也看到了,图3里,我还在PREROUTING和OUTPUT这两个钩子处画出了KUBE-SERVICE和NAT。这里的KUBE-SERVICE是由kube-proxy创建的一个自定义链,kube-proxy还在NAT表中定义了分别在这两个钩子处生效的规则。既然规则存放在NAT表里,那肯定表示这些规则与地址转换有关。
图3中有两处出现了“路由选择”的标记,毫无疑问,这里涉及到路由。我将与本文有关的路由表信息也画在了图上。
图 3:netfilter package flow
回到本文讨论的场景。当我们从一个K8s Cluster的Pod向位于同集群的另一个service发起的请求时,请求从图3左下角的红框内(圈1处)进入。请求可能最终流入到作为本地进程的K8s Pod(圈2处,准确地说应该是Pod里面的container),也可能会被转发到位于其它Node上的K8s Pod(圈3处)。
下面我们来结合测试环境、iptables和路由表来详细看看。
2. 测试环境
下面是二哥准备的测试环境。service名字为nginx-web-service,它背后运行有3个名为nginx-web的Pod。简单起见,本文仅讨论类型为CluseterIP的service。
Service Cluster-IP为:172.16.255.220, Service服务分配的CLUSTER-IP以及监听的端口均是虚拟的。Pod的IP分别为:10.204.0.13,10.204.1.3和10.204.1.8。
如图1所示,IP为10.204.0.13的Pod运行在Node 1上,它所在的Node IP地址为130.211.97.55,相应地,IP为10.204.1.3和10.204.1.8的Pod运行在IP地址为130.211.99.206的Node 2上。
代码语言:javascript复制apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-web
namespace: LanceAndCloudnative
labels:
app: nginx-web
spec:
replicas: 3
selector:
matchLabels:
app: nginx-web
template:
metadata:
labels:
app: nginx-web
spec:
containers:
- name: nginx-web
image: nginx
ports:
- containerPort: 80
......
---
apiVersion: v1
kind: Service
metadata:
name: nginx-web-service
namespace: LanceAndCloudnative
spec:
selector:
app: nginx-web
type: ClusterIP
ports:
- port: 80
targetPort: 80
3. 细节详解
铺垫了这么多,终于到了详述细节的环节了。下面是用命令# iptables-save | grep LanceAndCloudnative
在Node 1上dump出来的iptables。
## 通过NAT重新分发到具体的Pod
-A KUBE-SEP-OALS23FQATZ4JKLQ -s 10.204.0.13/32 -m comment --comment "LanceAndCloudnative/nginx-web-service:"
-j KUBE-MARK-MASQ
-A KUBE-SEP-OALS23FQATZ4JKLQ -p tcp -m comment --comment "LanceAndCloudnative/nginx-web-service:"
-m tcp -j DNAT --to-destination 10.204.0.13:80
-A KUBE-SEP-U34NONI5MGHBFYFA -s 10.204.1.3/32 -m comment --comment "LanceAndCloudnative/nginx-web-service:"
-j KUBE-MARK-MASQ
-A KUBE-SEP-U34NONI5MGHBFYFA -p tcp -m comment --comment "LanceAndCloudnative/nginx-web-service:"
-m tcp -j DNAT --to-destination 10.204.1.3:80
-A KUBE-SEP-IYP2JLAPHWQ5VKF7 -s 10.204.1.8/32 -m comment --comment "LanceAndCloudnative/nginx-web-service:"
-j KUBE-MARK-MASQ
-A KUBE-SEP-IYP2JLAPHWQ5VKF7 -p tcp -m comment --comment "LanceAndCloudnative/nginx-web-service:"
-m tcp -j DNAT --to-destination 10.204.1.8:80
## 负载均衡
-A KUBE-SVC-4LFXAAP7ALRXDL3I -m comment --comment "LanceAndCloudnative/nginx-web-service:"
-m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-OALS23FQATZ4JKLQ
-A KUBE-SVC-4LFXAAP7ALRXDL3I -m comment --comment "LanceAndCloudnative/nginx-web-service:"
-m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-U34NONI5MGHBFYFA
-A KUBE-SVC-4LFXAAP7ALRXDL3I -m comment --comment "LanceAndCloudnative/nginx-web-service:"
-j KUBE-SEP-IYP2JLAPHWQ5VKF7
## 入口
-A KUBE-SERVICES -d 172.16.255.220/32 -p tcp -m comment
--comment "LanceAndCloudnative/nginx-web-service: cluster IP" -m tcp --dport 80 -j KUBE-SVC-4LFXAAP7ALRXDL3I
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
这份iptables配置分为三大部分:入口、负载均衡、通过NAT修改目的地址。我们分别来瞧瞧。
3.1 入口
在图2中,你一定看到了在PREROUTING和OUTPUT处的NAT table里有非常显眼的规则,这个规则很容易看懂:如果访问的IP是172.16.255.220,则跳转到子链 KUBE-SVC-4LFXAAP7ALRXDL3I 处。而172.16.255.220 即是我们这里谈及的service IP地址。
3.2 负载均衡
同名子链 KUBE-SVC-4LFXAAP7ALRXDL3I 有三个。为啥是3个?聪明的你一定能猜到,因为Pod replica是3。也就是说因为这个service背后有三个Pod提供支撑服务,所以这里有三条子链。那如果replica是100呢?呵呵,你猜。
不但如此,每个Node上都有着这样类似的三条子链。为啥每个Node都有?好问题,留着以后再聊吧。
岔个话题:我只能说K8s默认使用iptables来实现Service到Pod的转换欠下了大量的技术债。K8s的问题列表里面曾经记录了一个问题#44613:在100个Node的K8s集群里,kube-proxy有时会消耗70%的CPU。还有一个更恐怖的对比数据:当K8s里有5k个services(每个service平均需要插入8条rule,一共40k iptables rules)的时候,插入一条新的rule需要11分钟;而当services数量scale out 4倍到20k(160k rules)时,需要花费5个小时,而非44分钟,才能成功加入一条新的rule。可以看到时间消耗呈指数增加,而非线性。
那均衡的策略是什么呢?你也看到了,这里按30%-50%-20%的比例分配流量。
3.3 通过NAT修改目的地址
我们现在假设子链 KUBE-SEP-OALS23FQATZ4JKLQ 被负载均衡策略选中。在它的规则里我们很容易地读懂它通过DNAT,把dest IP替换成了10.204.0.13。还记得吗?10.204.0.13 是为这个service提供支撑的其中一个Pod 的IP地址。干得漂亮,通过这种方式,完成了从service IP地址到Pod IP地址的转换。
3.4 路由
可单单转换地址还不行,还得把流量导到那个Pod手上才算完成任务。
看到图2的左边的“路由选择”标记了吗?在它旁边的路由表里面写着:如果去子网10.204.0.13/24的话,从cni离开,且下一跳为0.0.0.0。
cni0是什么?哦,它是一个bridge,Pod都插在它的端口上。0.0.0.0表示目标和本机同属一个局域网,不需要经过任何gateway去路由,可以直接通过二层设备发送,比如switch,hub或者bridge。这也暗示了一点:在这种情况下,发起请求的Pod和处理请求的Pod位于同一个Node上。
那如果上一步中负载均衡策略选中的子链是 KUBE-SEP-U34NONI5MGHBFYFA 的话,很显然应该轮到Pod 10.204.1.3来提供服务了。按照路由表的设置:如果去子网10.204.1.0/24的话,这次得从flannel.1离开,且下一跳IP为10.204.1.1。这种场景就涉及到另外一个话题了:跨Node间Pod通信。
图 4是K8s Overlay网络模型下,跨Node间Pod通信时的细节放大图。这节所说的两种Pod间通信时的路由情况都浓缩在这张图里了,供你参考。
图 4:VXLAN容器网络方案全景图
3.5 Session Affinity(会话保持)
此处优(故)雅(意)地省略1000字。
4. 延展思考
写完这篇,二哥想到我坚持的一个观点:新技术真的是层出不穷,但更多的时候,它的本质是用旧积木搭出了新造型。比如这篇我们讨论到的iptables、package flow和路由等知识点都已经出现了一二十年了,其中iptables诞生于2001年。
本文没有讨论类型为LoadBalancer和NodePort的Service场景,但它们的实现都依赖于ClusterIP这种类型的Service。本文也没有讨论Session Affinity(会话保持)功能,但它实现的基础还是iptables。二哥觉得把本文的知识掌握了,再去看这些进阶的部分,应该会比较容易。
实际上将流量从service导到一个Pod还有其它实现方法,比如Cilium就基于eBPF来实现更快速和高效的流量处理。