一文搞懂 Kubernetes 中的负载均衡

TL;DR: 文章主要涵盖了 Kubernetes 中的服务负载均衡,重点包括 kube-proxy、iptables、IPVS 的实现。

Kubernetes Service中的负载均衡

Kubernetes Service 并不是真实存在的, 没有进程监听Service的 IP 地址和端口。

可以通过访问 Kubernetes 集群中的任何节点并执行 netstat -ntlp 来检查是否存在这种情况,连IP地址都找不到。

Service的 IP 地址由控制器管理器中的控制平面(control plane)分配并存储在数据库(etcd)中,然后另一个组件kube-proxy使用相同的 IP 地址。

Kube-proxy 读取所有Service 的 IP 地址列表,并在每个节点中写入 iptables 规则的集合。 这些规则的意思是:"如果看到此服务 IP 地址,请重写请求并选择其中一个 Pod 作为目的地"。 Service IP 地址仅用作占位符 --- 这就是没有进程监听该 IP 地址或端口的原因。

iptables 是轮询吗? 答案是否定的,iptables 主要用于防火墙,它不是为负载均衡而设计的。

但是我们可以制定一组智能规则,使 iptables 的行为类似于负载均衡器。 这正是 Kubernetes 中使用的情况。

如果你有三个 Pod,kube-proxy 会编写以下规则: 选择 Pod 1 作为目的地的可能性为 33%。否则,转到下一条规则 选择 Pod 2 作为目的地的概率为 50%。否则,转到以下规则 选择 Pod 3 作为目的地(无概率) 复合概率是 Pod 1、Pod 2 和 Pod 3 都有三分之一的机会(33%)被选中。

实现的整体流程

  1. 增加了一个Service或者Pod,则使用informer通知 dnsController,通过CoreDNS做服务注册,注册后informer也通知kube-proxy写入到IPVS/iptables 中

  2. 当一个请求过来之后,通过DNS查询来发现Service/Pod

  3. Kube-proxy 通过 ipvs/iptables 的规则来重写Service的目的地址,实现负载均衡。

kube-proxy 的实现

kube-proxy 以 daemonSet的形式运行在集群中,维护每个节点的iptables信息。

ProxyServer

kube-proxy 通过命令行启动后,构建ProxyServer,这里默认策略是Iptables ,所以我们重点看构建出来的 iptables.Proxier

go 复制代码
func newProxyServer(config *kubeproxyconfig.KubeProxyConfiguration, master string, initOnly bool) (*ProxyServer, error) {
	s := &ProxyServer{Config: config}
	
	s.Proxier, err = s.createProxier(config, dualStackSupported, initOnly)
}

func (s *ProxyServer) createProxier(config *proxyconfigapi.KubeProxyConfiguration, dualStack, initOnly bool) (proxy.Provider, error) {
	if config.Mode == proxyconfigapi.ProxyModeIPTables {
		if dualStack {
			proxier, err = iptables.NewDualStackProxier()
		} else {
			proxier, err = iptables.NewProxier()
		}
	} else if config.Mode == proxyconfigapi.ProxyModeIPVS {

		if dualStack {
			proxier, err = ipvs.NewDualStackProxier()
		} else {
			proxier, err = ipvs.NewProxier()
		}
	}
	return proxier, nil
}

构建完成后,通过 Run() 方法启动服务监听,通过 informer 获取到服务最新的 Service 和 Endpoints ,并注册 EventHandler 来进行更新处理

go 复制代码
func (s *ProxyServer) Run() error {
	informerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.Config.ConfigSyncPeriod.Duration,
		informers.WithTweakListOptions(func(options *metav1.ListOptions) {
			options.LabelSelector = labelSelector.String()
		}))
	// 监听Service的变化
	serviceConfig := config.NewServiceConfig(informerFactory.Core().V1().Services(), s.Config.ConfigSyncPeriod.Duration)
	serviceConfig.RegisterEventHandler(s.Proxier)
	go serviceConfig.Run(wait.NeverStop)

	// 监听endPoints的变化
	endpointSliceConfig := config.NewEndpointSliceConfig(informerFactory.Discovery().V1().EndpointSlices(), s.Config.ConfigSyncPeriod.Duration)
	endpointSliceConfig.RegisterEventHandler(s.Proxier)
	go endpointSliceConfig.Run(wait.NeverStop)

	go s.Proxier.SyncLoop()

	return err
}

// 上面注册的Service Event Handler
func (proxier *Proxier) OnServiceAdd(service *v1.Service) {
	proxier.OnServiceUpdate(nil, service)
}

func (proxier *Proxier) OnServiceUpdate(oldService, service *v1.Service) {
	if proxier.serviceChanges.Update(oldService, service) && proxier.isInitialized() {
		proxier.Sync()
	}
}

func (proxier *Proxier) OnServiceDelete(service *v1.Service) {
	proxier.OnServiceUpdate(service, nil)
}

Proxier

具体干活的是 proixer ,构建出来后就会启动同步循环,负责更新endpointsMapsvcPortMap

go 复制代码
func NewProxier()(*Proxier, error){
	proxier.syncRunner = async.NewBoundedFrequencyRunner("sync-runner", proxier.syncProxyRules, minSyncPeriod, time.Hour, burstSyncs)
}

// 启动同步循环
func (proxier *Proxier) SyncLoop() {
	proxier.syncRunner.Loop(wait.NeverStop)
}

// BoundedFrequencyRunner 负责周期性执行任务或者是收到run信号后执行
func (bfr *BoundedFrequencyRunner) Loop(stop <-chan struct{}) {
	klog.V(3).Infof("%s Loop running", bfr.name)
	bfr.timer.Reset(bfr.maxInterval)
	for {
		select {
		case <-stop:
			bfr.stop()
			klog.V(3).Infof("%s Loop stopping", bfr.name)
			return
		case <-bfr.timer.C():
			bfr.tryRun()
		case <-bfr.run:
			bfr.tryRun()
		case <-bfr.retry:
			bfr.doRetry()
		}
	}
}

func (bfr *BoundedFrequencyRunner) Run() {
	select {
	case bfr.run <- struct{}{}:
	default:
	}
}

// 详细更新逻辑可以查看这个方法的代码,ipvs和 Iptables都有各自维护自己表信息的逻辑,都是执行命令进行更新维护
func (proxier *Proxier) syncProxyRules(){}

proxy.EndpointsMap 中就定义了ServicePortName的名字 map[ServicePortName][]Endpoint , 所以在更新的时候可以找到所有后端能提供服务的 endpoints

我们了解了 kube-proxy 如何维护 iptables之后,接下来来看看iptables 具体如何实现负载均衡的

通过一张图我们来回顾一下整个流程

我们通过ipvs proxier的实现类图,可以看到最终也是通过命令来执行规则的维护变更,这里不展开看代码,有兴趣的看 pkg/proxy/ipvs 或者 pkg/proxy/iptables包下代码继续深入了解

我们了解了 kube-proxy 如何维护 iptables之后,接下来来看看iptables 具体如何实现负载均衡的

iptables 实现负载均衡

netfilter 是真正的防火墙框架,位于内核空间中,iptables 用于操作 netfilter这个内核框架 ,netfilter / iptables 组成了Linux下的包过滤防火墙。

数据包经过防火墙实际上是经过了一组规则,匹配条件动作形成了一组规则。

不同阶段会有不同的链,如:PREROUTING、FORWARD、POSTROUTING

每条链上有多组规则,只要命中匹配条件,就会执行相应的动作。如匹配到Service目的地址是 10.244.10.1 就进行地址转换的动作。

iptables 动作有如下:

  • ACCEPT:允许数据包通过。
  • DROP:直接丢弃数据包,不给任何回应信息,这时候客户端会感觉自己的请求泥牛入海了,过了超时时间才会有反应。
  • REJECT:拒绝数据包通过,必要时会给数据发送端一个响应的信息,客户端刚请求就会收到拒绝的信息。
  • SNAT:源地址转换,解决内网用户用同一个公网地址上网的问题。
  • MASQUERADE:是SNAT的一种特殊形式,适用于动态的、临时会变的ip上。
  • DNAT:目标地址转换。
  • REDIRECT:在本机做端口映射。
  • LOG:在/var/log/messages文件中记录日志信息,然后将数据包传递给下一条规则,也就是说除了记录以外不对数据包做任何其他操作,仍然让下一条规则去匹配。

每条链上有一串规则,有些规则很相似,我们希望能够放在一起的话也会更方便管理,所以把规则的集合抽象成了表。

filter表:负责过滤功能,防火墙;内核模块:iptables_filter
nat表:network address translation,网络地址转换功能;内核模块:iptable_nat

mangle表:拆解报文,做出修改,并重新封装 的功能;iptable_mangle

raw表:关闭nat表上启用的连接追踪机制;iptable_raw

分离表和链的设计使得 iptables 更加灵活,能够根据需要对不同类型的数据包采取不同的处理方式。

数据包经过iptables 流程图如下:

由于地址的转发主要用到了 NAT (网络地址转化),我们重点来关注 nat 表

环境准备

使用 kubectl 创建出对应的资源 ,yaml文件如下:

yaml 复制代码
apiVersion: v1
kind: Namespace
metadata:
  name: nginx-test
---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: nginx-test
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.14.2
          ports:
            - containerPort: 80
---

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  namespace: nginx-test
spec:
  selector:
    app: nginx
  ports:
    - name: http
      port: 80
      targetPort: 80 # nginx pod 对应的端口

---

apiVersion: v1
kind: Service
metadata:
  name: nginx-service-np
  namespace: nginx-test
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
    - name: http
      port: 80
      targetPort: 80 # nginx pod 对应的端口
      nodePort: 36666 # 暴露的端口

提交后创建出来的信息如下:

  • Service ClusterIP:10.104.60.209
  • Service NodePort的ClusterIp: 10.104.122.7
  • nginx pod的三个ip地址:10.244.1.3:80, 10.244.2.234:80, 10.244.2.235:80

外部访问的NodePort的包会经过的数据链为:PREROUTING、FORWARD、POSTROUTING

通过命令行可以查看 nat 表的规则

iptables结果列说明
pkts (packets) : 代表已经匹配的数据包数量。这个字段显示有多少个数据包符合规则。
bytes : 代表已经匹配的数据包的总字节数。这个字段显示了规则匹配的数据包总共有多少字节。
target : 代表规则匹配后采取的动作。可能的值包括 ACCEPT(允许通过)、DROP(丢弃数据包)、RETURN(返回上一层处理)等。
prot (protocol) : 代表规则所应用的网络协议。可能的值包括 tcp、udp、icmp 等,也可以是 all 表示匹配所有协议。
opt (options) : 代表附加选项,显示特定规则的其他设置或条件。
in (input) : 代表数据包进入防火墙的网络接口。
out (output) : 代表数据包离开防火墙的网络接口。
source : 代表规则匹配的数据包的源地址或地址范围。
destination : 代表规则匹配的数据包的目标地址或地址范围。

首先查看 PREROUTING的链

shell 复制代码
$ iptables -h 
# --list    -L [chain [rulenum]] List the rules in a chain or all chains 列出所有规则
# --numeric     -n              numeric output of addresses and ports 将地址和端口数字化,方便我们查看

$ iptables -nvL PREROUTING -t nat # 查看PREROUTING的链
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
 224M   14G KUBE-SERVICES  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
 224M   14G DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

$ iptables -nvL KUBE-SERVICES -t nat # 查看 KUBE_SERVICES的链 ,这里可以看到我们上面的两个Service 规则 
pkts bytes target     prot opt in     out     source               destination
		0     0 KUBE-MARK-MASQ  tcp  --  *      *      !10.244.0.0/16        10.104.60.209        /* nginx-test/nginx-service:http cluster IP */ tcp dpt:80
    0     0 KUBE-SVC-JLLMT4ARS7XWDBFY  tcp  --  *      *       0.0.0.0/0            10.104.60.209        /* nginx-test/nginx-service:http cluster IP */ tcp dpt:80
    0     0 KUBE-MARK-MASQ  tcp  --  *      *      !10.244.0.0/16        10.104.122.7         /* nginx-test/nginx-service-np:http cluster IP */ tcp dpt:80
    0     0 KUBE-SVC-D66WLXX5FKD63X4W  tcp  --  *      *       0.0.0.0/0            10.104.122.7         /* nginx-test/nginx-service-np:http cluster IP */ tcp dpt:80

$ iptables -nvL KUBE-NODEPORTS -t nat |grep nginx # 查看NodePort规则的链
pkts bytes target     prot opt in     out     source               destination
    2   104 KUBE-MARK-MASQ  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* nginx-test/nginx-service-np:http */ tcp dpt:36666
    2   104 KUBE-SVC-D66WLXX5FKD63X4W  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* nginx-test/nginx-service-np:http */ tcp dpt:36666

$ iptables -nvL KUBE-MARK-MASQ -t nat # 这里我们看到它只有一条规则,使用mark命令,对数据包设置标记 0x4000,在KUBE-POSTROUTING链上有MARK标记的数据包进行一次MASQUERADE,即**SNAT,会用节点ip替换源ip地址。**
pkts bytes target     prot opt in     out     source               destination
    5   284 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK or 0x4000

$ iptables -nvL KUBE-SVC-D66WLXX5FKD63X4W -t nat # NodePort描述的Service,自定义的KUBE-SVC-D66WLXX5FKD63X4链通过概率实现了负载均衡
Chain KUBE-SVC-D66WLXX5FKD63X4W (2 references)
 pkts bytes target     prot opt in     out     source               destination
    1    52 KUBE-SEP-SPX6SLDO7E3RZ3D6  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* nginx-test/nginx-service-np:http */ statistic mode random probability 0.33333333349
    0     0 KUBE-SEP-D67LAGTWHT6R6X4R  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* nginx-test/nginx-service-np:http */ statistic mode random probability 0.50000000000
    1    52 KUBE-SEP-FKVKJM7KELS26OUG  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* nginx-test/nginx-service-np:http */

# 任意选取一条链进行查看,执行的动作是 DNAT,即目的地址的转换,将Service 的地址转换成具体 Pod的地址。
$ iptables -nvL KUBE-SEP-SPX6SLDO7E3RZ3D6 -t nat 
Chain KUBE-SEP-SPX6SLDO7E3RZ3D6 (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  all  --  *      *       10.244.1.3           0.0.0.0/0            /* nginx-test/nginx-service-np:http */
    1    52 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* nginx-test/nginx-service-np:http */ tcp to:10.244.1.3:80

$ iptables -nvL KUBE-SEP-D67LAGTWHT6R6X4R -t nat
Chain KUBE-SEP-D67LAGTWHT6R6X4R (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  all  --  *      *       10.244.2.234         0.0.0.0/0            /* nginx-test/nginx-service-np:http */
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* nginx-test/nginx-service-np:http */ tcp to:10.244.2.234:80

$ iptables -nvL KUBE-SEP-FKVKJM7KELS26OUG -t nat
Chain KUBE-SEP-FKVKJM7KELS26OUG (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  all  --  *      *       10.244.2.235         0.0.0.0/0            /* nginx-test/nginx-service-np:http */
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* nginx-test/nginx-service-np:http */ tcp to:10.244.2.235:80

在经过了PREROUTING链后,接下来会判断目的ip地址不是本机的ip地址,如果不是,则接下来会经过FORWARD链。在FORWARD链上,仅做了一件事情,就是将前面打了0x4000的数据包允许转发。

shell 复制代码
$ iptables -nvL FORWARD
Chain FORWARD (policy DROP 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
 203M  222G KUBE-FORWARD  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes forwarding rules */
   66  3600 KUBE-SERVICES  all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate NEW /* kubernetes service portals */

$ iptables -nvL KUBE-FORWARD # 标记 0x4000 的则接收 
Chain KUBE-FORWARD (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate INVALID
    0     0 ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes forwarding rules */ mark match 0x4000/0x4000
12201   15M ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes forwarding conntrack pod source rule */ ctstate RELATED,ESTABLISHED
    0     0 ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes forwarding conntrack pod destination rule */ ctstate RELATED,ESTABLISHED

最后处理结束后,POSTROUTING 会对数据包做一次 SNAT ,从而客户端拿不到Service的真实地址

shell 复制代码
$ iptables -nvL POSTROUTING -t nat # 可以看到查看 KUBE-POSTROUTING的链
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
  22M 1354M KUBE-POSTROUTING  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */
   19  1176 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0
  22M 1339M FLANNEL-POSTRTG  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* flanneld masq */

$ iptables -nvL KUBE-POSTROUTING -t nat # 查看到去除了0x4000的标记,并且对数据包执行 MASQUERADE 操作,即SNAT,会用节点ip替换源ip地址。
Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source               destination
 4098  251K RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            mark match ! 0x4000/0x4000
    8   480 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK xor 0x4000
    8   480 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ random-fully

SNAT 依赖 conntrack 记录的信息,将Service的地址重新替换回来,然后进行回包。另外这里如果不做MASQUERADE,流量发到目的的pod后,pod回包时目的地址为发起端的源地址,而发起端的源地址很可能是在k8s集群外部的,此时pod发回的包是不能回到发起端的。

至此一个数据包经过iptables实现了负载均衡,我们用以下图来进行总结:

iptables也存在比较明显的缺点:

  1. iptables规则特别乱,一旦出现问题非常难以排查
  2. 由于iptables规则是串行执行,算法复杂度为O(n),一旦iptables规则多了后,性能将非常差。
  3. iptables规则提供的负载均衡功能非常有限,不支持较为复杂的负载均衡算法。

所以有了IPVS来对这些缺点进行弥补。

IPVS

详细的内容可以参考 :github.com/kubernetes/...

IPVS(IP 虚拟服务器)实现传输层负载均衡,作为 Linux 内核的一部分。

IPVS模式与IPTABLES模式的区别如下:

  • IPVS 为大型集群提供了更好的可扩展性和性能。
  • IPVS 支持比 IPTABLES 更复杂的负载平衡算法(最小负载、最少连接、局部性、加权等)。
  • IPVS支持服务器健康检查、连接重试等。

IPVS proxier 使用 IPTABLES 进行数据包过滤、SNAT 。具体来说,IPVS proxier 将使用 ipset 来存储需要 DROP 或需要进行SNAT的流量,以确保 IPTABLES 规则的数量恒定,无论我们有多少服务,如果没有ipvs,每一个Service都会有相应的转发规则,大集群下性能会变差。

恭喜!至此我们已经学习了短连接的负载均衡,下一篇文章我会带大家看看长连接的负载均衡。

Reference

  1. learnk8s.io/kubernetes-...
  2. learnk8s.io/etcd-kubern...
  3. etcd.io/docs/v3.5/l...
  4. xigang.github.io/2019/07/21/...
  5. nigelpoulton.com/demystifyin...
  6. github.com/coredns/cor...
  7. 源码相关内容
    1. kube-proxy 源码分析: xiaorui.cc/archives/73...
    2. informer 源码分析: xiaorui.cc/archives/73...
    3. shareInformer源码分析:xiaorui.cc/archives/73...
    4. coredns :github.com/rfyiamcool/...
    5. CNI flannel: github.com/rfyiamcool/...
  8. github.com/kubernetes/...
  9. iptables
    1. 概念介绍:www.zsythink.net/archives/11...
    2. 动作 DNAT和SNAT:www.zsythink.net/archives/17...
    3. 让iptables 配置的像负载均衡器:scalingo.com/blog/iptabl...
    4. conntrack 实现 :arthurchiao.art/blog/conntr...
    5. iptables 规则分析:kuring.me/post/kube-p...
  10. ipvs: www.linuxvirtualserver.org/software/ip...
相关推荐
Cat_Rocky44 分钟前
kubernetes ingress粗浅学习
学习·容器·kubernetes
PH = 72 小时前
K8S集群部署Dashboard
云原生·容器·kubernetes
陈陈CHENCHEN5 小时前
【Kubernetes】Ubuntu 24.04 二进制方式部署 K8s
云原生·容器·kubernetes
成为你的宁宁6 小时前
【K8s Service 基础知识、五大类型应用机制及Endpoint 深度解析】
云原生·容器·kubernetes
老卢聊运维6 小时前
K8s 资源一直 Terminating?kubectl 强制删除完整实操手册
云原生·容器·kubernetes
眷蓝天6 小时前
Kubernetes Ingress 资源对象
云原生·容器·kubernetes
Nice_Fold6 小时前
Kubernetes Ingress 七层负载均衡与Nginx实现
nginx·kubernetes·负载均衡
ん贤7 小时前
Kubernetes入门
云原生·容器·kubernetes
shizhan_cloud7 小时前
K8S部署LNMP架构 ECShop
kubernetes
A-刘晨阳1 天前
K8s之负载均衡
linux·运维·容器·kubernetes·负载均衡