k8s 如何实现负载均衡?




一、四层与七层


1、四层负载均衡

负载均衡器用 ip+port 接收请求,再直接转发到后端对应服务上;

工作在传输层 ;

客户端和服务器之间建立一次TCP连接,而负载均衡设备只是起到一个类似路由器的转发动作。


2、七层负载均衡

负载均衡器根据 虚拟的 url 或主机名 来接收请求,经过处理后再转向相应的后端服务上;
工作在应用层 ;

七层负载均衡需要建立两次 TCP 连接,
client 到 LB,LB根据消息中的内容( 比如 URL 或者 cookie 中的信息 )来做出负载均衡的决定;
然后,建立 LB 到 server 的连接。

负载均衡设备需要先代理最终的服务器和客户端建立 TCP 连接后,才可能接收到客户端发送的真正应用层内容的报文,
然后再根据该报文中的特定字段,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。

具有七层负载均衡功能的设备通常也被称为反向代理服务器。

k8s 如何实现负载均衡?




二、四层负载均衡实现:kube-proxy


K8s 的内部服务发现是基于 DNS 解析实现的,
默认解析到一个稳定虚拟 IP (Service),该虚拟 IP 再通过 kube_proxy 将流量均衡到后端 Pods 上。
( Pod 的 IP 可能会随着 Pod 的重建而变动,但 Service 的 IP 是稳定的 )

kube-proxy 是 k8s 原生组件,主要通过 NodePort 方式暴露服务。


NodePort 方式是什么呢?
k8s 能保证在任意 Pod 挂掉时自动启动一个新的,甚至是动态扩容,这就意味着 Pod IP 是会动态变化的;
因此这个 Pod IP 你不适合暴露出去,
而 Service 可以以标签的形式选定一组带有指定标签的 Pod,并监控和自动负载这些 Pod IP;
因此对外只暴露 Service IP 就行了;
这就是 NodePort 模式,即在每个节点 node 上起一个端口,然后转发到内部 Pod IP 上。


上面提到的这个稳定虚拟 IP 就是一个 ClusterIP 类型的 Service,
这个 Service 会根据 kube-proxy 的代理模式的不同,有不同的性能表现:


1、userspace 模式

v1.0及之前版本的默认模式;
在 userspace 模式下,service 的请求会先从用户空间进入内核 iptables,然后再回到用户空间,
由 kube-proxy 完成后端 Endpoints 的选择和代理工作,这样流量从用户空间进出内核带来的性能损耗是不可接受的。


userspace 模式工作流程如下图:
k8s 如何实现负载均衡?

请求到达 iptables 时会进入内核,而 kube-proxy 监听是在用户态,
这样请求就形成了从用户态到内核态再返回到用户态的传递过程, 降低了服务性能。

因此,userspace 性能差。


2、iptables 模式

v1.1 版本中开始增加了 iptables mode,并在 v1.2 版本中正式取代 userspace 成为默认模式;

通过 Iptables 实现一个四层 TCP NAT ;
kube_proxy 只负责创建 iptables 的 nat 规则,不负责流量转发。


这种基于 iptables 的负载均衡,虽然操作起来比较简单,但是当集群规模大导致 iptables rules 多起来的时候,
这种基于 iptables 的负载均衡的性能就比较差了。

当添加一个 service 的时候,
iptables 命令行工具需要从内核中读取当前所有的 service 列表,
然后编辑列表,
最后再将新的列表传入内核。
假如要添加 N 个 service,复杂度为 O(N^2) 。

在转发面上,
所有 service ip 地址组成了一个 list,
每一个报文都需要查找这个 list,命中后才能执行规则。
假如一个 ip 在 list 末尾,需遍历 N 次。复杂度为 O(N/2)。


iptables 模式工作流程如下图:
k8s 如何实现负载均衡?

iptables 模式虽然克服了 userspace 那种 内核态–用户态 之间反复传递的缺陷,
但是在集群规模大的情况下,iptables rules 过多会导致性能显著下降。

因此,iptables 性能勉强适中 。


3、ipvs 模式

在 1.8 以上的版本中,kube-proxy 组件增加了 ipvs 模式;

ipvs 基于 NAT 实现,不创建反向代理, 也不创建 iptables 规则,通过 netlink 创建规则;
而 netlink 通过 hashtable 组织 service,其控制面和转发面的性能都是 O(1) 的,而且直接工作在内核态,因此在性能上比 userspace 和 iptables 都更优秀。


ipvs 模式工作流程如下图:
k8s 如何实现负载均衡?

因此,ipvs 可谓是性能与负载均衡兼得。


另外,
ipvs 还可通过 --ipvs-scheduler 指定负载均衡算法,有多种算法可选,
详情可参考: ipvs-based-in-cluster-load-balancing


kube-proxy 的代理模式,可通过指定 --proxy-mode 参数来配置。



四层这种负载均衡方式存在缺陷:

Service可能有很多,如果每个都绑定一个 node 主机端口的话,主机则需要开放外围的端口进行服务调用,管理上会比较混乱。

比较优雅的方式是通过一个外部的负载均衡器,比如 nginx ,绑定固定的端口比如80,然后根据域名/服务名向后面的Service ip转发,
但是这里对问题在于:
当有新服务加入的时候如何修改 Nginx 配置? 手动改或者 Rolling Update Nginx Pod 都是不现实的。

对于这个问题, k8s 给出的七层解决方案是 Ingress。




三、七层负载均衡实现: Ingress


Ingress 是 k8s 的一种资源对象,
该对象允许外部访问 k8s 服务, 通过创建规则集合来配置访问权限,这些规则定义了哪些入站连接可以访问哪些服务;
Ingress 仅支持 HTTP 和 HTTPS 协议;
ingress 可配置用于提供外部可访问的服务 url、负载均衡流量、SSL终端和提供虚拟主机名配置。


ingress 的工作流程如下;
k8s 如何实现负载均衡?

ingress-controller 是实现反向代理和负载均衡的程序,
通过监听 Ingress 这个 api 对象里的配置规则并转化成 Nginx 的配置 , 然后对外部提供服务


Ingress 对于上面提到的 “如何修改 Nginx 配置” 这个问题的解决方案是:
把 “修改 Nginx 配置各种域名对应哪个 Service ” 这些动作抽象为一个 Ingress 对象,
然后直接改 yml 创建/更新就行了,不用再修改 nginx 。

而 ingress-controller 通过与 k8s API 交互,动态感知集群中 Ingress 规则的变化并读取它,
然后按照模板生成一段 Nginx 配置,再写到 Nginx Pod 里,最后再 reload 一下生效。

大概的访问路径如下:

用户访问 --> LB --> ingress-nginx-service --> ingressController-ingress-nginx-pod --> ingress字段中调用的后端pod

注意后端 pod 的 service 只提供 pod 归类,
归类后 ingress 会将此 service 中的后端 pod 信息提取出来,
然后动态注入到 ingress-nginx-pod 中的 ingress 字段中,
如此,后端 pod 就能被调用了。




参考文档:

通过Ingress提供7层服务访问

负载均衡基础知识

官方 Ingress 文档