HAProxy 调度算法详解
HAProxy 是一个高性能的负载均衡器和代理服务器,广泛应用于 Web 应用程序和服务中。它不仅能够处理大量的并发连接,而且还提供了多种负载均衡算法来优化流量分发。本文将详细介绍 HAProxy 中可用的不同调度算法,并深入讨论它们的特点和适用场景。
- HAProxy通过固定参数 balance 指明对后端服务器的调度算法
- balance参数可以配置在listen或backend选项中。
- HAProxy的调度算法分为静态和动态调度算法
- 有些算法可以根据参数在静态和动态算法中相互转换
1. HAProxy 调度算法概览
HAProxy 支持多种调度算法,这些算法可以分为两大类:静态算法和动态算法。每种算法都有其特定的应用场景和优缺点。
1.1 静态算法
静态算法按照事先定义好的规则进行轮询公平调度,不考虑后端服务器的实际负载情况以及连接响应速度等情况,同时也不能根据服务器的当前状态进行动态调整。
- static-rr(静态轮询):基于权重进行轮询,不支持运行时利用工具进行权重的动态调整。
- first(首次):始终将请求发送到第一个可用后端服务器。
1.2 动态算法
动态算法考虑后端服务器的当前状态,可以根据服务器的连接数、响应速度等因素动态调整调度决策。权重能够在haproxy运行时动态的调整。
- roundrobin(轮询):按顺序将请求分发给后端服务器。
- leastconn(最少连接):将新的连接请求分发给连接数最少的后端服务器。
- source(源 IP 哈希):根据客户端 IP 地址进行哈希计算,将来自同一 IP 的请求发送到同一台后端服务器。
- uri(URI 哈希):根据 URI(Uniform Resource Identifier)进行哈希计算,将相同 URI 的请求发送到同一台后端服务器。
- url_param(URL 参数哈希):根据 URL 中的参数进行哈希计算,将相同参数的请求发送到同一台后端服务器。
- hdr(HTTP 头哈希):根据 HTTP 请求头进行哈希计算,将具有相同请求头的请求发送到同一台后端服务器。
- rdp-cookie(Cookie 哈希):根据 Cookie 进行哈希计算,将具有相同 Cookie 的请求发送到同一台后端服务器。
- random(随机):随机选择一台后端服务器。
2. 调度算法详解
2.1 static-rr(静态轮询)
- 描述:按照固定的顺序和权重将请求分发给后端服务器。权重不能动态调整,只能在重启 HAProxy 时生效。
- 适用场景:适用于后端服务器负载相对稳定的场景。
- 配置示例:
backend servers bind *:80 mode http balance static-rr server server1 192.168.1.10:80 check weight 2 server server2 192.168.1.11:80 check weight 1
- static-rr不支持运行时利用socat进行权重的动态调整(仅能调为0%或100%)
- static-rr不不支持服务端慢启动
- static-rr不其后端主机数量没有限制,相当于LVS中的wrr
慢启动是指在服务器刚刚启动上不会把他所应该承担的访问压力全部给它,而是先给一部分,当没问题后在给一部分
2.2 first(首次)
- 描述:始终将请求发送到第一个可用后端服务器。相当于往水箱中装水,仅有装满后才会装下一台。
- 适用场景:适用于简单测试环境或特殊配置需要。
- 配置示例:
backend servers bind *:80 mode http balance first server server1 192.168.1.10:80 check server server2 192.168.1.11:80 check
- 根据服务器在列表中的位置,自上而下进行调度
- 其只会当第一台服务器的连接数达到上限,新请求才会分配给下一台服务
- 其会忽略服务器的权重设置
- 不支持用socat进行动态修改权重,可以设置0和1,可以设置其它值但无效
2.3 roundrobin(轮询)
- 描述:按照顺序将请求分发给后端服务器,可以给每个服务器分配不同的权重。并且在后端主机过负载时会无视权重轮询下一台服务器。
- 适用场景:适用于大多数情况下的负载均衡需求。
- 配置示例:
backend servers bind *:80 mode http balance roundrobin server server1 192.168.1.10:80 check weight 2 server server2 192.168.1.11:80 check weight 1
- 基于权重的轮询动态调度算法,
- 支持权重的运行时调整,不同于lvs中的rr轮训模式,
- HAProxy中的roundrobin支持慢启动(新加的服务器会逐渐增加转发数),
- 其每个后端backend中最多支持4095个real server,
- 支持对real server权重动态调整,
- roundrobin为默认调度算法,此算法使用广泛
2.4 leastconn(最少连接)
- 描述:将新的连接请求分发给当前连接数最少的后端服务器。
- 适用场景:适用于连接密集型应用,如聊天服务、在线游戏等。
- 配置示例:
backend servers bind *:80 mode http balance leastconn server server1 192.168.1.10:80 check server server2 192.168.1.11:80 check
- leastconn加权的最少连接的动态
- 支持权重的运行时调整和慢启动,即:根据当前连接最少的后端服务器而非权重进行优先调度(新客户 端连接)
- 比较适合长连接的场景使用,比如:MySQL等场景
2.5 source(源 IP 哈希)
- 描述:根据客户端 IP 地址进行哈希计算,确保来自同一IP 的请求被发送到同一台后端服务器。
- 适用场景:适用于需要会话保持的应用场景。
- 配置示例:
backend servers bind *:80 mode http balance source server server1 192.168.1.10:80 check server server2 192.168.1.11:80 check
如果访问端IP地址内有多台主机,那么他们会都被定向到同一台服务器。
2.6 uri(URI 哈希)
- 描述:根据用户请求的 URI 进行哈希计算,确保相同 URI 的请求被发送到同一台后端服务器。
- 适用场景:适用于需要将特定 URI 映射到特定后端服务器的应用场景。
- 配置示例:
backend servers bind *:80 mode http balance uri hash-type consistent #一致性hash配置 server server1 192.168.1.10:80 check server server2 192.168.1.11:80 check
2.7 url_param(URL 参数哈希)
- 描述:根据 URL 中的参数进行哈希计算,确保相同参数的请求被发送到同一台后端服务器。
- 适用场景:适用于需要根据 URL 参数进行路由的应用场景。
- 配置示例:
backend servers bind *:80 mode http balance url_param name,userid hash-type consistent #一致性hash配置 server server1 192.168.1.10:80 check server server2 192.168.1.11:80 check
2.8 hdr(HTTP 头哈希)
- 描述:根据 HTTP 请求头进行哈希计算,确保具有相同请求头的请求被发送到同一台后端服务器。此处由name指定的http首部会被取出并作hash计算。
- 适用场景:适用于需要根据请求头进行路由的应用场景。
- 配置示例:
backend servers bind *:80 mode http balance hdr(X-Header) hash-type consistent #一致性hash配置 server server1 192.168.1.10:80 check server server2 192.168.1.11:80 check
2.9 rdp-cookie(Cookie 哈希)
- 描述:根据 Cookie 进行哈希计算,确保具有相同 Cookie 的请求被发送到同一台后端服务器。
- 适用场景:适用于需要根据 Cookie 进行会话保持的应用场景。
- 配置示例:
backend servers bind *:80 mode http balance rdp-cookie server server1 192.168.1.10:80 check server server2 192.168.1.11:80 check
2.10 random(随机)
- 描述:随机选择一台后端服务器。
- 适用场景:适用于需要简单随机负载均衡的应用场景。
- 配置示例:
backend servers bind *:80 mode http balance random server server1 192.168.1.10:80 check server server2 192.168.1.11:80 check
3.hash模式
在涉及到hash的动态算法中出现了不同的hash计算方法,他们分别是取模法与一致性hash。
3.1. 取模法(Modulo Hashing)
3.1.1 基本原理
取模法是一种简单的哈希方法,它通过将键值经过哈希函数计算后得到一个整数,然后对这个整数取模运算(通常是对哈希表大小取模),以确定数据存储的位置。
3.1.2 计算步骤
- 选择哈希函数:选择一个合适的哈希函数,将键值映射到一个整数。
- 取模操作:使用取模运算(通常是
%
符号)来将哈希值缩小到一个固定范围内。 - 确定位置:根据取模结果来确定数据应该存储的位置。
3.1.3 优点
- 简单易实现:取模法实现简单,易于理解。
- 空间利用率高:在理想情况下,可以达到较高的空间利用率。
3.1.4 缺点
- 扩展性差:当哈希表的大小变化时,可能会导致大量的数据迁移。
- 不均匀分布:如果哈希函数选择不当,可能导致数据分布不均。
3.1.5 使用场景
- 小规模应用:对于数据量不大且不需要频繁扩展的应用场景,取模法是一个不错的选择。
- 静态哈希表:在哈希表大小不变的情况下,取模法可以有效减少数据迁移。
3.2. 一致性哈希(Consistent Hashing)
3.2.1 基本原理
一致性哈希是一种特殊的哈希算法,它旨在解决分布式系统中节点加入或离开时的数据迁移问题。它通过将节点和键值映射到一个环形的哈希空间中,来减少数据迁移的成本。
3.2.2 计算步骤
- 选择哈希函数:选择一个合适的哈希函数,将键值映射到一个整数。
- 映射到环:将所有节点和键值映射到一个环形的哈希空间中。
- 查找节点:找到距离键值最近的节点作为存储位置。
3.2.3 特点
- 数据迁移最小化:即使节点加入或离开,也只需要少量的数据迁移。
- 可扩展性好:一致性哈希支持动态添加或删除节点,对现有数据影响较小。
- 负载均衡:通过虚拟节点技术可以实现较好的负载均衡。
3.2.4 优点
- 动态调整:支持节点的动态加入和离开,减少了数据迁移成本。
- 负载均衡:通过虚拟节点技术可以实现较好的负载均衡。
3.2.5 缺点
- 复杂性:相对于取模法来说,一致性哈希的实现更为复杂。
- 性能开销:由于需要维护环形结构,可能会有一定的性能开销。
3.2.6 使用场景
- 分布式系统:在需要动态扩展和收缩的分布式系统中,一致性哈希是非常合适的选择。
- 缓存系统:例如 Memcached 使用一致性哈希来管理缓存节点,以支持节点的动态增减。
示例代码
取模法示例
def modulo_hash(key, hash_table_size):
hash_value = hash(key) % hash_table_size
return hash_value
# 示例
hash_table_size = 10
key = "example"
hash_location = modulo_hash(key, hash_table_size)
print(f"Hash location for '{key}': {hash_location}")
一致性哈希示例
import hashlib
from bisect import bisect_left
class ConsistentHash:
def __init__(self, nodes=None, replicas=100):
self.ring = {}
self.sorted_keys = []
if nodes is not None:
for node in nodes:
self.add_node(node, replicas)
def add_node(self, node, replicas):
for i in range(replicas):
key = self._hash(f"{node}-{i}")
self.ring[key] = node
self.sorted_keys.append(key)
self.sorted_keys.sort()
def _hash(self, key):
m = hashlib.md5()
m.update(key.encode('utf-8'))
return int(m.hexdigest(), 16) & 0xffffffffffffffff
def get_node(self, key):
hash_key = self._hash(key)
pos = bisect_left(self.sorted_keys, hash_key)
if pos == len(self.sorted_keys):
pos = 0
return self.ring[self.sorted_keys[pos]]
# 示例
nodes = ["node1", "node2", "node3"]
chash = ConsistentHash(nodes, replicas=100)
key = "example"
node = chash.get_node(key)
print(f"Node for '{key}': {node}")
其中有关一致性hash
本质上一致性hash也是使用取模的方法,但是一致性hash是对2^32取模,什么意思呢?
首先,我们把二的三十二次方想象成一个圆,就像钟表一样,钟表的圆可以理解成由60个点组成的圆,而此处我们把这个圆想象成由2^32个点组成的圆,示意图如下:
圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1
我们把这个由2的32次方个点组成的圆环称为hash环。
那么,一致性哈希算法与上图中的圆环有什么关系呢?我们继续聊,仍然以之前描述的场景为例,假设我们有3台缓存服务器,服务器A、服务器B、服务器C,那么,在生产环境中,这三台服务器肯定有自己的IP地址,我们使用它们各自的IP地址进行哈希计算,使用哈希后的结果对2^32取模,可以使用如下公式示意。
hash(服务器A的IP地址) % 2^32
通过上述公式算出的结果一定是一个0到2^32-1之间的一个整数,我们就用算出的这个整数,代表服务器A,既然这个整数肯定处于0到2^32-1之间,那么,上图中的hash环上必定有一个点与这个整数对应,而我们刚才已经说明,使用这个整数代表服务器A,那么,服务器A就可以映射到这个环上,用下图示意
同理,服务器B与服务器C也可以通过相同的方法映射到上图中的hash环中
hash(服务器B的IP地址) % 2^32
hash(服务器C的IP地址) % 2^32
通过上述方法,可以将服务器B与服务器C映射到上图中的hash环上,示意图如下
假设3台服务器映射到hash环上以后如上图所示(当然,这是理想的情况)。
好了,到目前为止,我们已经把缓存服务器与hash环联系在了一起,我们通过上述方法,把缓存服务器映射到了hash环上,那么使用同样的方法,我们也可以将需要缓存的对象映射到hash环上。
假设,我们需要使用缓存服务器缓存图片,而且我们仍然使用图片的名称作为找到图片的key,那么我们使用如下公式可以将图片映射到上图中的hash环上。
hash(图片名称) % 2^32
映射后的示意图如下,下图中的橘黄色圆形表示图片
好了,现在服务器与图片都被映射到了hash环上,那么上图中的这个图片到底应该被缓存到哪一台服务器上呢?上图中的图片将会被缓存到服务器A上,为什么呢?因为从图片的位置开始,沿顺时针方向遇到的第一个服务器就是A服务器,所以,上图中的图片将会被缓存到服务器A上,如下图所示。
没错,一致性哈希算法就是通过这种方法,判断一个对象应该被缓存到哪台服务器上的,将缓存服务器与被缓存对象都映射到hash环上以后,从被缓存对象的位置出发,沿顺时针方向遇到的第一个服务器,就是当前对象将要缓存于的服务器,由于被缓存对象与服务器hash后的值是固定的,所以,在服务器不变的情况下,一张图片必定会被缓存到固定的服务器上,那么,当下次想要访问这张图片时,只要再次使用相同的算法进行计算,即可算出这个图片被缓存在哪个服务器上,直接去对应的服务器查找对应的图片即可。
刚才的示例只使用了一张图片进行演示,假设有四张图片需要缓存,示意图如下
1号、2号图片将会被缓存到服务器A上,3号图片将会被缓存到服务器B上,4号图片将会被缓存到服务器C上。
一致性哈希算法的优点
经过上述描述,我想兄弟你应该已经明白了一致性哈希算法的原理了,但是话说回来,一致性哈希算法能够解决之前出现的问题吗,我们说过,如果简单的对服务器数量进行取模,那么当服务器数量发生变化时,会产生缓存的雪崩,从而很有可能导致系统崩溃,那么使用一致性哈希算法,能够避免这个问题吗?我们来模拟一遍,即可得到答案。
假设,服务器B出现了故障,我们现在需要将服务器B移除,那么,我们将上图中的服务器B从hash环上移除即可,移除服务器B以后示意图如下。
在服务器B未移除时,图片3应该被缓存到服务器B中,可是当服务器B移除以后,按照之前描述的一致性哈希算法的规则,图片3应该被缓存到服务器C中,因为从图片3的位置出发,沿顺时针方向遇到的第一个缓存服务器节点就是服务器C,也就是说,如果服务器B出现故障被移除时,图片3的缓存位置会发生改变
但是,图片4仍然会被缓存到服务器C中,图片1与图片2仍然会被缓存到服务器A中,这与服务器B移除之前并没有任何区别,这就是一致性哈希算法的优点,如果使用之前的hash算法,服务器数量发生改变时,所有服务器的所有缓存在同一时间失效了,而使用一致性哈希算法时,服务器的数量如果发生改变,并不是所有缓存都会失效,而是只有部分缓存会失效,前端的缓存仍然能分担整个系统的压力,而不至于所有压力都在同一时间集中到后端服务器上。
这就是一致性哈希算法所体现出的优点
4. 如何选择合适的调度算法
选择合适的调度算法需要考虑以下几个因素:
- 服务器负载情况:如果服务器负载较为均衡,可以选择轮询算法;如果服务器负载差异较大,则可以选择最少连接算法。
- 会话保持需求:如果需要保持客户端与后端服务器之间的会话,可以选择基于源 IP 或 Cookie 的哈希算法。
- 特殊需求:根据具体的应用需求,如 URL 参数或 HTTP 头的路由,可以选择相应的算法。
- 系统性能:考虑调度算法对系统性能的影响,选择最优化的算法。
5. 实践案例分析
5.1 高并发网站
对于高并发的网站应用,可以采用 leastconn
算法来确保连接数较少的服务器能够优先处理新的请求,从而平衡整个集群的负载。
5.2 会话保持
在需要会话保持的应用场景中,例如在线购物平台,可以使用 source
或 rdp-cookie
算法来确保来自同一客户端的请求总是被发送到同一台后端服务器。
5.3 内容分发网络
对于内容分发网络(CDN),可以使用 uri
或 url_param
算法来确保特定内容的请求被发送到预先定义的服务器,从而优化缓存效果。