ipvs (IP Virtual Server) 是基于 Netfilter 的,作为 linux 内核的一部分实现了传输层负载均衡,ipvs 集成在LVS(Linux Virtual Server)中,它在主机中运行,并在真实服务器集群前充当负载均衡器。ipvs 可以将对 TCP/UDP 服务的请求转发给后端的真实服务器,因此 ipvs 天然支持 Kubernetes Service。ipvs 也包含了多种不同的负载均衡算法,例如轮询、最短期望延迟、最少连接以及各种哈希方法等,ipvs 的设计就是用来为大规模服务进行负载均衡的。

ipvs 的负载均衡方式

ipvs 有三种负载均衡方式,分别为:

关于三种模式的原理可以参考:。

上面的负载均衡方式中只有 NAT 模式可以进行端口映射,因此 kubernetes 中 ipvs 的实现使用了 NAT 模式,用来将 service IP 和 service port 映射到后端的 container ip 和container port。

NAT 模式下的工作流程如下所示:

其具体流程为:当用户发起一个请求时,请求从 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 所工作的几个钩子函数:

ipset

IP sets 是 Linux 内核中的一个框架,可以由 ipset 命令进行管理。根据不同的类型,IP set 可以以某种方式保存 IP地址、网络、(TCP/UDP)端口号、MAC地址、接口名或它们的组合,并且能够快速匹配。

根据官网的介绍,若有以下使用场景:

  • 在保存了多个 IP 地址或端口号的 iptables 规则集合中想使用哈希查找;
  • 根据 IP 地址或端口动态更新 iptables 规则时希望在性能上无损;
  • 在使用 iptables 工具创建一个基于 IP 地址和端口的复杂规则时觉得非常繁琐;

此时,使用 ipset 工具可能是你最好的选择。

ipset 是 iptables 的一种扩展,在 iptables 中可以使用启用 ipset 模块,具体来说,ipvs 使用 ipset 来存储需要 NAT 或 masquared 时的 ip 和端口列表。在数据包过滤过程中,首先遍历 iptables 规则,在定义了使用 ipset 的条件下会跳转到 ipset 列表中进行匹配。

kube-proxy 的 ipvs 模式是在 2015 年由 k8s 社区的大佬 提出的(Try kube-proxy via ipvs instead of iptables or userspace),在 2017 年由华为云团队实现的()。前面的文章已经提到了,在kubernetes v1.8 中已经引入了 ipvs 模式。

NAT 表:

kube-proxy ipvs 模式源码分析 - 图1

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. $ modprobe -- ip_vs_rr
  3. $ modprobe -- ip_vs_wrr
  4. $ modprobe -- ip_vs_sh
  5. $ modprobe -- nf_conntrack_ipv4
  6. $ cut -f1 -d " " /proc/modules | grep -e ip_vs -e nf_conntrack_ipv4

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 网卡
  2. $ ip addr
  3. ......
  4. 4: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
  5. link/ether de:be:c0:73:bc:c7 brd ff:ff:ff:ff:ff:ff
  6. inet 10.96.0.1/32 brd 10.96.0.1 scope global kube-ipvs0
  7. valid_lft forever preferred_lft forever
  8. inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0
  9. valid_lft forever preferred_lft forever
  10. inet 10.97.4.140/32 brd 10.97.4.140 scope global kube-ipvs0
  11. valid_lft forever preferred_lft forever
  12. ......
  13. // 10.97.4.140 为 CLUSTER-IP 挂载在 kube-ipvs0 上
  14. $ kubectl get svc
  15. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  16. tenant-service ClusterIP 10.97.4.140 <none> 7000/TCP 23s
  17. // 10.97.4.140 后端的 realserver 分别为 10.244.1.2 和 10.244.1.3
  18. $ ipvsadm -L -n
  19. IP Virtual Server version 1.2.1 (size=4096)
  20. Prot LocalAddress:Port Scheduler Flags
  21. -> RemoteAddress:Port Forward Weight ActiveConn InActConn
  22. TCP 10.97.4.140:7000 rr
  23. -> 10.244.1.2:7000 Masq 1 0 0
  24. -> 10.244.1.3:7000 Masq 1 0 0

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
  2. -A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
  3. // 执行完 PREROUTING 规则,数据打上0x4000/0x4000的标记
  4. -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
  5. -A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst -j ACCEPT

KUBE-CLUSTER-IP 为 ipset 列表:

  1. # ipset list | grep -A 20 KUBE-CLUSTER-IP
  2. Name: KUBE-CLUSTER-IP
  3. Type: hash:ip,port
  4. Revision: 5
  5. Header: family inet hashsize 1024 maxelem 65536
  6. Size in memory: 352
  7. References: 2
  8. Members:
  9. 10.96.0.10,17:53
  10. 10.96.0.10,6:53
  11. 10.96.0.1,6:443
  12. 10.96.0.10,6:9153

然后会进入 INPUT:

如果进来的数据带有 0x8000/0x8000 标记则丢弃,若有 0x4000/0x4000 标记则正常执行:

  1. -A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
  2. -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

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
  2. -A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
  3. -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
  4. -A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODE-PORT

KUBE-NODE-PORT 对应的 ipset 列表:

  1. # ipset list | grep -B 10 KUBE-NODE-PORT
  2. Name: KUBE-NODE-PORT-TCP
  3. Type: bitmap:port
  4. Revision: 3
  5. Header: range 0-65535
  6. Size in memory: 8268
  7. References: 0

流入 INPUT 后与 ClusterIP 的访问方式相同。

kubernetes 版本:v1.16

  1. func NewProxier(......) {
  2. ......
  3. proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner", proxier.syncProxyRules, minSyncPeriod, syncPeriod, burstSyncs)
  4. ......
  5. }

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() {
  2. proxier.mu.Lock()
  3. defer proxier.mu.Unlock()
  4. serviceUpdateResult := proxy.UpdateServiceMap(proxier.serviceMap, proxier.serviceChanges)
  5. endpointUpdateResult := proxier.endpointsMap.Update(proxier.endpointsChanges)
  6. staleServices := serviceUpdateResult.UDPStaleClusterIP
  7. // 合并 service 列表
  8. for _, svcPortName := range endpointUpdateResult.StaleServiceNames {
  9. if svcInfo, ok := proxier.serviceMap[svcPortName]; ok && svcInfo != nil && svcInfo.Protocol() == v1.ProtocolUDP {
  10. staleServices.Insert(svcInfo.ClusterIP().String())
  11. for _, extIP := range svcInfo.ExternalIPStrings() {
  12. staleServices.Insert(extIP)
  13. }
  14. }
  15. ......

读取系统 iptables 到内存,创建自定义链以及 iptables 规则,创建 dummy interface kube-ipvs0,创建默认的 ipset 规则。

对每一个服务创建 ipvs 规则。根据 endpoint 列表,更新 KUBE-LOOP-BACK 的 ipset 列表。

  1. for svcName, svc := range proxier.serviceMap {
  2. svcInfo, ok := svc.(*serviceInfo)
  3. if !ok {
  4. ......
  5. }
  6. for _, e := range proxier.endpointsMap[svcName] {
  7. ep, ok := e.(*proxy.BaseEndpointInfo)
  8. if !ok {
  9. klog.Errorf("Failed to cast BaseEndpointInfo %q", e.String())
  10. continue
  11. }
  12. ......
  13. if valid := proxier.ipsetList[kubeLoopBackIPSet].validateEntry(entry); !valid {
  14. ......
  15. }
  16. proxier.ipsetList[kubeLoopBackIPSet].activeEntries.Insert(entry.String())
  17. }

对于 clusterIP 类型更新对应的 ipset 列表 KUBE-CLUSTER-IP。

  1. if valid := proxier.ipsetList[kubeClusterIPSet].validateEntry(entry); !valid {
  2. ......
  3. }
  4. proxier.ipsetList[kubeClusterIPSet].activeEntries.Insert(entry.String())
  5. ......
  6. if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
  7. ......
  8. }
  9. // 绑定 ClusterIP to dummy interface
  10. if err := proxier.syncService(svcNameString, serv, true); err == nil {
  11. // 同步 endpoints 信息
  12. if err := proxier.syncEndpoint(svcName, false, serv); err != nil {
  13. ......
  14. }
  15. } else {
  16. ......
  17. }

为 externalIP 创建 ipvs 规则。

  1. for _, externalIP := range svcInfo.ExternalIPStrings() {
  2. if local, err := utilproxy.IsLocalIP(externalIP); err != nil {
  3. ......
  4. } else if local && (svcInfo.Protocol() != v1.ProtocolSCTP) {
  5. ......
  6. if proxier.portsMap[lp] != nil {
  7. ......
  8. } else {
  9. socket, err := proxier.portMapper.OpenLocalPort(&lp)
  10. if err != nil {
  11. ......
  12. }
  13. replacementPortsMap[lp] = socket
  14. }
  15. }
  16. ......
  17. if valid := proxier.ipsetList[kubeExternalIPSet].validateEntry(entry); !valid {
  18. ......
  19. }
  20. proxier.ipsetList[kubeExternalIPSet].activeEntries.Insert(entry.String())
  21. ......
  22. if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
  23. ......
  24. }
  25. if err := proxier.syncService(svcNameString, serv, true); err == nil {
  26. ......
  27. if err := proxier.syncEndpoint(svcName, false, serv); err != nil {
  28. ......
  29. }
  30. } else {
  31. ......
  32. }
  33. }

为 load-balancer类型创建 ipvs 规则。

  1. for _, ingress := range svcInfo.LoadBalancerIPStrings() {
  2. if ingress != "" {
  3. ......
  4. if valid := proxier.ipsetList[kubeLoadBalancerSet].validateEntry(entry); !valid {
  5. ......
  6. }
  7. proxier.ipsetList[kubeLoadBalancerSet].activeEntries.Insert(entry.String())
  8. if svcInfo.OnlyNodeLocalEndpoints() {
  9. }
  10. if len(svcInfo.LoadBalancerSourceRanges()) != 0 {
  11. ......
  12. for _, src := range svcInfo.LoadBalancerSourceRanges() {
  13. ......
  14. }
  15. ......
  16. }
  17. ......
  18. if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
  19. ......
  20. }
  21. if err := proxier.syncService(svcNameString, serv, true); err == nil {
  22. ......
  23. if err := proxier.syncEndpoint(svcName, svcInfo.OnlyNodeLocalEndpoints(), serv); err != nil {
  24. }
  25. } else {
  26. ......
  27. }
  28. }
  29. }

为 nodePort 类型创建 ipvs 规则。

  1. if svcInfo.NodePort() != 0 {
  2. ......
  3. var lps []utilproxy.LocalPort
  4. for _, address := range nodeAddresses {
  5. ......
  6. lps = append(lps, lp)
  7. }
  8. for _, lp := range lps {
  9. if proxier.portsMap[lp] != nil {
  10. ......
  11. } else if svcInfo.Protocol() != v1.ProtocolSCTP {
  12. socket, err := proxier.portMapper.OpenLocalPort(&lp)
  13. if err != nil {
  14. ......
  15. }
  16. if lp.Protocol == "udp" {
  17. ......
  18. }
  19. }
  20. }
  21. switch protocol {
  22. case "tcp":
  23. ......
  24. case "udp":
  25. ......
  26. case "sctp":
  27. ......
  28. default:
  29. ......
  30. }
  31. if nodePortSet != nil {
  32. for _, entry := range entries {
  33. ......
  34. nodePortSet.activeEntries.Insert(entry.String())
  35. }
  36. }
  37. if svcInfo.OnlyNodeLocalEndpoints() {
  38. var nodePortLocalSet *IPSet
  39. switch protocol {
  40. case "tcp":
  41. nodePortLocalSet = proxier.ipsetList[kubeNodePortLocalSetTCP]
  42. case "udp":
  43. nodePortLocalSet = proxier.ipsetList[kubeNodePortLocalSetUDP]
  44. case "sctp":
  45. nodePortLocalSet = proxier.ipsetList[kubeNodePortLocalSetSCTP]
  46. default:
  47. ......
  48. }
  49. if nodePortLocalSet != nil {
  50. entryInvalidErr := false
  51. for _, entry := range entries {
  52. ......
  53. nodePortLocalSet.activeEntries.Insert(entry.String())
  54. }
  55. ......
  56. }
  57. }
  58. for _, nodeIP := range nodeIPs {
  59. ......
  60. if svcInfo.SessionAffinityType() == v1.ServiceAffinityClientIP {
  61. ......
  62. }
  63. if err := proxier.syncService(svcNameString, serv, false); err == nil {
  64. if err := proxier.syncEndpoint(svcName, svcInfo.OnlyNodeLocalEndpoints(), serv); err != nil {
  65. ......
  66. }
  67. } else {
  68. ......
  69. }
  70. }
  71. }
  72. }

同步 ipset 记录,清理 conntrack。

  1. for _, set := range proxier.ipsetList {
  2. set.syncIPSetEntries()
  3. }
  4. proxier.writeIptablesRules()
  5. proxier.iptablesData.Reset()
  6. proxier.iptablesData.Write(proxier.natChains.Bytes())
  7. proxier.iptablesData.Write(proxier.natRules.Bytes())
  8. proxier.iptablesData.Write(proxier.filterChains.Bytes())
  9. proxier.iptablesData.Write(proxier.filterRules.Bytes())
  10. err = proxier.iptables.RestoreAll(proxier.iptablesData.Bytes(), utiliptables.NoFlushTables, utiliptables.RestoreCounters)
  11. if err != nil {
  12. ......
  13. }
  14. ......
  15. proxier.deleteEndpointConnections(endpointUpdateResult.StaleEndpoints)

本文主要讲述了 kube-proxy ipvs 模式的原理与实现,iptables 模式与 ipvs 模式下在源码实现上有许多相似之处,但二者原理不同,理解了原理分析代码则更加容易,笔者对于 ipvs 的知识也是现学的,文中如有不当之处望指正。虽然 ipvs 的性能要比 iptables 更好,但社区中已有相关的文章指出 BPF(Berkeley Packet Filter) 比 ipvs 的性能更好,且 BPF 将要取代 iptables,至于下一步如何发展,让我们拭目以待。

参考:

https://bestsamina.github.io/posts/2018-10-19-ipvs-based-kube-proxy-4-scaled-k8s-lb/

https://blog.51cto.com/goome/2369150

https://segmentfault.com/a/1190000016333317