写点什么

一次客户需求引发的 K8s 网络探究

发布于: 2021 年 03 月 29 日


在本次案例中,京东云的中台技术工程师遇到了来自客户提出的打破 K8s 产品功能限制的特殊需求,面对这个极具挑战的任务,攻城狮最终是否克服了重重困难,帮助客户完美实现了需求?且看本期 K8s 技术案例分享!(友情提示:文章篇幅较长,建议各位看官先收藏再阅读,同时在阅读过程中注意劳逸结合,保持身心健康!)


第一部分:“颇有个性”的需求

某日,我们京东云的技术中台工程师接到了客户的求助。客户在云上环境使用了托管 K8s 集群产品部署测试集群。因业务需要,研发同事需要在办公网环境能直接访问 K8s 集群的 clueterIP 类型的 service 和后端的 pod。通常 K8s 的 pod 只能在集群内通过其他 pod 或者集群 node 访问,不能直接在集群外进行访问。而 pod 对集群内外提供服务时需要通过 service 对外暴露访问地址和端口,service 除了起到 pod 应用访问入口的作用,还会对 pod 的相应端口进行探活,实现健康检查。同时当后端有多个 Pod 时,service 还将根据调度算法将客户端请求转发至不同的 pod,实现负载均衡的作用。常用的 service 类型有如下几种:


service 类型简介

1、 clusterIP 类型,创建 service 时如果不指定类型的话的默认会创建该类型 service,clusterIP 类型的 service 只能在集群内通过 cluster IP 被 pod 和 node 访问,集群外无法访问。通常像 K8s 集群系统服务 kubernetes 等不需要对集群外提供服务,只需要在集群内部进行访问的 service 会使用这种类型;


2、 nodeport 类型,为了解决集群外部对 service 的访问需求,设计了 nodeport 类型,将 service 的端口映射至集群每个节点的端口上。当集群外访问 service 时,通过对节点 IP 和指定端口的访问,将请求转发至后端 pod;


3、 loadbalancer 类型,该类型通常需要调用云厂商的 API 接口,在云平台上创建负载均衡产品,并根据设置创建监听器。在 K8s 内部,loadbalancer 类型服务实际上还是和 nodeport 类型一样将服务端口映射至每个节点的固定端口上。然后将节点设置为负载均衡的后端,监听器将客户端请求转发至后端节点上的服务映射端口,请求到达节点端口后,再转发至后端 pod。Loadbalancer 类型的 service 弥补了 nodeport 类型有多个节点时客户端需要访问多个节点 IP 地址的不足,只要统一访问 LB 的 IP 即可。同时使用 LB 类型的 service 对外提供服务,K8s 节点无需绑定公网 IP,只需要给 LB 绑定公网 IP 即可,提升了节点安全性,也节约了公网 IP 资源。利用 LB 对后端节点的健康检查功能,可实现服务高可用。避免某个 K8s 节点故障导致服务无法访问。


Part1 小结


通过对 K8s 集群 service 类型的了解,我们可以知道客户想在集群外对 service 进行访问,首先推荐使用的是 LB 类型的 service。由于目前 K8s 集群产品的节点还不支持绑定公网 IP,因此使用 nodeport 类型的 service 无法实现通过公网访问,除非客户使用专线连接或者 IPSEC 将自己的办公网与云上网络打通,才能访问 nodeport 类型的 service。而对于 pod,只能在集群内部使用其他 pod 或者集群节点进行访问。同时 K8s 集群的 clusterIP 和 pod 设计为不允许集群外部访问,也是出于提高安全性的考虑。如果将访问限制打破,可能会导致安全问题发生。所以我们的建议客户还是使用 LB 类型的 service 对外暴露服务,或者从办公网连接 K8s 集群的 NAT 主机,然后通过 NAT 主机可以连接至 K8s 节点,再访问 clusterIP 类型的 service,或者访问后端 pod。


客户表示目前测试集群的 clusterIP 类型服务有上百个,如果都改造成 LB 类型的 service 就要创建上百个 LB 实例,绑定上百个公网 IP,这显然是不现实的,而都改造成 Nodeport 类型的 service 的工作量也十分巨大。同时如果通过 NAT 主机跳转登录至集群节点,就需要给研发同事提供 NAT 主机和集群节点的系统密码,不利于运维管理,从操作便利性上也不如研发可以直接通过网络访问 service 和 pod 简便。

第二部分:方法总比困难多?

虽然客户的访问方式违背了 K8s 集群的设计逻辑,显得有些“非主流”,但是对于客户的使用场景来说也是迫不得已的强需求。作为技术中台的攻城狮,我们要尽最大努力帮助客户解决技术问题!因此我们根据客户的需求和场景架构,来规划实现方案。


既然是网络打通,首先要从客户的办公网和云上 K8s 集群网络架构分析。客户办公网有统一的公网出口设备,而云上 K8s 集群的网络架构如下,K8s 集群 master 节点对用户不可见,用户创建 K8s 集群后,会在用户选定的 VPC 网络下创建三个子网。分别是用于 K8s 节点通讯的 node 子网,用于部署 NAT 主机和 LB 类型 serivce 创建的负载均衡实例的 NAT 与 LB 子网,以及用于 pod 通讯的 pod 子网。K8s 集群的节点搭建在云主机上,node 子网访问公网地址的路由下一跳指向 NAT 主机,也就是说集群节点不能绑定公网 IP,使用 NAT 主机作为统一的公网访问出口,做 SNAT,实现公网访问。由于 NAT 主机只有 SNAT 功能,没有 DNAT 功能,因此也就无法从集群外通过 NAT 主机访问 node 节点。


关于 pod 子网的规划目的,首先要介绍下 pod 在节点上的网络架构。如下图所示:



在节点上,pod 中的容器通过 veth 对与 docker0 设备连通,而 docker0 与节点的网卡之间通过自研 CNI 网络插件连通。为了实现集群控制流量与数据流量的分离,提高网络性能,集群在每个节点上单独绑定弹性网卡,专门供 pod 通讯使用。创建 pod 时,会在弹性网卡上为 Pod 分配 IP 地址。每个弹性网卡最多可以分配 21 个 IP,当一张弹性网卡上的 IP 分配满后,会再绑定一张新的网卡供后续新建的 pod 使用。弹性网卡所属的子网就是 pod 子网,基于这样的架构,可以降低节点 eth0 主网卡的负载压力,实现控制流量与数据流量分离,同时 pod 的 IP 在 VPC 网络中有实际对应的网络接口和 IP,可实现 VPC 网络内对 pod 地址的路由。


你需要了解的打通方式

了解完两端的网络架构后我们来选择打通方式。通常将云下网络和云上网络打通,有专线产品连接方式,或者用户自建 VPN 连接方式。专线产品连接需要布设从客户办公网到云上机房的网络专线,然后在客户办公网侧的网络出口设备和云上网络侧的 bgw 边界网关配置到彼此对端的路由。如下图所示:



基于现有专线产品 BGW 的功能限制,云上一侧的路由只能指向 K8s 集群所在的 VPC,无法指向具体的某个 K8s 节点。而想要访问 clusterIP 类型 service 和 pod,必须在集群内的节点和 pod 访问。因此访问 service 和 pod 的路由下一跳,必须是某个集群节点。所以使用专线产品显然是无法满足需求的。


我们来看自建 VPN 方式,自建 VPN 在客户办公网和云上网络各有一个有公网 IP 的端点设备,两个设备之间建立加密通讯隧道,实际底层还是基于公网通讯。如果使用该方案,云上的端点我们可以选择和集群节点在同一 VPC 的不同子网下的有公网 IP 的云主机。办公网侧对 service 和 pod 的访问数据包通过 VPN 隧道发送至云主机后,可以通过配置云主机所在子网路由,将数据包路由至某个集群节点,然后在集群节点所在子网配置到客户端的路由下一跳指向端点云主机,同时需要在 pod 子网也做相同的路由配置。至于 VPN 的实现方式,通过和客户沟通,我们选取 ipsec 隧道方式。


确定了方案,我们需要在测试环境实施方案验证可行性。由于我们没有云下环境,因此选取和 K8s 集群不同地域的云主机代替客户的办公网端点设备。在华东上海地域创建云主机 office-ipsec-sh 模拟客户办公网客户端,在华北北京地域的 K8s 集群 K8s-BJTEST01 所在 VPC 的 NAT/LB 子网创建一个有公网 IP 的云主机 K8s-ipsec-bj,模拟客户场景下的 ipsec 云上端点,与华东上海云主机 office-ipsec-sh 建立 ipsec 隧道。设置 NAT/LB 子网的路由表,添加到 service 网段的路由下一跳指向 K8s 集群节点 K8s-node-vmlppp-bs9jq8pua,以下简称 node A。由于 pod 子网和 NAT/LB 子网同属于一个 VPC,所以无需配置到 pod 网段的路由,访问 pod 时会直接匹配 local 路由,转发至对应的弹性网卡上。为了实现数据包的返回,在 node 子网和 pod 子网分别配置到上海云主机 office-ipsec-sh 的路由,下一跳指向 K8s-ipsec-bj。完整架构如下图所示:



第三部分:实践出“问题”

既然确定了方案,我们就开始搭建环境了。首先在 K8s 集群的 NAT/LB 子网创建 K8s-ipsec-bj 云主机,并绑定公网 IP。然后与上海云主机 office-ipsec-sh 建立 ipsec 隧道。关于 ipsec 部分的配置方法网络上有很多文档,在此不做详细叙述,有兴趣的童鞋可以参照文档自己实践下。隧道建立后,在两端互 ping 对端的内网 IP,如果可以 ping 通的话,证明 ipsec 工作正常。按照规划配置好 NAT/LB 子网和 node 子网以及 pod 子网的路由。我们在 K8s 集群的 serivce 中,选择一个名为 nginx 的 serivce,clusterIP 为 10.0.58.158,如图所示:



该服务后端的 pod 是 10.0.0.13,部署 nginx 默认页面,并监听 80 端口。在上海云主机上测试 ping service 的 IP 10.0.58.158,可以 ping 通,同时使用 paping 工具 ping 服务的 80 端口,也可以 ping 通!



使用 curl http://10.0.58.158进行 http 请求,也可以成功!



再测试直接访问后端 pod,也没有问题:)



正当攻城狮心里美滋滋,以为一切都大功告成的时候,测试访问另一个 service 的结果犹如一盆冷水泼来。我们接着选取了 mysql 这个 service,测试访问 3306 端口。该 serivce 的 clusterIP 是 10.0.60.80,后端 pod 的 IP 是 10.0.0.14。



在上海云主机直接 ping service 的 clusterIP,没有问题。但是 paping 3306 端口的时候,居然不通了!



然后我们测试直接访问 serivce 的后端 pod,诡异的是,后端 pod 无论是 ping IP 还是 paping 3306 端口,都是可以连通的!



肿么回事?

这是肿么回事?经过攻城狮一番对比分析,发现两个 serivce 唯一的不同是,可以连通 nginx 服务的后端 pod 10.0.0.13 就部署在客户端请求转发到的 node A 上。而不能连通的 mysql 服务的后端 pod 不在 node A 上,在另一个节点上。为了验证问题原因是否就在于此,我们单独修改 NAT/LB 子网路由,到 mysql 服务的下一跳指向后端 pod 所在的节点。然后再次测试。果然!现在可以访问 mysql 服务的 3306 端口了!



第四部分:三个为什么?

此时此刻,攻城狮的心中有三个疑问:

(1)为什么请求转发至 service 后端 pod 所在的节点时可以连通?

(2)为什么请求转发至 service 后端 pod 不在的节点时不能连通?

(3)为什么不管转发至哪个节点,service 的 IP 都可以 ping 通?


深入分析,消除问号

为了消除我们心中的小问号,我们就要深入分析,了解导致问题的原因,然后再对症下药。既然要排查网络问题,当然还是要祭出经典法宝——tcpdump 抓包工具。为了把焦点集中,我们对测试环境的架构进行了调整。上海到北京的 ipsec 部分维持现有架构不变,我们对 K8s 集群节点进行扩容,新建一个没有任何 pod 的空节点 K8s-node-vmcrm9-bst9jq8pua,以下简称 node B,该节点只做请求转发。修改 NAT/LB 子网路由,访问 service 地址的路由下一跳指向该节点。测试的 service 我们选取之前使用的 nginx 服务 10.0.58.158 和后端 pod 10.0.0.13,如下图所示:



当需要测试请求转发至 pod 所在节点的场景时,我们将 service 路由下一跳修改为 K8s-node-A 即可。


万事俱备,让我们开启解惑之旅!Go Go Go!


首先探究疑问 1 场景,我们在 K8s-node-A 上执行命令抓取与上海云主机 172.16.0.50 的包,命令如下:


tcpdump -i any host 172.16.0.50 -w /tmp/dst-node-client.cap


各位童鞋是否还记得我们之前提到过,在托管 K8s 集群中,所有 pod 的数据流量均通过节点的弹性网卡收发?在 K8s-node-A 上 pod 使用的弹性网卡是 eth1。我们首先在上海云主机上使用 curl 命令请求http://10.0.58.158,同时执行命令抓取 K8s-node-A 的 eth1 上是否有 pod 10.0.0.13 的包收发,命令如下:


tcpdump –i eth1 host 10.0.0.13


结果如下图:



并没有任何 10.0.0.13 的包从 eth1 收发,但此时上海云主机上的 curl 操作是可以请求成功的,说明 10.0.0.13 必然给客户端回包了,但是并没有通过 eth1 回包。那么我们将抓包范围扩大至全部接口,命令如下:


tcpdump -i any host 10.0.0.13


结果如下图:



可以看到这次确实抓到了 10.0.0.13 和 172.16.0.50 交互的数据包,为了便于分析,我们使用命令 tcpdump -i any host 10.0.0.13 -w /tmp/dst-node-pod.cap 将包输出为 cap 文件。


同时我们再执行 tcpdump -i any host 10.0.58.158,对 service IP 进行抓包。



可以看到 172.16.0.50 执行 curl 请求时可以抓到数据包,且只有 10.0.58.158 与 172.16.0.50 交互的数据包,不执行请求时没有数据包。由于这一部分数据包会包含在对 172.16.0.50 的抓包中,因此我们不再单独分析。


将针对 172.16.0.50 和 10.0.0.13 的抓包文件取出,使用 wireshark 工具进行分析,首先分析对客户端 172.16.0.50 的抓包,详情如下图所示:



可以发现客户端 172.16.0.50 先给 service IP 10.0.58.158 发了一个包,然后又给 pod IP 10.0.0.13 发了一个包,两个包的 ID,内容等完全一致。而最后回包时,pod 10.0.0.13 给客户端回了一个包,然后 service IP 10.0.58.158 也给客户端回了一个 ID 和内容完全相同的包。这是什么原因导致的呢?


通过之前的介绍,我们知道 service 将客户端请求转发至后端 pod,在这个过程中客户端请求的是 service 的 IP,然后 service 会做 DNAT(根据目的 IP 做 NAT 转发),将请求转发至后端的 pod IP。虽然我们抓包看到的是客户端发了两次包,分别发给 service 和 pod,实际上客户端并没有重新发包,而是由 service 完成了目的地址转换。而 pod 回包时,也是将包回给 service,然后再由 service 转发给客户端。因为是相同节点内请求,这一过程应该是在节点的内部虚拟网络中完成,所以我们在 pod 使用的 eth1 网卡上并没有抓到和客户端交互的任何数据包。再结合 pod 维度的抓包,我们可以看到针对 client 抓包时抓到的 http get 请求包在对 pod 的抓包中也能抓到,也验证了我们的分析。




那么 pod 是通过哪个网络接口进行收发包的呢?执行命令 netstat -rn 查看 node A 上的网络路由,我们有了如下发现:



在节点内,所有访问 10.0.0.13 的路由都指向了 cni34f0b149874 这个网络接口。很显然这个接口是 CNI 网络插件创建的虚拟网络设备。为了验证 pod 所有的流量是否都通过该接口收发,我们再次在客户端请求 service 地址,在 node A 以客户端维度和 pod 维度抓包,但是这次以 pod 维度抓包时,我们不再使用-i any 参数,而是替换为-i cni34f0b149874。抓包后分析对比,发现如我们所料,客户端对 pod 的所有请求包都能在对 cni34f0b149874 的抓包中找到,同时对系统中除了 cni34f0b149874 之外的其他网络接口抓包,均没有抓到与客户端交互的任何数据包。因此可以证明我们的推断正确。


综上所述,在客户端请求转发至 pod 所在节点时,数据通路如下图所示:



接下来我们探究最为关心的问题 2 场景,修改 NAT/LB 子网路由到 service 的下一跳指向新建节点 node B,如图所示



这次我们需要在 node B 和 node A 上同时抓包。在客户端还是使用 curl 方式请求 service 地址。在转发节点 node B 上,我们先执行命令 tcpdump -i eth0 host 10.0.58.158 抓取 service 维度的数据包,发现抓取到了客户端到 service 的请求包,但是 service 没有任何回包,如图所示:



各位童鞋可能会有疑惑,为什么抓取的是 10.0.58.158,但抓包中显示的目的端是该节点名?实际上这与 service 的实现机制有关。在集群中创建 service 后,集群网络组件会在各个节点上都选取一个随机端口进行监听,然后在节点的 iptables 中配置转发规则,凡是在节点内请求 service IP 均转发至该随机端口,然后由集群网络组件进行处理。所以在节点内访问 service 时,实际访问的是节点上的某个端口。如果将抓包导出为 cap 文件,可以看到请求的目的 IP 仍然是 10.0.58.158,如图所示:



这也解释了为什么 clusterIP 只能在集群内的节点或者 pod 访问,因为集群外的设备没有 K8s 网络组件创建的 iptables 规则,不能将请求 service 地址转为请求节点的端口,即使数据包发送至集群,由于 service 的 clusterIP 在节点的网络中实际是不存在的,因此会被丢弃。(奇怪的姿势又增长了呢)


回到问题本身,在转发节点上抓取 service 相关包,发现 service 没有像转发到 pod 所在节点时给客户端回包。我们再执行命令 tcpdump -i any host 172.16.0.50 -w /tmp/fwd-node-client.cap 以客户端维度抓包,包内容如下:



我们发现客户端请求转发节点 node B 上的 service 后,service 同样做了 DNAT,将请求转发到 node A 上的 10.0.0.13。但是在转发节点上没有收到 10.0.0.13 回给客户端的任何数据包,之后客户端重传了几次请求包,均没有回应。


那么 node A 是否收到了客户端的请求包呢?pod 又有没有给客户端回包呢?我们移步 node A 进行抓包。在 node B 上的抓包我们可以获悉 node A 上应该只有客户端 IP 和 pod IP 的交互,因此我们就从这两个维度抓包。根据之前抓包的分析结果,数据包进入节点内之后,应该通过虚拟设备 cni34f0b149874 与 pod 交互。而 node B 节点访问 pod 应该从 node A 的弹性网卡 eth1 进入节点,而不是 eth0,为了验证,首先执行命令 tcpdump -i eth0 host 172.16.0.50 和 tcpdump -i eth0 host 10.0.0.13,没有抓到任何数据包。



说明数据包没有走 eth0。再分别执行 tcpdump -i eth1 host 172.16.0.50 -w /tmp/dst-node-client-eth1.cap 和 tcpdump -i cni34f0b149874 host 172.16.0.50 -w /tmp/dst-node-client-cni.cap 抓取客户端维度数据包,对比发现数据包内容完全一致,说明数据包从 eth1 进入 Node A 后,通过系统内路由转发至 cni34f0b149874。数据包内容如下:



可以看到客户端给 pod 发包后,pod 给客户端回了包。执行 tcpdump -i eth1 host 10.0.0.13 -w /tmp/dst-node-pod-eth1.cap 和 tcpdump -i host 10.0.0.13 -w /tmp/dst-node-pod-cni.cap 抓取 pod 维度数据包,对比发现数据包内容完全一致,说明 pod 给客户端的回包通过 cni34f0b149874 发出,然后从 eth1 网卡离开 node A 节点。数据包内容也可以看到 pod 给客户端返回了包,但没有收到客户端对于返回包的回应,触发了重传。



那么既然 pod 的回包已经发出,为什么 node B 上没有收到回包,客户端也没有收到回包呢?查看 eth1 网卡所属的 pod 子网路由表,我们恍然大悟!



由于 pod 给客户端回包是从 node A 的 eth1 网卡发出的,所以虽然按照正常 DNAT 规则,数据包应该发回给 node B 上的 service 端口,但是受 eth1 子网路由表影响,数据包直接被“劫持”到了 K8s-ipsec-bj 这个主机上。而数据包到了这个主机上之后,由于没有经过 service 的转换,回包的源地址是 pod 地址 10.0.0.13,目的地址是 172.16.0.50,这个数据包回复的是源地址 172.16.0.50,目的地址 10.0.58.158 这个数据包。相当于请求包的目的地址和回复包的源地址不一致,对于 K8s-ipsec-bj 来说,只看到了 10.0.0.13 给 172.16.0.50 的 reply 包,但是没有收到过 172.16.0.50 给 10.0.0.13 的 request 包,云平台虚拟网络的机制是遇到只有 reply 包,没有 request 包的情况会将 request 包丢弃,避免利用地址欺骗发起网络攻击。所以客户端不会收到 10.0.0.13 的回包,也就无法完成对 service 的请求。在这个场景下,数据包的通路如下图所示:



此时客户端可以成功请求 pod 的原因也一目了然 ,请求 pod 的数据通路如下:



请求包和返回包的路径一致,都经过 K8s-ipsec-bj 节点且源目 IP 没有发生改变,因此 pod 可以连通。


看到这里,机智的童鞋可能已经想到,那修改 eth1 所属的 pod 子网路由,让去往 172.16.0.50 的数据包下一跳不发送到 K8s-ipsec-bj,而是返回给 K8s-node-B,不就可以让回包沿着来路原路返回,不会被丢弃吗?是的,经过我们的测试验证,这样确实可以使客户端成功请求服务。但是别忘了,用户还有一个需求是客户端可以直接访问后端 pod,如果 pod 回包返回给 node B,那么客户端请求 pod 时的数据通路是怎样的呢?



如图所示,可以看到客户端对 Pod 的请求到达 K8s-ipsec-bj 后,由于是同一 vpc 内的地址访问,所以遵循 local 路由规则直接转发到 node A eth1 网卡,而 pod 给客户端回包时,受 eth1 网卡路由控制,发送到了 node B 上。node B 之前没有收到过客户端对 pod 的 request 包,同样会遇到只有 reply 包没有 request 包的问题,所以回包被丢弃,客户端无法请求 pod。


至此,我们搞清楚了为什么客户端请求转发至 service 后端 pod 不在的节点上时无法成功访问 service 的原因。那么为什么在此时虽然请求 service 的端口失败,但是可以 ping 通 service 地址呢?攻城狮推断,既然 service 对后端的 pod 起到 DNAT 和负载均衡的作用,那么当客户端 ping service 地址时,ICMP 包应该是由 service 直接应答客户端的,即 service 代替后端 pod 答复客户端的 ping 包。为了验证我们的推断是否正确,我们在集群中新建一个没有关联任何后端的空服务,如图所示:



然后在客户端 ping 10.0.62.200,结果如下:



果不其然,即使 service 后端没有任何 pod,也可以 ping 通,因此证明 ICMP 包均为 service 代答,不存在实际请求后端 pod 时的问题,因此可以 ping 通。

第五部分:天无绝人之路

既然费尽周折找到了访问失败的原因,接下来我们就要想办法解决这个问题。事实上只要想办法让 pod 跨节点给客户端回包时隐藏自己的 IP,对外显示的是 service 的 IP,就可以避免包被丢弃。原理上类似于 SNAT(基于源 IP 的地址转换)。可以类比为没有公网 IP 的局域网设备有自己的内网 IP,当访问公网时需要通过统一的公网出口,而此时外部看到的客户端 IP 是公网出口的 IP,并不是局域网设备的内网 IP。实现 SNAT,我们首先会想到通过节点操作系统上的 iptables 规则。我们在 pod 所在节点 node A 上执行 iptables-save 命令,查看系统已有的 iptables 规则都有哪些。




敲黑板,注意啦

可以看到系统创建了近千条 iptables 规则,大多数与 K8s 有关。我们重点关注上图中的 nat 类型规则,发现了有如下几条引起了我们的注意:



首先看红框部分规则

-A KUBE-SERVICES -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP src,dst -j KUBE-MARK-MASQ


该规则表示如果访问的源地址或者目的地址是 cluster ip +端口,出于 masquerade 目的,将跳转至 KUBE-MARK-MASQ 链,masquerade 也就是地址伪装的意思!在 NAT 转换中会用到地址伪装。


接下来看蓝框部分规则


-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000


该规则表示对于数据包打上需要做地址伪装的标记 0x4000/0x4000。


最后看黄框部分规则


-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE


该规则表示对于标记为 0x4000/0x4000 需要做 SNAT 的数据包,将跳转至 MASQUERADE 链进行地址伪装。


这三条规则所做的操作貌似正是我们需要 iptables 帮我们实现的,但是从之前的测试来看显然这三条规则并没有生效。这是为什么呢?是否是 K8s 的网络组件里有某个参数控制着是否会对访问 clusterIP 时的数据包进行 SNAT?


这就要从负责 service 与 pod 之间网络代理转发的组件——kube-proxy 的工作模式和参数进行研究了。我们已经知道 service 会对后端 pod 进行负载均衡和代理转发,要想实现该功能,依赖的是 kube-proxy 组件,从名称上可以看出这是一个代理性质的网络组件。它以 pod 形式运行在每个 K8s 节点上,当以 service 的 clusterIP+端口方式访问时,通过 iptables 规则将请求转发至节点上对应的随机端口,之后请求由 kube-proxy 组件接手处理,通过 kube-proxy 内部的路由和调度算法,转发至相应的后端 Pod。最初,kube-proxy 的工作模式是 userspace(用户空间代理)模式,kube-proxy 进程在这一时期是一个真实的 TCP/UDP 代理,类似 HA Proxy。由于该模式在 1.2 版本 K8s 开始已被 iptables 模式取代,在此不做赘述,有兴趣的童鞋可以自行研究下。


1.2 版本引入的 iptables 模式作为 kube-proxy 的默认模式,kube-proxy 本身不再起到代理的作用,而是通过创建和维护对应的 iptables 规则实现 service 到 pod 的流量转发。但是依赖 iptables 规则实现代理存在无法避免的缺陷,在集群中的 service 和 pod 大量增加后,iptables 规则的数量也会急剧增加,会导致转发性能显著下降,极端情况下甚至会出现规则丢失的情况。


为了解决 iptables 模式的弊端,K8s 在 1.8 版本开始引入 IPVS(IP Virtual Server)模式。IPVS 模式专门用于高性能负载均衡,使用更高效的 hash 表数据结构,为大型集群提供了更好的扩展性和性能。比 iptables 模式支持更复杂的负载均衡调度算法等。托管集群的 kube-proxy 正是使用了 IPVS 模式。


但是 IPVS 模式无法提供包过滤,地址伪装和 SNAT 等功能,所以在需要使用这些功能的场景下,IPVS 还是要搭配 iptables 规则使用。等等,地址伪装和 SNAT,这不正是我们之前在 iptables 规则中看到过的?这也就是说,iptables 在不进行地址伪装和 SNAT 时,不会遵循相应的 iptables 规则,而一旦设置了某个参数开启地址伪装和 SNAT,之前看到的 iptables 规则就会生效!于是我们到 kubernetes 官网查找 kube-proxy 的工作参数,有了令人激动的发现:



好一个蓦然回首!攻城狮的第六感告诉我们,--masquerade-all 参数就是解决我们问题的关键!

第六部分:真·方法比困难多

我们决定测试开启下--masquerade-all 这个参数。kube-proxy 在集群中的每个节点上以 pod 形式运行,而 kube-proxy 的参数配置都以 configmap 形式挂载到 pod 上。我们执行 kubectl get cm -n kube-system 查看 kube-proxy 的 configmap,如图所示:



红框里的就是 kube-proxy 的配置 configmap,执行 kubectl edit cm kube-proxy-config-khc289cbhd -n kube-system 编辑这个 configmap,如图所示


找到了 masqueradeALL 参数,默认是 false,我们修改为 true,然后保存修改。


要想使配置生效,需要逐一删除当前的 kube-proxy pod,daemonset 会自动重建 pod,重建的 pod 会挂载修改过的 configmap,masqueradeALL 功能也就开启了。如图所示:



期待地搓手手

接下来激动人心的时刻到来了,我们将访问 service 的路由指向 node B,然后在上海客户端上执行 paping 10.0.58.158 -p 80 观察测试结果(期待地搓手手):


此情此景,不禁让攻城狮流下了欣喜的泪水……


再测试下 curl http://10.0.58.158 同样可以成功!奥力给~



再测试下直接访问后端 Pod,以及请求转发至 pod 所在节点,都没有问题。至此客户的需求终于卍解,长舒一口气!

大结局:知其所以然

虽然问题已经解决,但是我们的探究还没有结束。开启 masqueradeALL 参数后,service 是如何对数据包做 SNAT,避免了之前的丢包问题呢?还是通过抓包进行分析。


首先分析转发至 pod 不在的节点时的场景,客户端请求服务时,在 pod 所在节点对客户端 IP 进行抓包,没有抓到任何包。



说明开启参数后,到后端 pod 的请求不再是以客户端 IP 发起的。

在转发节点对 pod IP 进行抓包可以抓到转发节点的 service 端口与 pod 之间的交互包。


说明 pod 没有直接回包给客户端 172.16.0.50。这样看来,相当于客户端和 pod 互相不知道彼此的存在,所有交互都通过 service 来转发。


再在转发节点对客户端进行抓包,包内容如下:



同时在 pod 所在节点对 pod 进行抓包,包内容如下:



可以看到转发节点收到序号 708 的 curl 请求包后,在 pod 所在节点收到了序号相同的请求包,只不过源目 IP 从 172.16.0.50/10.0.58.158 转换为了 10.0.32.23/10.0.0.13。这里 10.0.32.23 是转发节点的内网 IP,实际上就是节点上 service 对应的随机端口,所以可以理解为源目 IP 转换为了 10.0.58.158/10.0.0.13。而回包时的流程相同,pod 发出序号 17178 的包,转发节点将相同序号的包发给客户端,源目 IP 从 10.0.0.13/10.0.58.158 转换为了 10.0.58.158/172.16.0.50


根据以上现象可以得知,service 对客户端和后端都做了 SNAT,可以理解为关闭了透传客户端源 IP 的负载均衡,即客户端和后端都不知道彼此的存在,只知道 service 的地址。该场景下的数据通路如下图:



对 Pod 的请求不涉及 SNAT 转换,与 masqueradeALL 参数不开启时是一样的,因此我们不再做分析。


当客户端请求转发至 pod 所在节点时,service 依然会进行 SNAT 转换,只不过这一过程均在节点内部完成。通过之前的分析我们也已经了解,客户端请求转发至 pod 所在节点时,是否进行 SNAT 对访问结果没有影响。

总结

至此对于客户的需求,我们可以给出现阶段最优的方案。当然在生产环境,为了业务安全和稳定,还是不建议用户将 clusterIP 类型服务和 pod 直接暴露在集群之外。同时 masqueradeALL 参数开启后,对集群网络性能和其他功能是否有影响也没有经过测试验证,在生产环境开启的风险是未知的,还需要谨慎对待。通过解决客户需求的过程,我们对 K8s 集群的 service 和 pod 网络机制有了一定程度的了解,并了解了 kube-proxy 的 masqueradeALL 参数,对今后的学习和运维工作还是受益匪浅的。


在此感谢各位童鞋阅读,如果能够对大家有所帮助,欢迎点赞转发,并关注我们的公众号,更多精彩内容会持续放送!


推荐阅读


欢迎点击【京东科技】,了解开发者社区

更多精彩技术实践与独家干货解析

欢迎关注【京东科技开发者】公众号


发布于: 2021 年 03 月 29 日阅读数: 18
用户头像

拥抱技术,与开发者携手创造未来! 2018.11.20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东科技开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
一次客户需求引发的K8s网络探究