浅谈Kubernetes Service负载均衡实现机制
Kubernetes Serivce是一组具有相同label Pod集合的抽象(可以简单的理解为集群内的LB),集群内外的各个服务可以通过Service进行互相通信。但是Service的类型有多种,每种类型的Service适合怎样的场景以及kube-proxy是如何实现Service负载均衡的将是本文讨论的重点。
Service和kube-proxy在kubernetes集群中的工作原理
在介绍Service和kube-proxy之前,先绍下它们在Kubernetes集群中所起到的作用,我觉得很有必要。
让我们分析下上面这张图:
- 运行在每个Node节点的kube-proxy会实时的watch Services和 Endpoints对象。
当用户在kubernetes集群中创建了含有label的Service之后,同时会在集群中创建出一个同名的Endpoints对象,用于存储该Service下的Pod IP. 它们的关系如下图所示:
2.每个运行在Node节点的kube-proxy感知到Services和Endpoints的变化之后,会在各自的Node节点设置相关的iptables或IPVS规则,用于之后用户通过Service的ClusterIP去访问该Service下的服务。
3.当kube-proxy把需要的规则设置完成之后,用户便可以在集群内的Node或客户端Pod上通过ClusterIP经过iptables或IPVS设置的规则进行路由和转发,最终将客户端请求发送到真实的后端Pod。
对于kube-proxy如何设置Iptables和IPVS策略后续会讲。接下来先介绍下每种不同类型的Service的使用场景。
Service 类型
当前Kubernetes Service支持如下几种类型,并在介绍类型的同时便可以了解每种类型的Service的具体使用场景。
ClusterIP
ClusterIP类型的Service是Kubernetes集群默认的Service, 它只能用于集群内部通信。不能用于外部通信。
ClusterIP Service类型的结构如下图所示:
NodePort
如果你想要在集群外访问集群内部的服务,你可以使用这种类型的Service。NodePort类型的Service会在集群内部的所有Node节点打开一个指定的端口。之后所有的流量直接发送到这个端口之后,就会转发的Service去对真实的服务进行访问。
NodePort Service类型的结构如下图所示:
LoadBalancer
LoadBalancer类型的Service通常和云厂商的LB结合一起使用,用于将集群内部的服务暴露到外网,云厂商的LoadBalancer会给用户分配一个IP,之后通过该IP的流量会转发到你的Service.
LoadBalancer Service类型的结构如下图所示:
Ingress
Ingress 其实不是Service的一个类型,但是它可以作用于多个Service,作为集群内部服务的入口。
Ingress 能做许多不同的事,比如根据不同的路由,将请求转发到不同的Service上等等。
Ingress 的结构如下图所示:
Service 服务发现
Service当前支持两种类型的服务发现机制,一种是通过环境变量,另一种是通过DNS。在这两种方案中,建议使用后者。
环境变量
当一个Pod创建完成之后,kubelet会在该Pod中注册该集群已经创建的所有Service相关的环境变量,但是需要注意的是,Service创建之前的所有的POD是不会注册该Service的环境变量的,所以在平时使用时,建议通过DNS的方式进行Service之间的服务发现。
DNS
可以在集群中部署CoreDNS服务(旧版本的kubernetes集群使用的是kubeDNS), 来达到集群内部的Pod通过DNS的方式进行集群内部各个服务之间的通讯。
当前kubernetes集群默认使用CoreDNS作为默认的DNS服务,主要原因是CoreDNS是基于Plugin的方式进行扩展的简单,灵活。并且不完全被Kubernetes所捆绑。
Service 负载均衡
在本文的最初已经介绍了service和kube-proxy在集群中是如何配合来达到服务的负载均衡。kube-proxy在其中起到了关键性的作用,kube-proxy作为一个控制器,作为k8s和Linux kernel Netfilter交互的一个枢纽。监听kubernetes集群Services和Endpoints对象的变化,并根据kube-proxy不同的模式(iptables or ipvs), 对内核设置不同的规则,来实现路由转发。接下来分别介绍下kube-proxy基于Iptables和IPVS两种模式实现Service负载均衡的工作机制。
Iptables 实现负载均衡
Iptables)是一个用户态程序,通过配置Netfilter规则来构建Linux内核防火墙。Netfilter是Linux内核的网络包管理框架,提供了一整套的hook函数的管理机制,使得诸如数据包过滤,网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能,Netfilter在内核中的位置如下图所示。
本文不会更多的介绍关于Iptales和Netfilter的细节,如果想更多的了解请查看:Iptables)和 什么是Netfilter
接下来介绍kube-proxy是如何利用Iptables做负载均衡的。数据包在Iptables中的匹配流程如下图所示:
在Iptables模式下,kube-proxy通过在目标node节点上的Iptables中的NAT表的PREROUTIN和POSTROUTING链中创建一系列的自定义链(这些自定义链主要是”KUBE-SERVICE”链, “KUBE-POSTROUTING”链,每个服务对应的”KUBE-SVC-XXXXXX”链和”KUBE-SEP-XXXX”链),然后通过这些自定义链对流经到该Node的数据包做DNAT和SNAT操作从而实现路由,负载均衡和地址转化,如下图所示:
kube-proxy中,客户端的请求数据包在Iptables规则中具体的匹配过程为:
1.PREROUTING链或者OUTPUT链(集群内的Pod通过clusterIP访问Service时经过OUTPUT链, 而当集群外主机通过NodePort方式访问Service时,通过PREROUTING链,两个链都会跳转到KUBE-SERVICE链)
2.KUBE-SERVICES链(每一个Service所暴露的每一个端口在KUBE-SERVICES链中都会对应一条相应的规则,当Service的数量达到一定规模时,KUBE-SERVICES链中的规则的数据将会非常的大,而Iptables在进行查找匹配时是线性查找,这将耗费很长时间,时间复杂度O(n))。
3.KUBE-SVC-XXXXX链 (在KUBE-SVC-XXXXX链中(后面那串 hash 值由 Service 的虚 IP 生成),会以一定的概率匹配下面的某一条规则执行,通过statistic模块为每个后端设置权重,已实现负载均衡的目的,每个KUBE-SEP-XXXXX链代表Service后面的一个具体的Pod(后面那串 hash 值由后端 Pod 实际 IP 生成),这样便实现了负载均衡的目的)
4.KUBE-SEP-XXXX链 (通过DNAT,将数据包的目的IP修改为服务端的Pod IP)
5.POSTROUTING链
6.KUBE_POSTROUTING链 (对标记的数据包做SNAT)
通过上面的这个设置便实现了基于Iptables实现了负载均衡。但是Iptbles做负载均衡存在一些问题:
- 规则线性匹配时延:
KUBE-SERVICES链挂了一长串KUBE-SVC-*链,访问每个service,要遍历每条链直到匹配,时间复杂度O(N) - 规则更新时延:
非增量式,需要先iptables-save拷贝Iptables状态,然后再更新部分规则,最后再通过 iptables-restore写入到内核。当规则数到达一定程度时,这个过程就会变得非常缓慢。 - 可扩展性:
当系统存在大量的Iptables规则链时,增加/删除规则会出现kernel lock,这时只能等待。 - 可用性: 服务扩容/缩容时, Iptables规则的刷新会导致连接断开,服务不可用。
为了解决Iptables当前存在的这些问题,华为开源团队的同学为社区贡献了IPVS模式,接下来介绍下IPVS是如何实现负载均衡的。
IPVS 实现负载均衡
IPVS是LVS项目的一部分,是一款运行在Linux kernel当中的4层负载均衡器,性能异常优秀。使用调优后的内核,可以轻松处理每秒10万次以上的转发请求。
IPVS具有以下特点:
- 传输层Load Balancer, LVS负载均衡器的实现。
- 与Iptables同样基于Netfilter, 但是使用的是hash表。
- 支持TCP, UDP, SCTP协议,支持IPV4, IPV6。
- 支持多种负载均衡策略:
- rr: round-robin
- lc: least connection
- dh: destination hashing
- sh: source hashing
- sed: shortest expected delay
- nq: never queue
- 支持会话保持
LVS的工作原理如下图所示:
1.当客户端的请求到达负载均衡器的内核空间时,首先会达到PREROUTING链。
2.当内核发现请求的数据包的目的地址是本机时,将数据包送往INPUT链。
3.当数据包达到INPUT链时, 首先会被IPVS检查,如果数据包里面的目的地址及端口没有在IPVS规则里面,则这条数据包将被放行至用户空间。
4.如果数据包里面的目的地址和端口在IPVS规则里面,那么这条数据报文的目的地址会被修改为通过负责均衡算法选好的后后端服务器(DNAT),并发往POSROUTING链。
5.最后经由POSTROUTING链发往后端的服务器。
LVS主要由三种工作模式, 分别是NAT DR, Tunnel模式,而在kube-proxy中,IPVS工作在NAT模式,所以下面主要对NAT模式进行介绍:
还是分析上面的那种图:
- 客户端将请求发往前端的负载均衡器,请求报文源地址是CIP(客户端IP), 目的地址是VIP(负载均衡器前端地址)
- 负载均衡器收到报文之后,发现请求的是在规则里面存在的地址,那么它将请求的报文的目的地址改为后端服务器的RIP地址,并将报文根据响应的负责均衡策略发送出去。
- 报文发送到Real Server后,由于报文的目的地址是自己,所有会响应请求,并将响应的报文返回给LVS。
- 然后LVS将此报文的源地址修改本机的IP地址并发送给客户端。
介绍完基本的工作原理之后,下面我们看看如何在kube-proxy中使用IPVS模式进行负载均衡。
首先需要在启动kube-proxy的参数中指定如下参数:
1 | --proxy-mode=ipvs //将kube-proxy的模式设置为IPVS |
设置完这些参数之后,重启启动kube-proxy服务即可。当创建ClusterIP类型的Service时,IPVS模式的kube-proxy会做下面几件事儿:
- 创建虚拟网卡,默认是kube-ipvs0。
- 绑定service IP地址到虚拟网卡kube-ipvs0.
- 为每一个Service IP地址创建IPVS虚拟服务器
同时IPVS还支持会话保持功能,通过在创建Srevice对象时,指定service.spec.sessionAffinity
参数为ClusterIP
默认是None
和 指定service.spec.sessionAffinityConfig.clientIP.timeoutSeconds
参数为需要的时间,默认是10800s
。
下面是一个创建Service,指定会话保持的一个具体的例子:
1 | kind: Service |
之后通过ipvsadm -L
即可查看会话保持功能是否设置成功。
这样kube-proxy便可以通过IPVS的模式实现负载均衡。
总结
kube-proxy在使用iptables和ipvs实现对Service的负载均衡,但是通过iptables的实现方式,由于Iptables本身的特性,新增规则,更新规则是非增量式的,需要先iptables-save然后在内存中更新规则,在内核中修改规则,在iptables-restore,并且Iptables在进行规则查找匹配时是线性查找,这将耗费很长时间,时间复杂度O(n)。而使用IPVS的实现方式,其连接过程的时间复杂度是O(1)。基本就是说连接的效率与集群Service的数量是无关的。因此随着集群内部Service的不断增加,IPVS的性能优势就体现出来了。哈哈
参考
- https://zhuanlan.zhihu.com/p/37230013
- https://zhuanlan.zhihu.com/p/39909011
- https://www.projectcalico.org/comparing-kube-proxy-modes-iptables-or-ipvs/
- https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0
- https://wiki.archlinux.org/index.php/Iptables_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)
- https://kubernetes.io/docs/concepts/services-networking/service/
- http://blog.chinaunix.net/uid-23069658-id-3160506.html
- https://tonydeng.github.io/sdn-handbook/linux/loadbalance.html
- https://www.josedomingo.org/pledin/2018/11/recursos-de-kubernetes-services/