浅析负载均衡

276 阅读3分钟

1. 概述

负载均衡(LoadBalance)是微服务架构的重要能力。负载均衡是按照一定的策略,将请求分配给服务器。 举个例子,一主三从的数据库架构,当客户端的查询请求选择请求从库时,可以选择直连一台从库工作,但从库集群部署的意义就失去了。此时就需要有负载均衡,根据策略将请求分配给从库阶段。 通过负载均衡分配流量,提高系统的可用性。

由谁来实现和负责负载均衡?可以是中心代理服务,可以是由sidecar程序调度,甚至可以是在客户端框架层实现,取决使用的基础架构能力。

2. 负载均衡策略

负载均衡有一些常用的策略。

完全随机 每一个请求到来,均随机分配给集群节点。

轮询 按顺序分配给集群节点。

权重轮询 每个集群节点都有一个权重值,按照权重值轮询,权重越高分配的请求越多。

哈希 基于特定key哈希(如IP、Cookie等),固定分配到某一个集群节点。比如http层保持用户Session;数据库主从分离查询,从库实例的选择上,可以基于用户ID做哈希负载均衡,进而实现「单调读」。

一致性哈希 将哈希结果映射到哈希环上,比起普通哈希策略,使用一致性哈希策略能更加友好的应对节点变更的场景。 普通哈希会使得增加或者删除节点时会使得原有的hash结果大规模失效,比如节点数从5 -> 7,原先hash值15 mod 5 = 0节点,变更后 15 mod 7 = 1节点。而使用hash环只会影响新节点临近的节点。

3. 权重轮询的实现

📌 权重轮询是应用最广泛的负载均衡策略。普通的轮询可以理解为全节点等权的权重轮询。业务可以基于业务特性,赋予节点不同权重值,去定制系统负载均衡能力。

权重轮询一般特指平滑权重轮询,算法核心思想是,每一轮筛选,节点当前权重都增加该节点初始的权重值,选择权重最高的节点,被选中的节点在减去总权重值,每一轮筛选所有节点的权重总和都保持不变。算法核心逻辑用Golang表达如下

var selected *Node
totalWeight := 0

for _, node := range w.Nodes {
    node.CurrentWeight += node.Weight
    totalWeight += node.Weight
    if selected == nil || node.CurrentWeight > selected.CurrentWeight {
        selected = node
    }
}

selected.CurrentWeight -= totalWeight
return selected

Nginx实现的算法:github.com/nginx/nginx…

模拟代码实现

type WRRLB struct {
    Nodes []*Node
}

type Node struct {
    Name          string
    Weight        int
    CurrentWeight int
}

func NewWRRLB() *WRRLB {
    return &WRRLB{}
}

func (w *WRRLB) AddNode(node *Node) {
    w.Nodes = append(w.Nodes, node)
}

func (w *WRRLB) Pick() *Node {
    var selected *Node
    totalWeight := 0

    for _, node := range w.Nodes {
        node.CurrentWeight += node.Weight
        totalWeight += node.Weight
        if selected == nil || node.CurrentWeight > selected.CurrentWeight {
            selected = node
        }
    }

    selected.CurrentWeight -= totalWeight
    return selected
}

测试用例:使用三个节点命名A、B、C,权重分别为5、3、1,调度18次 运行结果:A选择了10次,B选择6次,C选择2次,符合权重比例。

func main() {
    lb := wrrlb.NewWRRLB()
    nodeA := &wrrlb.Node{Name: "A", Weight: 5}
    nodeB := &wrrlb.Node{Name: "B", Weight: 3}
    nodeC := &wrrlb.Node{Name: "C", Weight: 1}

    lb.AddNode(nodeA)
    lb.AddNode(nodeB)
    lb.AddNode(nodeC)

    times := 18
    count := make(map[string]int)
    for i := 0; i < times; i++ {
        node := lb.Pick()
        count[node.Name]++
        fmt.Println("Pick node:", node.Name)
    }
    fmt.Println("Count:", count)
}

lb_test1.png

4. 动态负载均衡

以权重轮询作为基础,负载均衡器可以增强节点权重的计算逻辑,实现动态负载均衡。 如平台人工调整权重,实时生效;灰度发布场景(如金丝雀发布);最常见的是根据节点的负载情况,,比如实例的错误率、延时、请求排队时间,动态计算一个权重(如故障实例自动剔除),滚动发布。

模拟节点下线 在上述模拟的代码中,增加一个goroutine,在500ms后,将A节点权重调整为0,模拟下线状态。查看运行效果,调整后A阶段不再被选择

go func() {
    select {
    case <-time.After(500 * time.Millisecond):
        nodeA.Weight = 0
        fmt.Println("Node A weight updated to 0")
    }
}()

times := 18
count := make(map[string]int)
for i := 0; i < times; i++ {
    node := lb.Pick()
    count[node.Name]++
    fmt.Println("Pick node:", node.Name)
    time.Sleep(100 * time.Millisecond)
}
fmt.Println("Count:", count)

lb_test2.png

标题