负载均衡能够将客户端流量平均分配到服务器阵列,解决大量并发访问服务问题,是分布式场景下服务治理的重要手段。
1、什么是负载均衡
早期互联网应用比较简单,单台服务器即可满足负载需求,也就不存在什么均衡问题。随着流量增大,单机变成了服务器集群,此时流量会由多台服务器共同承载,此时就要保证每台服务器能够“平均”的承载流量,防止流量集中到某个单台机器,导致机器甚至集群崩溃。在实际操作中,均衡策略往往不是按照流量进行平均分流的,因为集群机器性能会有所不同。
总结来说,负载均衡(load balancer)是指把用户访问流量,通过负载均衡器,根据某种均衡策略分发到多台服务器上,从而实现分散负载的效果。
2、负载均衡分类
负载均衡根据所采用的设备对象(软/硬件负载均衡),应用的 OSI 网络层次(网络层次上的负载均衡:二层负载均衡MAC、三层负载均衡IP、四层负载均衡TCP-IP+port、七层负载均衡HTTP),及应用的地理结构(本地/全局负载均衡)等有不同的分类方式。
从软硬件角度看,硬件负载均衡是通过专门的硬件设备来实现负载均衡,功能强大,性能好,稳定,同时具备较好的安全防护功能,但同时价格也非常昂贵,维护和扩展能力也比较差。而软件负载均衡可以在普通服务器上运行使用,根据不同场景可以选择四层或七层负载均衡,易操作灵活,比如常用的NGINX、HAproxy、LVS等。
从网络分层角度,从二层负载均衡MAC、三层负载均衡IP、四层负载均衡TCP-IP+port,到七层负载均衡HTTP,适用于不同的业务场景。其中NGINX一般是七层负载均衡支持HTTP、email协议,同时也支持四层负载均衡;LVS运行在内核态,运行在三层负载均衡,性能也较好。
不论何种负载均衡器,都需要通过均衡策略来实现系统的负载均衡。
3、负载均衡策略
常用的负载均衡策略有随机策略、轮询策略、最近最少访问策略、粘滞策略、一致性hash策略、组合策略,每种策略都有其特点。
假设有N台服务器{S0,S1,...,Sn},那么负载均衡策略实际就是如何选取服务器,从而保证在一定时间段内,每个服务器收到的流量是均衡的。
3.1、随机策略
随机策略就是每次都通过随机算法从N台服务器中选取出一个来承载本次流量。从概率论角度看,短期内可能流量有一定程度失衡,但是长时间后是趋向于均衡。
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
servers := []string{"A","B","C"}
cntMap := make(map[string]int)
for _, server := range servers {
cntMap[server] = 0
}
times := 0
for times <= 10000 {
cntMap[randomV1(servers)] = cntMap[randomV1(servers)]+1
times++
}
fmt.Print(cntMap)
}
func randomV1(servers []string) string {
if len(servers) == 0 {
return ""
}
rand.Seed(time.Now().UnixNano())
return servers[rand.Intn(len(servers))]
}
// 演示结果
// 10000次
$ map[A:3315 B:3368 C:3318]%
// 100次
$ map[A:25 B:37 C:39]%
从这里看出,短期内随机策略的实际效果有较大偏差;在较长时间后,随机策略的实际效果是非常良好的。
另外,还有一种情况是,当服务器资源配置不同时,给每个服务器分配的权重就会有所不同,此时就不能简单采用随机选取策略。以下是集中随机计算策略。
策略1:采取枚举方式将权重转换为机器数量,这样就模拟为服务器数量为权重总和。然后以权重总和为最大值进行随机选取
servers = {"A":4, "B":2, "C":1}
virtual_servers = {"A","A","A","A","B","B","C"}
然后根据随机策略选取virtual_servers中的一个节点
该策略比较简单,但是最大的问题是,如果服务器权重较大,那么服务器集合的总长度本身就会占用极大内存空间。
策略2:将总权重视作一段固定长度,每个服务器都是其中的一段。再在这个总长度中计算一个随机偏移量offset,并计算落在哪个服务器区间,从而选取出对应服务器。
该策略不占用额外的内存空间,但是选取时需要遍历服务器集合,时间复杂度O(N).
策略3:对策略2进行简单优化,将权重较大的服务器放到前面,则offset跨越的区间会减少,从而降低遍历次数。
在实际dubbo的随机策略实现中,代码先对节点进行遍历判断,如果各服务器节点权重都相同,则会退化为简单随机策略;如果权重不一致,则采用策略2中的算法来获取服务器节点。
3.2、轮询(Round-Robin)策略
轮询策略就是顺序遍历可用服务器节点集合中的每个节点。因此每次选取节点都需要知道上次访问位置,然后本次offset+1即为当前选取节点。
而在各个节点权重不同时,一种方案是类似随机策略中的策略,按照权重转换为机器列表,再按照顺序轮询 方式选取节点。这样就导致权重低的节点在一段时间内处于空闲状态,没有平滑分配机器节点。
一种改进方案就是平滑带权重轮询方法(NGINX使用)
效果如下:
A:3, B:2, C:1
# 普通轮询方案
A-->A-->A-->B-->B-->C --A-->A-->A-->B-->B-->C
# 平滑带权重轮询方案
A-->B-->A-->C-->B-->A -->A-->B-->A-->C-->B-->A
该策略的Golang实现如下:
package main
import "fmt"
func main() {
A := newNode("A", 3)
B := newNode("B", 2)
C := newNode("C", 1)
nodes := []*node{A, B, C}
times := 0
for times < 12 {
fmt.Print(roundRobinWeight(nodes) + "-->")
times++
}
}
func roundRobinWeight(nodes []*node) string {
var choose *node
total := 0
for _, node := range nodes {
node.currWeight += node.effectiveWeight
total += node.effectiveWeight
if node.effectiveWeight < node.weight {
node.effectiveWeight += 1
}
if choose == nil || choose.currWeight < node.currWeight {
choose = node
}
}
if choose == nil {
return ""
}
choose.currWeight -= total
return choose.name
}
type node struct {
name string
weight int
effectiveWeight int
currWeight int
failNum int
}
func newNode(name string, weight int) *node {
return &node{
name: name,
weight: weight,
effectiveWeight: weight,
currWeight: 0,
failNum: 0,
}
}