负载均衡-粘性分配与平滑加权轮询

说到负载均衡,我个人习惯分为两种,一种是静态负载均衡,也就是把确定好的、有限的负载按照一定规则分配到不同的工作单元上,这种负载均衡算法只需要计算一次最终分配状态即可。一种是动态负载均衡,即每次把新到来的负载按照一定规则分配到不同的工作单元上,这种负载均衡算法需要实时动态计算,并且大部分需要保留上一次分配的状态。

比如说,kafka中一个topic有多个consumer,那每个consumer需要消费哪些partition都是提前计算好的。这里负载就是这个topic的所有partition,是一个确定好的、有限的负载。每个consumer就是不同的工作单元。负载均衡算法只需要根据partition数量与consumer数量计算一次,计算出每个consumer消费哪些partition即可。这种就属于静态负载均衡。

再比如说,RocketMQ中,producer生产一条消息,需要放到哪个consumeQueue中,是需要实时计算的,并且要根据上一次的结果,才能计算出下一次分配的结果。这里每一条消息就是负载,consumeQueue就是工作单元,我们需要知道上一次的分配结果,并且每一个负载的分配都需要动态计算。这种就属于动态负载均衡。

以上纯是个人的一些理解,如有误导,请及时指正。

再说负载均衡的目的,负载均衡是把负载分摊到多个操作单元上进行执行,从而共同完成任务。是一种解决单点、高并发、水平扩展的方案。所以我们有时候不需要为了负载均衡而做负载均衡,而是应该先有单点、高并发、水平扩展等需求的时候,再做负载均衡,不能本末倒置。

比如,RocketMQ中发送消息的负载均衡,首先是因为多个消费端同时消费时,如果都从一个队列中取数据会有数据竞争的问题,如果进行加锁又会有性能问题,为了解决性能问题,提高消费能力,所以把消息索引分到多个队列中存储。因为消息分配到多个队列中,所以才有了消息的负载均衡,RocketMQ采用轮询的方式,把消息按顺序依次保存到不同的队列中。

再比如Kafka中某个主题的某个partition的master属于哪个broker,是因为只有master才能对外提供服务,如果所有master都集中在一个broker上,会导致broker的负载不均衡,导致性能降低。所以要对master进行负载均衡。

以上这两个负载均衡的案例都是利用水平扩展的方式,提高服务的性能,同时也自然而然的带来了负载均衡的问题。

所以,可以说是因为先有了性能、单点等问题,所以要对工作单元水平扩展。有了多个工作单元,才需要考虑负载均衡的问题。

常见的负载均衡算法有很多:

按照我的分类分为,静态算法:轮询分配、范围分配、粘性分配、一致性hash、hash等等。

动态算法:轮询、加权、最快响应、平滑加权轮询等。

这里比较有意思的两种分配算法,一个是静态算法里的粘性分配,一个是动态算法里的平滑加权轮询。

首先粘性分配,是指当工作单元发生变化时,负载分配结果的变化最小。其实一致性hash算法就是为了解决这个问题。但是,当工作单元与负载数量都很少的情况下,一致性hash算法的分配结果很有很大程度的不均衡现象。

所以又单独提出来粘性分配,这是一种需要依赖前一个状态的分配算法,例如kafka中StickyAssignor算法。

他的大概思想就是,先把工作单元与负载按一定的顺序排序,然后按照range或者round的方式进行初始分配。并保存分配结果。当工作单元发生变化时,则按照负载算法计算出变化后每个工作单元应该分配的负载数量,如果实际分配的负载比计算后的结果多,则从已分配的负载中按顺序取出排序最大的负载加入到待分配负载集合中。如果实际分配的负载比计算后的结果少,则从待分配负载集合中按顺序取出排序最小的负载分配给该工作单元。并保存最终分配结果。

基本流程:

  • 工作单元与负载按顺序排序。
  • 如果上一次分配状态为空,即初始分配,直接按range或round方式分配,并保存分配结果。结束。
  • 如果上一次分配状态不为空,如果负载数量增加,则把新增加的负载直接加入到待分配集合中;如果负载数量减少,则直接从上一次分配中踢出缩减的负载。
  • 根据一定的负载算法计算出工作单元变化后,每个工作单元应该分配的负载数量。
  • 如果上一次分配负载数量大于计算后结果,则按照负载排序结果,按顺序取出负载加入到待分配集合中。
  • 如果上一次分配负载数量小于计算后结果,则从待分配集合中按顺序取出负载分配到给该工作单元。
  • 保存分配结果。

例如,有序号0到9的负载,序号0、1的工作单元。

初始分配结果:0号工作单元对应负载0到4,1号工作单元对应负载5到9。

当新增序号2的工作单元后,计算每个工作单元应分配的负载数量为:0号3个,1号3个,2号4个。

则遍历0号工作单元后,0号分配负载:0、1、2,待分配负载集合:3、4。

遍历1号工作单元后,1号分配负载:5、6、7,待分配负载集合:3、4、8、9

遍历2号工作单元后,2号分配负载:3、4、8、9,待分配集合空。

 

平滑加权轮询是加权轮询的优化算法,首先加权轮询算法会有流量集中分配的问题,即比如3个工作单元A、B、C的权重分别为{5、2、3}。

则负载的分配结果为:

A-A-A-A-A-B-B-C-C-C

平滑加权轮询就是为了解决这个问题。

它的基本算法步骤是:

  • 初始每个工作单元 i 的权重为Wi,并计算所有权重总和Wsum。
  • 将权重最大的工作单元 k 权重Wk,减去权重总和Wsum。
  • 工作单元 k 作为计算结果。
  • 再把每个工作单元 i 的权重加上最原始分配的权重Wi。
  • 重复以上步骤。

例如还是A、B、C三个工作单元的权重分别分{5、2、3}。

权重 结果 分配后权重
5、2、3 A -5、2、3
0、4、6 C 0、4、-4
5、6、-1 B 5、-4、-1
10、-2、2 A

0、-2、2

5、0、5 A -5、0、5
0、2、8 C 0、2、-2
5、4、1 A -5、4、1
0、6、4 B 0、-4、4
5、-2、7 C 5、-2、-3
10、0、0 A 0、0、0

通过表格可以看出来,计算结果分布比较均匀,并且保证了按权重分配。并且每一轮后,权重都会重新恢复到最原始状态,保证后续操作都是重复的。

我们在通过算法证明一下调度算法的合理性。

首先是证明第 i 个工作单元,在一轮中最多被分配Wi次。

记第 i 个工作单元的权重为Wi,W1+W2+......+Wn=S,即总权重为S,需要证明在S次分配中,第 i 个工作单元最多被分配Wi次。

假设在S次分配中,已经分配了t次,其中第 i 个工作单元已经被分配了Wi次,那么 i 的当前权重为Wci = t * Wi - S * Wi + Wi = (t-S+1)Wi。

因为t <= S-1,所以Wci <= 0,又因为Wc1+Wc2+......+Wcn = S > 0,所以一定有一个工作单元 k 的权重大于0,将选择工作单元k。

并且需要连续S-t次不被选择后,Wci才能大于等于0,才有机会被选中。即剩余的S-t次内,工作单元 i 的权重Wci <= 0。

综上,在每轮中S次选择中,工作单元 i 做多被选中Wi次。又因为S=W1+W2+......+Wn。所以,工作单元 i 只能被选中Wi次。

再证明工作单元 i 不能被连续选择Wi次。

我们只需要证明,如果工作单元 i 被连续选择Wi-1次后,下一次一定不会被选中即可。

即工作单元 i 被连续选中Wi-1次后,

Wci = (Wi-1)*Wi - S*(Wi-1)+Wi =(Wi-S)*(Wi-1)+Wi <= -1*(Wi-1)+Wi = 1

即工作单元 i 被连续选择Wi-1次后,权重小于等于1。

又因为一定存在另外一个工作单元 j 的权重为:(Wi-1)*Wj>1。所以下一次一定不会再选择工作单元i。

所以平滑加权轮询算法是一个有效算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章