前几篇文章已经分析了 service 的原理以及 kube-proxy iptables 模式的原理与实现,本篇文章会继续分析 kube-proxy ipvs 模式的原理与实现。
ipvs
ipvs (IP Virtual Server) 是基于 Netfilter 的,作为 linux 内核的一部分实现了传输层负载均衡,ipvs 集成在LVS(Linux Virtual Server)中,它在主机中运行,并在真实服务器集群前充当负载均衡器。ipvs 可以将对 TCP/UDP 服务的请求转发给后端的真实服务器,因此 ipvs 天然支持 Kubernetes Service。ipvs 也包含了多种不同的负载均衡算法,例如轮询、最短期望延迟、最少连接以及各种哈希方法等,ipvs 的设计就是用来为大规模服务进行负载均衡的。
ipvs 的负载均衡方式
ipvs 有三种负载均衡方式,分别为:
关于三种模式的原理可以参考:LVS 配置小结。
上面的负载均衡方式中只有 NAT 模式可以进行端口映射,因此 kubernetes 中 ipvs 的实现使用了 NAT 模式,用来将 service IP 和 service port 映射到后端的 container ip 和container port。
NAT 模式下的工作流程如下所示:
1 | +--------+ |
其具体流程为:当用户发起一个请求时,请求从 VIP 接口流入,此时数据源地址是 CIP,目标地址是 VIP,当接收到请求后拆掉 mac 地址封装后看到目标 IP 地址就是自己,按照正常流程会通过 INPUT 转入用户空间,但此时工作在 INPUT 链上的 ipvs 会强行将数据转到 POSTROUTING 链上,并根据相应的负载均衡算法选择后端具体的服务器,再通过 DNAT 转发给 Real server,此时源地址 CIP,目标地址变成了 RIP。
ipvs 与 iptables 的区别与联系
区别:
- 底层数据结构:iptables 使用链表,ipvs 使用哈希表
- 负载均衡算法:iptables 只支持随机、轮询两种负载均衡算法而 ipvs 支持的多达 8 种;
- 操作工具:iptables 需要使用 iptables 命令行工作来定义规则,ipvs 需要使用 ipvsadm 来定义规则。
此外 ipvs 还支持 realserver 运行状况检查、连接重试、端口映射、会话保持等功能。
联系:
ipvs 和 iptables 都是基于 netfilter内核模块,两者都是在内核中的五个钩子函数处工作,下图是 ipvs 所工作的几个钩子函数:
关于 kube-proxy iptables 与 ipvs 模式的区别,更多详细信息可以查看官方文档:https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/README.md。
ipset
IP sets 是 Linux 内核中的一个框架,可以由 ipset 命令进行管理。根据不同的类型,IP set 可以以某种方式保存 IP地址、网络、(TCP/UDP)端口号、MAC地址、接口名或它们的组合,并且能够快速匹配。
根据官网的介绍,若有以下使用场景:
- 在保存了多个 IP 地址或端口号的 iptables 规则集合中想使用哈希查找;
- 根据 IP 地址或端口动态更新 iptables 规则时希望在性能上无损;
- 在使用 iptables 工具创建一个基于 IP 地址和端口的复杂规则时觉得非常繁琐;
此时,使用 ipset 工具可能是你最好的选择。
ipset 是 iptables 的一种扩展,在 iptables 中可以使用-m set
启用 ipset 模块,具体来说,ipvs 使用 ipset 来存储需要 NAT 或 masquared 时的 ip 和端口列表。在数据包过滤过程中,首先遍历 iptables 规则,在定义了使用 ipset 的条件下会跳转到 ipset 列表中进行匹配。
kube-proxy ipvs 模式
kube-proxy 的 ipvs 模式是在 2015 年由 k8s 社区的大佬 thockin 提出的(Try kube-proxy via ipvs instead of iptables or userspace),在 2017 年由华为云团队实现的(Implement IPVS-based in-cluster service load balancing)。前面的文章已经提到了,在kubernetes
v1.8 中已经引入了 ipvs 模式。
kube-proxy 在 ipvs 模式下自定义了八条链,分别为 KUBE-SERVICES、KUBE-FIREWALL、KUBE-POSTROUTING、KUBE-MARK-MASQ、KUBE-NODE-PORT、KUBE-MARK-DROP、KUBE-FORWARD、KUBE-LOAD-BALANCER ,如下所示:
NAT 表:
Filter 表:
此外,由于 linux 内核原生的 ipvs 模式只支持 DNAT,不支持 SNAT,所以,在以下几种场景中 ipvs 仍需要依赖 iptables 规则:
- 1、kube-proxy 启动时指定
–-masquerade-all=true
参数,即集群中所有经过 kube-proxy 的包都做一次 SNAT; - 2、kube-proxy 启动时指定
--cluster-cidr=
参数; - 3、对于 Load Balancer 类型的 service,用于配置白名单;
- 4、对于 NodePort 类型的 service,用于配置 MASQUERADE;
- 5、对于 externalIPs 类型的 service;
但对于 ipvs 模式的 kube-proxy,无论有多少 pod/service,iptables 的规则数都是固定的。
ipvs 模式的启用
1、首先要加载 IPVS 所需要的 kernel module
1 | $ modprobe -- ip_vs |
2、在启动 kube-proxy 时,指定 proxy-mode 参数
1 | --proxy-mode=ipvs |
(如果要使用其他负载均衡算法,可以指定 --ipvs-scheduler=
参数,默认为 rr)
当创建 ClusterIP type 的 service 时,IPVS proxier 会执行以下三个操作:
- 确保本机已创建 dummy 网卡,默认为 kube-ipvs0。为什么要创建 dummy 网卡?因为 ipvs netfilter 的 DNAT 钩子挂载在 INPUT 链上,当访问 ClusterIP 时,将 ClusterIP 绑定在 dummy 网卡上为了让内核识别该 IP 就是本机 IP,进而进入 INPUT 链,然后通过钩子函数 ip_vs_in 转发到 POSTROUTING 链;
- 将 ClusterIP 绑定到 dummy 网卡;
- 为每个 ClusterIP 创建 IPVS virtual servers 和 real server,分别对应 service 和 endpoints;
例如下面的示例:
1 | // kube-ipvs0 dummy 网卡 |
ipvs 模式下数据包的流向
clusterIP 访问方式
1 | PREROUTING --> KUBE-SERVICES --> KUBE-CLUSTER-IP --> INPUT --> KUBE-FIREWALL --> POSTROUTING |
- 首先进入 PREROUTING 链
- 从 PREROUTING 链会转到 KUBE-SERVICES 链,10.244.0.0/16 为 ClusterIP 网段
- 在 KUBE-SERVICES 链打标记
- 从 KUBE-SERVICES 链再进入到 KUBE-CLUSTER-IP 链
- KUBE-CLUSTER-IP 为 ipset 集合,在此处会进行 DNAT
- 然后会进入 INPUT 链
- 从 INPUT 链会转到 KUBE-FIREWALL 链,在此处检查标记
- 在 INPUT 链处,ipvs 的 LOCAL_IN Hook 发现此包在 ipvs 规则中则直接转发到 POSTROUTING 链
1 | -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES |
KUBE-CLUSTER-IP 为 ipset 列表:
1 | # ipset list | grep -A 20 KUBE-CLUSTER-IP |
然后会进入 INPUT:
1 | -A INPUT -j KUBE-FIREWALL |
如果进来的数据带有 0x8000/0x8000 标记则丢弃,若有 0x4000/0x4000 标记则正常执行:
1 | -A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING |
nodePort 方式
1 | PREROUTING --> KUBE-SERVICES --> KUBE-NODE-PORT --> INPUT --> KUBE-FIREWALL --> POSTROUTING |
- 首先进入 PREROUTING 链
- 从 PREROUTING 链会转到 KUBE-SERVICES 链
- 在 KUBE-SERVICES 链打标记
- 从 KUBE-SERVICES 链再进入到 KUBE-NODE-PORT 链
- KUBE-NODE-PORT 为 ipset 集合,在此处会进行 DNAT
- 然后会进入 INPUT 链
- 从 INPUT 链会转到 KUBE-FIREWALL 链,在此处检查标记
- 在 INPUT 链处,ipvs 的 LOCAL_IN Hook 发现此包在 ipvs 规则中则直接转发到 POSTROUTING 链
1 | -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES |
KUBE-NODE-PORT 对应的 ipset 列表:
1 | # ipset list | grep -B 10 KUBE-NODE-PORT |
流入 INPUT 后与 ClusterIP 的访问方式相同。
kube-proxy ipvs 源码分析
kubernetes 版本:v1.16
在前面的文章中已经介绍过 ipvs 的初始化了,下面直接看其核心方法:proxier.syncRunner。
1 | func NewProxier(......) { |
proxier.syncRunner()
执行流程:
- 通过 iptables-save 获取现有的 Filter 和 NAT 表存在的链数据
- 创建自定义链与规则
- 创建 Dummy 接口和 ipset 默认列表
- 为每个服务生成 ipvs 规则
- 对 serviceMap 内的每个服务进行遍历处理,对不同的服务类型(clusterip/nodePort/externalIPs/load-balancer)进行不同的处理(ipset 列表/vip/ipvs 后端服务器)
- 根据 endpoint 列表,更新 KUBE-LOOP-BACK 的 ipset 列表
- 若为 clusterIP 类型更新对应的 ipset 列表 KUBE-CLUSTER-IP
- 若为 externalIPs 类型更新对应的 ipset 列表 KUBE-EXTERNAL-IP
- 若为 load-balancer 类型更新对应的 ipset 列表 KUBE-LOAD-BALANCER、KUBE-LOAD-BALANCER-LOCAL、KUBE-LOAD-BALANCER-FW、KUBE-LOAD-BALANCER-SOURCE-CIDR、KUBE-LOAD-BALANCER-SOURCE-IP
- 若为 NodePort 类型更新对应的 ipset 列表 KUBE-NODE-PORT-TCP、KUBE-NODE-PORT-LOCAL-TCP、KUBE-NODE-PORT-LOCAL-SCTP-HASH、KUBE-NODE-PORT-LOCAL-UDP、KUBE-NODE-PORT-SCTP-HASH、KUBE-NODE-PORT-UDP
- 同步 ipset 记录
- 刷新 iptables 规则
1 | func (proxier *Proxier) syncProxyRules() { |
读取系统 iptables 到内存,创建自定义链以及 iptables 规则,创建 dummy interface kube-ipvs0,创建默认的 ipset 规则。
1 | proxier.natChains.Reset() |
对每一个服务创建 ipvs 规则。根据 endpoint 列表,更新 KUBE-LOOP-BACK 的 ipset 列表。
1 | for svcName, svc := range proxier.serviceMap { |
对于 clusterIP 类型更新对应的 ipset 列表 KUBE-CLUSTER-IP。
1 | if valid := proxier.ipsetList[kubeClusterIPSet].validateEntry(entry); !valid { |
为 externalIP 创建 ipvs 规则。
1 | for _, externalIP := range svcInfo.ExternalIPStrings() { |
为 load-balancer类型创建 ipvs 规则。
1 | for _, ingress := range svcInfo.LoadBalancerIPStrings() { |
为 nodePort 类型创建 ipvs 规则。
1 | if svcInfo.NodePort() != 0 { |
同步 ipset 记录,清理 conntrack。
1 | for _, set := range proxier.ipsetList { |
总结
本文主要讲述了 kube-proxy ipvs 模式的原理与实现,iptables 模式与 ipvs 模式下在源码实现上有许多相似之处,但二者原理不同,理解了原理分析代码则更加容易,笔者对于 ipvs 的知识也是现学的,文中如有不当之处望指正。虽然 ipvs 的性能要比 iptables 更好,但社区中已有相关的文章指出 BPF(Berkeley Packet Filter) 比 ipvs 的性能更好,且 BPF 将要取代 iptables,至于下一步如何发展,让我们拭目以待。
参考:
http://www.austintek.com/LVS/LVS-HOWTO/HOWTO/LVS-HOWTO.filter_rules.html
https://bestsamina.github.io/posts/2018-10-19-ipvs-based-kube-proxy-4-scaled-k8s-lb/
https://www.bookstack.cn/read/k8s-source-code-analysis/core-kube-proxy-ipvs.md
https://blog.51cto.com/goome/2369150
https://xigang.github.io/2019/07/21/kubernetes-service/
https://segmentfault.com/a/1190000016333317
https://cilium.io/blog/2018/04/17/why-is-the-kernel-community-replacing-iptables/