关于负载均衡的一切:总结与思考
古人云,不患寡而患不均。
在计算机的世界,这就是大家耳熟能详的负载均衡(load balancing),所谓负载均衡,就是说如果一组计算机节点(或者一组进程)提供相同的(同质的)服务,那么对服务的请求就应该均匀的分摊到这些节点上。负载均衡的前提一定是“provide a single Internet service from multiple servers”, 这些提供服务的节点被称之为server farm、server pool或者backend servers。
这里的服务是广义的,可以是简单的计算,也可能是数据的读取或者存储。负载均衡也不是新事物,这种思想在多核CPU时代就有了,只不过在分布式系统中,负载均衡更是无处不在,这是分布式系统的天然特性决定的,分布式就是利用大量计算机节点完成单个计算机无法完成的计算、存储服务,既然有大量计算机节点,那么均衡的调度就非常重要。
负载均衡的意义在于,让所有节点以最小的代价、最好的状态对外提供服务,这样系统吞吐量最大,性能更高,对于用户而言请求的时间也更小。而且,负载均衡增强了系统的可靠性,最大化降低了单个节点过载、甚至crash的概率。不难想象,如果一个系统绝大部分请求都落在同一个节点上,那么这些请求响应时间都很慢,而且万一节点降级或者崩溃,那么所有请求又会转移到下一个节点,造成雪崩。
事实上,网上有很多文章介绍负载均衡的算法,大多都是大同小异。本文更多的是自己对这些算法的总结与思考。
一分钟了解负载均衡的一切
本章节的标题和内容都来自一分钟了解负载均衡的一切这一篇文章。当然,原文的标题是夸张了点,不过文中列出了在一个大型web网站中各层是如何用到负载均衡的,一目了然。
常见互联网分布式架构如上,分为客户端层、反向代理nginx层、站点层、服务层、数据层。可以看到,每一个下游都有多个上游调用,只需要做到,每一个上游都均匀访问每一个下游,就能实现“将请求/数据【均匀】分摊到多个操作单元上执行”。
(1)【客户端层】到【反向代理层】的负载均衡,是通过“DNS轮询”实现的
(2)【反向代理层】到【站点层】的负载均衡,是通过“nginx”实现的
(3)【站点层】到【服务层】的负载均衡,是通过“服务连接池”实现的
(4)【数据层】的负载均衡,要考虑“数据的均衡”与“请求的均衡”两个点,常见的方式有“按照范围水平切分”与“hash水平切分”。
数据层的负载均衡,在我之前的《带着问题学习分布式系统之数据分片》中有详细介绍。
算法衡量
在我看来,当我们提到一个负载均衡算法,或者具体的应用场景时,应该考虑以下问题
第一,是否意识到不同节点的服务能力是不一样的,比如CPU、内存、网络、地理位置
第二,是否意识到节点的服务能力是动态变化的,高配的机器也有可能由于一些突发原因导致处理速度变得很慢
第三,是否考虑将同一个客户端,或者说同样的请求分发到同一个处理节点,这对于“有状态”的服务非常重要,比如session,比如分布式存储
第四,谁来负责负载均衡,即谁充当负载均衡器(load balancer),balancer本身是否会成为瓶颈
下面会结合具体的算法来考虑这些问题
负载均衡算法
轮询算法(round-robin)
思想很简单,就是提供同质服务的节点逐个对外提供服务,这样能做到绝对的均衡。Python示例代码如下
SERVER_LIST = [
'10.246.10.1',
'10.246.10.2',
'10.246.10.3',
]
def round_robin(server_lst, cur = [0]):
length = len(server_lst)
ret = server_lst[cur[0] % length]
cur[0] = (cur[0] + 1) % length
return ret
可以看到,所有的节点都是以同样的概率提供服务,即没有考虑到节点的差异,也许同样数目的请求,高配的机器CPU才20%,低配的机器CPU已经80%了
加权轮询算法(weight round-robin)
加权轮训算法就是在轮训算法的基础上,考虑到机器的差异性,分配给机器不同的权重,能者多劳。注意,这个权重的分配依赖于请求的类型,比如计算密集型,那就考虑CPU、内存;如果是IO密集型,那就考虑磁盘性能。Python示例代码如下
WEIGHT_SERVER_LIST = {
'10.246.10.1': 1,
'10.246.10.2': 3,
'10.246.10.3': 2,
}
def weight_round_robin(servers, cur = [0]):
weighted_list = []
for k, v in servers.iteritems():
weighted_list.extend([k] * v)
length = len(weighted_list)
ret = weighted_list[cur[0] % length]
cur[0] = (cur[0] + 1) % length
return ret
随机算法(random)
这个就更好理解了,随机选择一个节点服务,按照概率,只要请求数量足够多,那么也能达到绝对均衡的效果。而且实现简单很多
def random_choose(server_lst):
import random
random.seed()
return random.choice(server_lst)
加权随机算法(random)
如同加权轮训算法至于轮训算法一样,也是在随机的时候引入不同节点的权重,实现也很类似。
def weight_random_choose(servers):
import random
random.seed()
weighted_list = []
for k, v in servers.iteritems():
weighted_list.extend([k] * v)
return random.choice(weighted_list)
当然,如果节点列表以及权重变化不大,那么也可以对所有节点归一化,然后按概率区间选择
def normalize_servers(servers):
normalized_servers = {}
total = sum(servers.values())
cur_sum = 0
for k, v in servers.iteritems():