【阅读原文】戳:服务网格 ASM 负载均衡算法全面解析
引言:当今,分布式微服务是毫无争议地主流架构模式,微服务将复杂大型系统拆分成多个松耦合的微型系统,以此使得大型系统的迭代更为灵活,容错率也变得更高。对于其中的单一微服务,支持横向扩容,以根据业务情况灵活地调整系统中每个服务所使用的资源。要正确地调用多副本服务,有两个基础的问题需要解决:服务发现和负载均衡。K8s作为当前业界广泛采用的部署平台,通过服务(Service)非入侵式地解决了服务发现和负载均衡的问题,但是,面对日益复杂的业务场景,K8s服务仍然存在一些不尽人意之处,例如,其负载均衡算法仅支持随机,而对于一些特定场景,随机算法的表现较差,且没有调优余地。因此,越来越多地微服务运维团队选择了服务网格作为网络基础设施,其在K8s服务的基础上,提供了比K8s更强大的负载均衡能力。在本文中,笔者将解析服务网格的多种负载均衡算法的实现原理和使用场景,为服务网格负载均衡算法的选择提供参考。
服务网格负载均衡的优势
K8s Service通过iptables规则,实现了连接级别的负载均衡,这种负载均衡不需要入侵应用,而是直接从网络层面将流量做DNAT,实现了负载均衡。但K8s的负载均衡存在一些显著的缺陷,首先,由于其使用iptables规则实现负载均衡,这使得K8s的负载均衡算法的时间复杂度是O(n),因此在集群规模逐渐膨胀时,负载均衡的性能可能受到显著影响。其次,其只支持随机负载均衡算法,这种算法并不能严格地将请求均匀地分布到后端实例上。由于其基于iptables规则实现的缘故,因为其工作在3层(IP层),因此只能做到4层,即TCP连接级别的负载均衡,而当今大多数主流应用使用HTTP/GRPC协议进行通信,基于iptables规则实现的K8s服务负载均衡注定无法实现请求级别的路由。而服务网格控制平面从K8s集群ApiServer获得工作负载和服务信息,并将这些信息下发至SIdecar,借助其7层能力,支持对HTTP/GRPC请求实现请求级负载均衡,同时支持了适用于多种场景的负载均衡算法,用户可以根据业务特性,为特定服务选择适用的负载均衡算法,以更高效地利用系统资源。
负载均衡算法
服务网格ASM提供了多种负载均衡算法,其中包括与社区Istio兼容的RANDOM、ROUND_ROBIN,LEAST_REQUEST以及ASM特有的PEAK_EWMA。在本文接下来的内容中,笔者将逐一分析每种负载均衡算法的特点及其适用场景。
RANDOM和ROUND_ROBIN
服务网格ASM提供了经典的随机(RANDOM)和轮询(ROUND_ROBIN)算法,对于随机负载均衡,与K8s服务不同的是,由于ASM Sidecar工作在7层,因此对于HTTP、GRPC协议的通信可以提供请求级别的随机负载均衡。随机负载均衡由于随机计算本身的不稳定性,不能保证请求完全均匀地分布到后端。此时若工作负载的性能和请求产生的计算量都比较平均,则可以考虑使用轮询算法进行负载均衡,轮询算法可以完全平均地将请求分布到后端工作负载。这两种算法被广泛使用在各种分布式系统的负载均衡中,它们足够简单直观,且被几乎所有负载均衡软硬件支持,但是,在实践中却存在一些场景,使得这两种算法的表现不尽人意。
RANDOM和ROUND_ROBIN的不足之处
由于RANDOM和ROUND_ROBIN两种负载均衡算法均是完全不考虑后端服务状态,仅使用本地状态计算得出负载均衡结果的算法,因此,后端服务状态变化完全不会影响到负载均衡计算的结果,从而导致做出不够理想的负载均衡决策,例如以下这些场景:
1.后端处理能力不均衡
在后端处理能力不均衡时,完全均衡地分布请求反而会影响服务质量,由于后端负载能力不平均,处理能力弱的后端会更早过载,而负载均衡算法仍然以平均分配的方式将请求分配至后端,这些被分配至已过载的后端的请求会加剧已过载后端的压力,进一步恶化服务的延迟,影响服务的整体表现,例如下图例子中,Server-1的处理能力更强,还能够接受新的请求,而Server-2已经满载,但随机或者轮询负载均衡算法仍然将新请求打到Server-2,而不是尚有余力的Server-1,这将导致这个请求的返回实践因Server-2的满载而显著延长。
2.请求计算量不平均
即使后端的处理能力类似,同类请求对后端造成的压力也可能存在显著的差异,一个典型的例子是LLM应用,同样是几个单词的prompt,“write me a novel”和“what is the time”两个prompt在后端计算量上的差异可谓天壤之别,因此返回时间可能也存在显著差异,例如下图的例子中,Server-1和Server-2正在处理的请求虽然都是2个,但是,Server-2处理了一个大计算量的请求,导致其已经满载,然而随机或者轮询负载均衡算法仍然将新请求打到Server-2,而不是尚有余力的Server-1,这将导致这个请求的返回实践因Server-2的满载而显著延长。
即使是在请求计算量大致均等的场景下,错误的请求也可能导致类似的问题,例如客户端发送了错误的请求,服务端可能在校验失败后不做任何处理就立即返回,就会间接导致请求计算量不平均的问题。
如何避免RANDOM和ROUND_ROBIN的不足
RANDOM和ROUND_ROBIN存在的缺陷本质上还是因为负载均衡无法感知后端的状态所致,理论上来说,最优的负载均衡决策应该结合后端实时状态和请求的潜在计算量做出,然而,这两者在真正的负载均衡实现中都存在很大的挑战,那么是否存在对后端状态间接感知的方式呢?笔者将在下面的篇幅中介绍ASM支持的另外两种负载均衡算法。
LEAST_REQUEST
LEAST_REQUEST算法在用户未显式指定时被作为ASM默认提供的负载均衡算法,该负载均衡器会记录当前每个后端上未完成的请求数量,处理能力更强的后端往往会因请求更快完成而更快地消耗这个计数,负载均衡器借助这个现象来间接感知后端的压力状态,并总是将请求路由到未完成请求数最少的后端。
下面,我们来通过例子直观地感受一下LEAST_REQUEST的优势,首先以前面提到过的后端处理能力不均衡场景为例,在T1时刻,负载均衡向Server-1和Server-2分别分配了3个请求,由于Server-1的并发处理能力更大,到T2时刻时,Server-1已经完成了对全部3个请求的处理,而Server-2仍然有一个请求未完成处理,由于LEAST_REQUEST总是会向进行中请求最少的后端分配请求,因此,新到达的请求被分配至负载更低的Server-1。
我们再来看看请求计算量不均的场景,这个场景下同样的请求对后端的计算复杂度差异显著,下图例子中,在T1时刻,Server-1有2个进行中的请求,Server-2只有一个,但Server-2处理的是一个大计算量请求(例如一个复杂的Prompt)。到T2时刻,Server-1的2个请求都已经返回,而Server-2尚未完成一个复杂请求的计算和返回。此时,如果负载均衡器收到了新的请求,由于Server-1的进行中请求数量少于Server-2,新请求会被分配至Server-1。
结合上述两个例子,我们可以直观地看到LEAST_REQUEST负载均衡算法在应对更加复杂场景时相对于RANDOM和ROUND_ROBIN算法表现出了更强的自适应能力,通过进行中请求数的变化实时地感知后端地状态以做出更优的负载均衡决策,降低请求平均延迟,提升整个系统的利用率。这样的特性非常适合对于同类请求计算量差异的AI场景。
LEAST_REQUEST能在绝大多数情况下提供较好的负载均衡表现,但是LESAT_REQUEST是任何情况下的最优解吗?我们不妨思考这样一种情况,当后端因为异常出现报错时,由于错误请求没有被实际处理,往往会立即返回,LEAST_REQUEST负载均衡器中记录的报错节点的进行中的请求数量会快速降低,该后端在该评价体系下被误认为是“高性能”的,因此更多的请求被分配到发生错误的后端,进而造成系统的整体错误率提升。显然,这个缺陷是由于LEAST_REQUEST评估体系无法感知错误率的存在导致的,那么是否有更好的选择呢,接下来,笔者将介绍ASM提供的PEAK_EWMA负载均衡器。
PEAK_EWMA
ASM在1.21版本提供了PEAK_EWMA负载均衡器,该负载均衡器会ASM结合端点在过去一段时间内的响应时间、错误率,静态权重等因子计算出端点的负载均衡权重计算节点的分值,在一段时间内响应时间越短、错误率越低,静态权重越高,则该节点的分值越高,更容易被负载均衡选中。相比于LEAST_REQUEST,PEAK_EWMA的感知范围更全面,延迟升高、错误率上升都会造成该后端的权重降低,更少的请求被路由到这个后端,从而尽可能地提升服务整体地延迟和成功率表现。
这里我们提供了一个PEAK_EWMA负载均衡器的端到端实例,该示例使用simple-server应用作为服务端,该应用可通过启动参数指定,模拟不同状态码返回、随机延迟范围等多种服务端响应行为。在本例中,我们将部署两个Deployment,其中名为simple-server-503的deployment使用--mode=503启动参数设置为固定返回503,模拟一个收到请求立即返回503状态码的后端;名为simple-server-normal的deployment则使用--mode=normal--delayMin=50--delayMax=200模拟一个正常返回200的后端,其处理延迟在50-200之间波动。
前提条件
•已创建ASM 1.21或以上版本的实例
•已创建K8s 1.21或以上版本的集群,并已将K8s集群添加至ASM实例
部署示例
在K8s集群中通过以下yaml部署simple-server应用和服务,以及用于发起测试流量的sleep应用:
apiVersion: apps/v1 kind: Deployment metadata: labels: app: simple-server name: simple-server-normal namespace: default spec: replicas: 1 selector: matchLabels: app: simple-server template: metadata: labels: app: simple-server spec: containers: - args: - --mode - normal image: registry-cn-hangzhou.ack.aliyuncs.com/test-public/simple-server:v1.0.0.2-gae1f6f9-aliyun imagePullPolicy: IfNotPresent name: simple-server env: - name: POD_NAME valueFrom: fieldRef: apiVersion: v1 fieldPath: metadata.name --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: simple-server name: simple-server-503 namespace: default spec: replicas: 1 selector: matchLabels: app: simple-server template: metadata: labels: app: simple-server spec: containers: - args: - --mode - "503" image: registry-cn-hangzhou.ack.aliyuncs.com/test-public/simple-server:v1.0.0.2-gae1f6f9-aliyun imagePullPolicy: IfNotPresent name: simple-server env: - name: POD_NAME valueFrom: fieldRef: apiVersion: v1 fieldPath: metadata.name --- apiVersion: v1 kind: Service metadata: labels: app: simple-server name: simple-server namespace: default spec: ports: - name: http port: 8080 protocol: TCP targetPort: 8080 selector: app: simple-server --- apiVersion: v1 kind: ServiceAccount metadata: name: sleep --- apiVersion: apps/v1 kind: Deployment metadata: name: sleep spec: replicas: 1 selector: matchLabels: app: sleep template: metadata: labels: app: sleep spec: terminationGracePeriodSeconds: 0 serviceAccountName: sleep containers: - name: sleep image: registry-cn-hongkong.ack.aliyuncs.com/test/curl:asm-sleep command: ["/bin/sleep", "infinity"] imagePullPolicy: IfNotPresent
为了避免ASM默认存在的重试机制(默认重试2次)对验证结果造成影响,我们通过为simple-server应用VirtualService来显式地关闭重试。使用ASM实例的kubeconfig,应用以下VirtualService:
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: simple-server namespace: default spec: hosts: - simple-server.default.svc.cluster.local http: - name: default retries: attempts: 0 # 关闭重试 route: - destination: host: simple-server.default.svc.cluster.local
使用K8s集群kubeconfig,执行以下命令来发起测试:
$ kubectl exec -it deploy/sleep -c sleep -- sh -c 'for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code}\n" simple-server:8080/hello; done' 200 200 200 503 503 503 503 503 200 503
可以看到,由于simple-server服务下有一个后端是固定返回503的,默认的LEAST_REQUEST并不能很好地在负载均衡阶段避开返回错误的节点。接下来我们来看看ASM的PEAK_EWMA负载均衡器的表现,在ASM实例中应用以下DestinationRule,为simple-server服务启用PEAK_EWMA负载均衡器:
apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: simple-server namespace: default spec: host: simple-server.default.svc.cluster.local trafficPolicy: loadBalancer: simple: PEAK_EWMA ---
使用同样的命令再次执行测试:
$ kubectl exec -it deploy/sleep -c sleep -- sh -c 'for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code}\n" simple-server:8080/hello; done' 200 503 200 200 200 200 200 200 200 200
可以看到,在返回了一个503后,后续全部都是200,这是因为PEAK_EWMA负载均衡器在后端返回503后,导致错误率上升,从而降低了该断点的分值,因此在之后的一段时间内,该后端将在分值比较时低于正常的后端,因此不会被选择到。但是,过一段时间后,这种影响因为移动平均的缘故又被消除,从而使得负载均衡器可以重新选择到该后端。我们在前文提到过,PEAK_EWMA算法不仅可以感知后端的错误率,同时还可以感知后端延迟变化,尽可能将请求路由给延迟更低的端点,关于这一点ASM官方文档提供了一个例子,欢迎各位读者前往体验。
总结
负载均衡是一个现代分布式应用绕不开的一个重要话题,每一种负载均衡都有其适用的场景,根据业务场景正确地选择负载均衡算法能为应用带来更好的表现。阿里云服务网格ASM团队在结合了大量客户实际场景后,于1.21版本推出了PEAK_EWMA负载均衡算法,该负载均衡算法在传统的负载均衡算法上更进一步,提供了综合后端延迟、错误率、静态权重的负载均衡选择能力,让负载均衡能够动态适应应用状态的变化,做出更优的负载均衡决策,从而提升应用的全局表现。
我们是阿里巴巴云计算和大数据技术幕后的核心技术输出者。
获取关于我们的更多信息~