原文连接:fuckcloudnative.io/posts/ipvs-…python
Kubernetes
中的 Service
就是一组同 label 类型 Pod
的服务抽象,为服务提供了负载均衡和反向代理能力,在集群中表示一个微服务的概念。kube-proxy
组件则是 Service 的具体实现,了解了 kube-proxy 的工做原理,才能洞悉服务之间的通讯流程,再遇到网络不通时也不会一脸懵逼。算法
kube-proxy 有三种模式:userspace
、iptables
和 IPVS
,其中 userspace
模式不太经常使用。iptables
模式最主要的问题是在服务多的时候产生太多的 iptables 规则,非增量式更新会引入必定的时延,大规模状况下有明显的性能问题。为解决 iptables
模式的性能问题,v1.11 新增了 IPVS
模式(v1.8 开始支持测试版,并在 v1.11 GA),采用增量式更新,并能够保证 service 更新期间链接保持不断开。docker
目前网络上关于 kube-proxy
工做原理的文档几乎都是以 iptables
模式为例,不多说起 IPVS
,本文就来破例解读 kube-proxy IPVS 模式的工做原理。为了理解地更加完全,本文不会使用 Docker 和 Kubernetes,而是使用更加底层的工具来演示。后端
咱们都知道,Kubernetes 会为每一个 Pod 建立一个单独的网络命名空间 (Network Namespace) ,本文将会经过手动建立网络命名空间并启动 HTTP 服务来模拟 Kubernetes 中的 Pod。api
本文的目标是经过模拟如下的 Service
来探究 kube-proxy 的 IPVS
和 ipset
的工做原理:bash
apiVersion: v1
kind: Service
metadata:
name: app-service
spec:
clusterIP: 10.100.100.100
selector:
component: app
ports:
- protocol: TCP
port: 8080
targetPort: 8080
复制代码
跟着个人步骤,最后你就能够经过命令 curl 10.100.100.100:8080
来访问某个网络命名空间的 HTTP 服务。为了更好地理解本文的内容,推荐提早阅读如下的文章:markdown
注意:本文全部步骤皆是在 Ubuntu 20.04 中测试的,其余 Linux 发行版请自行测试。网络
首先须要开启 Linux 的路由转发功能:app
$ sysctl --write net.ipv4.ip_forward=1
复制代码
接下来的命令主要作了这么几件事:负载均衡
bridge_home
netns_dustin
和 netns_leah
bridge_home
netns_dustin
网络命名空间中的 veth 设备分配一个 IP 地址为 10.0.0.11
netns_leah
网络命名空间中的 veth 设备分配一个 IP 地址为 10.0.021
bridge_home
接口10.0.0.0/24
网段进行流量假装$ ip link add dev bridge_home type bridge
$ ip address add 10.0.0.1/24 dev bridge_home
$ ip netns add netns_dustin
$ mkdir -p /etc/netns/netns_dustin
echo "nameserver 114.114.114.114" | tee -a /etc/netns/netns_dustin/resolv.conf
$ ip netns exec netns_dustin ip link set dev lo up
$ ip link add dev veth_dustin type veth peer name veth_ns_dustin
$ ip link set dev veth_dustin master bridge_home
$ ip link set dev veth_dustin up
$ ip link set dev veth_ns_dustin netns netns_dustin
$ ip netns exec netns_dustin ip link set dev veth_ns_dustin up
$ ip netns exec netns_dustin ip address add 10.0.0.11/24 dev veth_ns_dustin
$ ip netns add netns_leah
$ mkdir -p /etc/netns/netns_leah
echo "nameserver 114.114.114.114" | tee -a /etc/netns/netns_leah/resolv.conf
$ ip netns exec netns_leah ip link set dev lo up
$ ip link add dev veth_leah type veth peer name veth_ns_leah
$ ip link set dev veth_leah master bridge_home
$ ip link set dev veth_leah up
$ ip link set dev veth_ns_leah netns netns_leah
$ ip netns exec netns_leah ip link set dev veth_ns_leah up
$ ip netns exec netns_leah ip address add 10.0.0.21/24 dev veth_ns_leah
$ ip link set bridge_home up
$ ip netns exec netns_dustin ip route add default via 10.0.0.1
$ ip netns exec netns_leah ip route add default via 10.0.0.1
$ iptables --table filter --append FORWARD --in-interface bridge_home --jump ACCEPT
$ iptables --table filter --append FORWARD --out-interface bridge_home --jump ACCEPT
$ iptables --table nat --append POSTROUTING --source 10.0.0.0/24 --jump MASQUERADE
复制代码
在网络命名空间 netns_dustin
中启动 HTTP 服务:
$ ip netns exec netns_dustin python3 -m http.server 8080
复制代码
打开另外一个终端窗口,在网络命名空间 netns_leah
中启动 HTTP 服务:
$ ip netns exec netns_leah python3 -m http.server 8080
复制代码
测试各个网络命名空间之间是否能正常通讯:
$ curl 10.0.0.11:8080
$ curl 10.0.0.21:8080
$ ip netns exec netns_dustin curl 10.0.0.21:8080
$ ip netns exec netns_leah curl 10.0.0.11:8080
复制代码
整个实验环境的网络拓扑结构如图:
为了便于调试 IPVS 和 ipset,须要安装两个 CLI 工具:
$ apt install ipset ipvsadm --yes
复制代码
本文使用的 ipset 和 ipvsadm 版本分别为
7.5-1~exp1
和1:1.31-1
。
下面咱们使用 IPVS
建立一个虚拟服务 (Virtual Service) 来模拟 Kubernetes 中的 Service :
$ ipvsadm \
--add-service \
--tcp-service 10.100.100.100:8080 \
--scheduler rr
复制代码
--tcp-service
来指定 TCP 协议,由于咱们须要模拟的 Service 就是 TCP 协议。目前 kube-proxy 只容许为全部 Service 指定同一个调度算法,将来将会支持为每个 Service 选择不一样的调度算法,详情可参考文章 IPVS-Based In-Cluster Load Balancing Deep Dive。
建立了虚拟服务以后,还得给它指定一个后端的 Real Server
,也就是后端的真实服务,即网络命名空间 netns_dustin
中的 HTTP 服务:
$ ipvsadm \
--add-server \
--tcp-service 10.100.100.100:8080 \
--real-server 10.0.0.11:8080 \
--masquerading
复制代码
该命令会将访问 10.100.100.100:8080
的 TCP 请求转发到 10.0.0.11:8080
。这里的 --masquerading
参数和 iptables 中的 MASQUERADE
相似,若是不指定,IPVS 就会尝试使用路由表来转发流量,这样确定是没法正常工做的。
译者注:因为 IPVS 未实现
POST_ROUTING
Hook 点,因此它须要 iptables 配合完成 IP 假装等功能。
测试是否正常工做:
$ curl 10.100.100.100:8080
复制代码
实验成功,请求被成功转发到了后端的 HTTP 服务!
上面只是在 Host 的网络命名空间中进行测试,如今咱们进入网络命名空间 netns_leah
中进行测试:
$ ip netns exec netns_leah curl 10.100.100.100:8080
复制代码
哦豁,访问失败!
要想顺利经过测试,只需将 10.100.100.100
这个 IP 分配给一个虚拟网络接口。至于为何要这么作,目前我还不清楚,我猜想多是由于网桥 bridge_home
不会调用 IPVS,而将虚拟服务的 IP 地址分配给一个网络接口则能够绕过这个问题。
Netfilter 是一个基于用户自定义的 Hook 实现多种网络操做的 Linux 内核框架。Netfilter 支持多种网络操做,好比包过滤、网络地址转换、端口转换等,以此实现包转发或禁止包转发至敏感网络。
针对 Linux 内核 2.6 及以上版本,Netfilter 框架实现了 5 个拦截和处理数据的系统调用接口,它容许内核模块注册内核网络协议栈的回调功能,这些功能调用的具体规则一般由 Netfilter 插件定义,经常使用的插件包括 iptables、IPVS 等,不一样插件实现的 Hook 点(拦截点)可能不一样。另外,不一样插件注册进内核时须要设置不一样的优先级,例如默认配置下,当某个 Hook 点同时存在 iptables 和 IPVS 规则时,iptables 会被优先处理。
Netfilter 提供了 5 个 Hook 点,系统内核协议栈在处理数据包时,每到达一个 Hook 点,都会调用内核模块中定义的处理函数。调用哪一个处理函数取决于数据包的转发方向,进站流量和出站流量触发的 Hook 点是不同的。
内核协议栈中预约义的回调函数有以下五个:
iptables 实现了全部的 Hook 点,而 IPVS 只实现了 LOCAL_IN
、LOCAL_OUT
、FORWARD
这三个 Hook 点。既然没有实现 PRE_ROUTING
,就不会在进入 LOCAL_IN 以前进行地址转换,那么数据包通过路由判断后,会进入 LOCAL_IN Hook 点,IPVS 回调函数若是发现目标 IP 地址不属于该节点,就会将数据包丢弃。
若是将目标 IP 分配给了虚拟网络接口,内核在处理数据包时,会发现该目标 IP 地址属于该节点,因而能够继续处理数据包。
固然,咱们不须要将 IP 地址分配给任何已经被使用的网络接口,咱们的目标是模拟 Kubernetes 的行为。Kubernetes 在这里建立了一个 dummy 接口,它和 loopback 接口相似,可是你能够建立任意多的 dummy 接口。它提供路由数据包的功能,但实际上又不进行转发。dummy 接口主要有两个用途:
看来 dummy 接口完美符合实验需求,那就建立一个 dummy 接口吧:
$ ip link add dev dustin-ipvs0 type dummy
复制代码
将虚拟 IP 分配给 dummy 接口 dustin-ipvs0
:
$ ip addr add 10.100.100.100/32 dev dustin-ipvs0
复制代码
到了这一步,仍然访问不了 HTTP 服务,还须要另一个黑科技:bridge-nf-call-iptables
。在解释 bridge-nf-call-iptables
以前,咱们先来回顾下容器网络通讯的基础知识。
Kubernetes 集群网络有不少种实现,有很大一部分都用到了 Linux 网桥:
无论是 iptables 仍是 ipvs 转发模式,Kubernetes 中访问 Service 都会进行 DNAT,将本来访问 ClusterIP:Port
的数据包 DNAT 成 Service 的某个 Endpoint (PodIP:Port)
,而后内核将链接信息插入 conntrack
表以记录链接,目的端回包的时候内核从 conntrack
表匹配链接并反向 NAT,这样原路返回造成一个完整的链接链路:
可是 Linux 网桥是一个虚拟的二层转发设备,而 iptables conntrack 是在三层上,因此若是直接访问同一网桥内的地址,就会直接走二层转发,不通过 conntrack:
Pod 访问 Service,目的 IP 是 Cluster IP,不是网桥内的地址,走三层转发,会被 DNAT 成 PodIP:Port。
若是 DNAT 后是转发到了同节点上的 Pod,目的 Pod 回包时发现目的 IP 在同一网桥上,就直接走二层转发了,没有调用 conntrack,致使回包时没有原路返回 (见下图)。
因为没有原路返回,客户端与服务端的通讯就不在一个 “频道” 上,不认为处在同一个链接,也就没法正常通讯。
启用 bridge-nf-call-iptables
这个内核参数 (置为 1),表示 bridge 设备在二层转发时也去调用 iptables 配置的三层规则 (包含 conntrack),因此开启这个参数就可以解决上述 Service 同节点通讯问题。
因此这里须要启用 bridge-nf-call-iptables
:
$ modprobe br_netfilter
$ sysctl --write net.bridge.bridge-nf-call-iptables=1
复制代码
如今再来测试一下连通性:
$ ip netns exec netns_leah curl 10.100.100.100:8080
复制代码
终于成功了!
虽然咱们能够从网络命名空间 netns_leah
中经过虚拟服务成功访问另外一个网络命名空间 netns_dustin
中的 HTTP 服务,但尚未测试过从 HTTP 服务所在的网络命名空间 netns_dustin
中直接经过虚拟服务访问本身,话很少说,直接测一把:
$ ip netns exec netns_dustin curl 10.100.100.100:8080
复制代码
啊哈?居然失败了,这又是哪里的问题呢?不要慌,开启 hairpin
模式就行了。那么什么是 hairpin
模式呢? 这是一个网络虚拟化技术中常提到的概念,也即交换机端口的VEPA模式。这种技术借助物理交换机解决了虚拟机间流量转发问题。很显然,这种状况下,源和目标都在一个方向,因此就是从哪里进从哪里出的模式。
怎么配置呢?很是简单,只需一条命令:
$ brctl hairpin bridge_home veth_dustin on
复制代码
再次进行测试:
$ ip netns exec netns_dustin curl 10.100.100.100:8080
复制代码
仍是失败了。。。
而后我花了一个下午的时间,终于搞清楚了启用混杂模式后为何仍是不能解决这个问题,由于混杂模式和下面的选项要一块儿启用才能对 IPVS 生效:
$ sysctl --write net.ipv4.vs.conntrack=1
复制代码
最后再测试一次:
$ ip netns exec netns_dustin curl 10.100.100.100:8080
复制代码
此次终于成功了,但我仍是不太明白为何启用 conntrack 能解决这个问题,有知道的大神欢迎留言告诉我!
译者注:IPVS 及其负载均衡算法只针对首个数据包,后继的包必须被
conntrack
表优先反转,若是没有conntrack
,IPVS 对于回来的包是没有任何办法的。能够经过conntrack -L
查看。
若是想让全部的网络命名空间都能经过虚拟服务访问本身,就须要在链接到网桥的全部 veth 接口上开启 hairpin
模式,这也太麻烦了吧。有一个办法能够不用配置每一个 veth 接口,那就是开启网桥的混杂模式。
什么是混杂模式呢?普通模式下网卡只接收发给本机的包(包括广播包)传递给上层程序,其它的包一概丢弃。混杂模式就是接收全部通过网卡的数据包,包括不是发给本机的包,即不验证MAC地址。
若是一个网桥开启了混杂模式,就等同于将全部链接到网桥上的端口(本文指的是 veth 接口)都启用了 hairpin
模式。能够经过如下命令来启用 bridge_home
的混杂模式:
$ ip link set bridge_home promisc on
复制代码
如今即便你把 veth 接口的 hairpin
模式关闭:
$ brctl hairpin bridge_home veth_dustin off
复制代码
仍然能够经过连通性测试:
$ ip netns exec netns_dustin curl 10.100.100.100:8080
复制代码
在文章开头准备实验环境的章节,执行了这么一条命令:
$ iptables \
--table nat \
--append POSTROUTING \
--source 10.0.0.0/24 \
--jump MASQUERADE
复制代码
这条 iptables 规则会对全部来自 10.0.0.0/24
的流量进行假装。然而 Kubernetes 并非这么作的,它为了提升性能,只对来自某些具体的 IP 的流量进行假装。
为了更加完美地模拟 Kubernetes,咱们继续改造规则,先把以前的规则删除:
$ iptables \
--table nat \
--delete POSTROUTING \
--source 10.0.0.0/24 \
--jump MASQUERADE
复制代码
而后添加针对具体 IP 的规则:
$ iptables \
--table nat \
--append POSTROUTING \
--source 10.0.0.11/32 \
--jump MASQUERADE
复制代码
果真,上面的全部测试都能经过。先别急着高兴,又有新问题了,如今只有两个网络命名空间,若是有不少个怎么办,每一个网络命名空间都建立这样一条 iptables 规则?我用 IPVS 是为了啥?就是为了防止有大量的 iptables 规则拖垮性能啊,如今岂不是又绕回去了。
不慌,继续从 Kubernetes 身上学习,使用 ipset
来解决这个问题。先把以前的 iptables 规则删除:
$ iptables \
--table nat \
--delete POSTROUTING \
--source 10.0.0.11/32 \
--jump MASQUERADE
复制代码
而后使用 ipset
建立一个集合 (set) :
$ ipset create DUSTIN-LOOP-BACK hash:ip,port,ip
复制代码
这条命令建立了一个名为 DUSTIN-LOOP-BACK
的集合,它是一个 hashmap
,里面存储了目标 IP、目标端口和源 IP。
接着向集合中添加条目:
$ ipset add DUSTIN-LOOP-BACK 10.0.0.11,tcp:8080,10.0.0.11
复制代码
如今无论有多少网络命名空间,都只须要添加一条 iptables 规则:
$ iptables \
--table nat \
--append POSTROUTING \
--match set \
--match-set DUSTIN-LOOP-BACK dst,dst,src \
--jump MASQUERADE
复制代码
网络连通性测试也没有问题:
$ curl 10.100.100.100:8080
$ ip netns exec netns_leah curl 10.100.100.100:8080
$ ip netns exec netns_dustin curl 10.100.100.100:8080
复制代码
最后,咱们把网络命名空间 netns_leah
中的 HTTP 服务也添加到虚拟服务的后端:
$ ipvsadm \
--add-server \
--tcp-service 10.100.100.100:8080 \
--real-server 10.0.0.21:8080 \
--masquerading
复制代码
再向 ipset 的集合 DUSTIN-LOOP-BACK
中添加一个条目:
$ ipset add DUSTIN-LOOP-BACK 10.0.0.21,tcp:8080,10.0.0.21
复制代码
终极测试来了,试着多运行几回如下的测试命令:
$ curl 10.100.100.100:8080
复制代码
你会发现轮询算法起做用了:
相信经过本文的实验和讲解,你们应该理解了 kube-proxy IPVS 模式的工做原理。在实验过程当中,咱们还用到了 ipset,它有助于解决在大规模集群中出现的 kube-proxy 性能问题。若是你对这篇文章有任何疑问,欢迎和我进行交流。