go 实现负载均衡器代码细节
文章目录
代码实现
原理介绍
版本1.0
版本2.0
**为了实现main函数等待所有其他协程执行完毕后再结束,使用了 sync.WaitGroup。**WaitGroup是一个同步原语,它等待一组操作完成。你可以通过调用Add方法来增加计数,然后在协程中调用Done方法来减少计数,最后调用Wait方法来阻塞直到计数归零。
1、增加请求体
在model包下新建 custom_data.go:
package model
type CustomData struct {
Data string
}
新建 request.go:
package model
import "time"
// Request 结构体表示一个请求的基本信息。
type Request struct {
// RequestID 是请求的唯一标识符。
RequestID int `json:"requestId"`
// RequestType 表示请求的类型,例如GET、POST等。
RequestType string `json:"requestType"`
// RequestTime 表示请求的发起时间。
RequestTime time.Time `json:"requestTime"`
// ClientIP 表示请求的来源IP地址。
ClientIP string `json:"clientIp"`
// Payload 表示请求的负载,可以是请求的具体数据。
Payload interface{} `json:"payload"`
}
2、增加服务节点信息结构体
一个服务器实例不只是一个单独的名称,因此,版本2.0补充了其他信息。
- NewNode 创建一个新的Node实例,并初始化相关的字段。通过通道接收请求数据。并且启动一个goroutine运行节点的主循环 run 方法。
- run 方法是Node实例的主循环,用于处理接收到的请求和完成信号。
- AcceptRequest 尝试接受请求,如果成功则返回 true,否则返回 false。
- ProcessRequest 模拟处理请求,并根据处理时间更新负载。
- ReleaseRequest 释放请求,减少节点的负载。
package server
import (
"fmt"
"strconv"
"sync"
"time"
"xun-go/com/xun/load-banlance/model"
)
// WG 同步原语,等待一组操作完成
var WG sync.WaitGroup
// Node 结构体代表网络中的一个节点,例如服务器或服务实例。包含名称、IP、端口、缓冲区容量、当前负载和状态等信息
type Node struct {
// NodeId 是节点的唯一ID,用于在系统中唯一识别节点。
NodeId int `json:"nodeId"`
// NodeName 是节点的名称,用于标识节点。
NodeName string `json:"nodeName"`
// NodeIpAddr 是节点的IP地址,用于网络通信。
NodeIpAddr string `json:"nodeIpAddr"`
// Port 是节点监听的端口号,用于网络通信。
Port string `json:"port"`
// Weight 是节点的权重,用于负载均衡算法,权重高的节点将接收更多请求。
Weight int `json:"weight"`
// Status 表示节点的状态,0表示离线,1表示在线。这个字段用于监控节点的运行状态。
Status int `json:"status"`
// BufferSize 是节点的缓冲区大小,用于接收网络请求。这个字段反映了节点的请求接收队列大小。
BufferSize int `json:"bufferSize"`
// Load 是节点的当前负载,用于评估节点的繁忙程度。这个字段可以用于动态调整负载均衡策略。
NodeLoad int `json:"nodeLoad"`
// ProcessingTime 是节点处理每个请求的平均时间,用于衡量节点的处理能力。
ProcessingTime int `json:"processingTime"`
// ProcessedRequestCount 是节点处理的请求数量,用于统计节点的处理情况。
ProcessedRequestCount int `json:"processedRequestCount"`
// mu 是一个互斥锁,用于保护节点的状态和负载信息。
mu sync.Mutex `json:"mu"`
// RequestChan 是用于接收请求通知的通道。
RequestChan chan model.Request `json:"requestChan"`
// DoneChan 是用于处理请求完成通知的通道。
DoneChan chan struct{} `json:"doneChan"`
}
// NewNode 创建一个新的Node实例,并初始化相关的字段。
// 参数包括节点的ID、名称、IP地址、端口、权重、状态、缓冲区大小和处理时间。
// 返回值是一个指向Node结构体的指针。
func NewNode(nodeId int, nodeName string, ipAddr string, port string, weight int, status int, bufferSize int, processingTime int) *Node {
// 创建Node实例
node := &Node{
NodeId: nodeId, // 设置节点ID
NodeName: nodeName, // 设置节点名称
NodeIpAddr: ipAddr, // 设置节点的IP地址
Port: port, // 设置节点的端口号
Weight: weight, // 设置节点的权重
Status: status, // 设置节点的状态
BufferSize: bufferSize, // 设置节点的缓冲区大小
NodeLoad: 0, // 初始化节点负载为0
mu: sync.Mutex{}, // 初始化互斥锁,用于保护共享资源
ProcessingTime: processingTime, // 设置节点处理请求的平均时间
RequestChan: make(chan model.Request), // 创建一个无缓冲的通道,用于接收请求信号
DoneChan: make(chan struct{}), // 创建一个无缓冲的通道,用于接收完成信号
}
// 启动一个goroutine运行节点的主循环
go node.run()
// 返回创建的Node实例
return node
}
// run 方法是Node实例的主循环,用于处理接收到的请求和完成信号。
func (n *Node) run() {
for {
// 使用select语句监听两个通道
select {
case request := <-n.RequestChan: // 当接收到请求信号时
// 启动一个新的goroutine来处理请求
WG.Add(1)
go n.ProcessRequest(request)
case <-n.DoneChan: // 当接收到完成信号时
// 启动一个新的goroutine来释放请求占用的资源
WG.Add(1)
go n.ReleaseRequest()
}
}
}
// AcceptRequest 尝试接受请求,如果成功则返回 true,否则返回 false。
func (n *Node) AcceptRequest() bool {
n.mu.Lock()
defer n.mu.Unlock()
if n.NodeLoad < n.BufferSize && n.Status == 1 {
n.NodeLoad++
return true
}
return false
}
// ReleaseRequest 释放请求,减少节点的负载。
func (n *Node) ReleaseRequest() {
defer WG.Done()
n.mu.Lock()
defer n.mu.Unlock()
fmt.Println(n.NodeIpAddr + " Processed a Request")
if n.NodeLoad > 0 {
n.NodeLoad--
}
n.ProcessedRequestCount++
}
// ProcessRequest 模拟处理请求,并根据处理时间更新负载。
func (n *Node) ProcessRequest(customRequest model.Request) {
defer WG.Done()
fmt.Println(n.NodeIpAddr + " recive a Request " + strconv.Itoa(customRequest.RequestID))
time.Sleep(time.Duration(n.ProcessingTime) * time.Second) // 模拟请求处理时间
// 进行类型断言来获取自定义结构体负载的数据
if data, ok := customRequest.Payload.(model.CustomData); ok {
fmt.Printf(n.NodeIpAddr+" recive Payload: data=%s\n", data)
} else {
fmt.Println(n.NodeIpAddr + " recive Payload is not a CustomData struct")
}
n.DoneChan <- struct{}{} // 通知处理完成
}
// SetStatus 设置节点状态。
func (n *Node) SetStatus(status int) {
n.Status = status
}
3、将版本1.0中的server从string改为定义的结构体Node
这里以其中一个最少连接数负载均衡器的实现为例,其他文件的修改可以参考git的commit记录:
// SelectServer 是 LeastConnection 负载均衡策略的核心方法,它从提供的服务器列表中选择连接数最少的服务器。
// 如果服务器列表为空,则返回错误。
// 使用互斥锁确保在并发环境中正确地读取和更新连接数。
func (lc *LeastConnection) SelectServer(servers []*server.Node) (*server.Node, error) {
if len(servers) == 0 { // 检查服务器列表是否为空
return nil, errors.New("no servers available") // 如果为空,返回错误
}
lc.mu.Lock() // 加锁,防止并发修改
defer lc.mu.Unlock() // 解锁
// 初始化最小连接数为最大整数值
minConnections := int(^uint(0) >> 1)
var selectedServer *server.Node // 用于存储连接数最少的服务器
// 遍历服务器列表,找到连接数最少的服务器
for _, server := range servers {
if lc.connections[server.NodeIpAddr] < minConnections {
minConnections = lc.connections[server.NodeIpAddr]
selectedServer = server
}
}
// 更新选中服务器的连接数
lc.connections[selectedServer.NodeIpAddr]++
return selectedServer, nil // 返回选中的服务器地址
}
// ReleaseServer 减少服务器的连接数。
// 当一个连接结束时,调用此方法来减少对应服务器的连接计数。
func (lc *LeastConnection) ReleaseServer(server *server.Node) {
lc.mu.Lock() // 加锁,确保并发安全
defer lc.mu.Unlock() // 解锁
// 减少服务器的连接数,确保不会减至负数
if lc.connections[server.NodeIpAddr] > 0 {
lc.connections[server.NodeIpAddr]--
}
}
4、平滑加权轮询负载均衡器实现
平滑加权轮询(Smooth Weighted Round Robin, SWRR)是一种优化的负载均衡策略,用于平滑地分配请求到具有不同权重的服务器上。其核心思想是通过在每轮选择权重最大的服务器,并动态调整服务器的临时权重,实现更均匀的请求分配。通过在每一轮选出权重最大的服务实例,然后将该实例的权重减去总权重,再对所有实例都加上各自原本的权重。可以起到一个平滑的分配作用,而不是在分配给一个服务后,单纯的将它的权重减1,以减少它的分配概率。
平滑加权轮询(Smooth Weighted Round Robin, SWRR)负载均衡策略的核心目标是通过动态调整服务器的临时权重来实现请求的平滑分配,避免某些服务器连续接收大量请求,而其他服务器长时间不被使用。以下是为什么这种算法能够实现平滑分配的详细解释:
临时权重计算过程
- 增加所有服务器的当前权重:
每个服务器在每一轮都会增加其固定权重 (W_i),即:
[CW_i = CW_i + W_i]
这一步确保了每个服务器的当前权重逐渐增加,体现其处理请求的能力。 - 选择当前权重最大的服务器:
在每一轮中,选择当前权重最大的服务器 (S_{max}):_
[S_{max} = argmax(CW_i)] - 调整被选中服务器的当前权重:
被选中的服务器 (S_{max}) 的当前权重需要减去总权重 (W_{total} = \sum_{i=1}^{n} W_i):_
[CW_{max} = CW_{max} - W_{total}]
这一步确保了被选中服务器的当前权重立即减少,防止其在接下来的几轮中继续被选中,直到其他服务器的当前权重也足够高。
实现平滑分配的原理
-
累积权重反映负载能力:
每台服务器的当前权重 (CW_i) 都是在其固定权重 (W_i) 基础上累积的,这反映了它的负载能力。权重高的服务器在每轮中累积权重的速度快,更容易被选中。 -
动态调整防止连续选中:
当一个服务器被选中后,其当前权重 (CW_{max}) 减去总权重 (W_{total}),使其在接下来的几轮中不容易被选中。这样可以防止某个服务器连续多次被选中,避免负载的不平衡。 -
平滑过渡:
由于每轮中所有服务器的当前权重 (CW_i) 都会增加其固定权重 (W_i),即使某个服务器在前一轮被选中并减少了当前权重,在接下来的几轮中也会逐渐恢复其当前权重。这样,当权重逐渐平衡后,服务器的选中概率恢复,达到平滑过渡的效果。
例子说明
假设有三台服务器 (S_1)、(S_2)、(S_3),权重分别为5、1、1,具体操作如下:
-
初始状态:
[CW_1 = 0, CW_2 = 0, CW_3 = 0] -
第一轮:
[CW_1 = 5, CW_2 = 1, CW_3 = 1]
选择 (S_1),调整权重:
[CW_1 = 5 - 7 = -2] -
第二轮:
[CW_1 = -2 + 5 = 3, CW_2 = 1 + 1 = 2, CW_3 = 1 + 1 = 2]
选择 (S_1),调整权重:
[CW_1 = 3 - 7 = -4] -
第三轮:
[CW_1 = -4 + 5 = 1, CW_2 = 2 + 1 = 3, CW_3 = 2 + 1 = 3]
选择 (S_2) 或 (S_3)(假设选择 (S_2)),调整权重:
[CW_2 = 3 - 7 = -4]
通过这种方式,每台服务器的选中概率都能够在其权重范围内平滑变化,避免了某一台服务器在短时间内过载,而其他服务器长期闲置,从而实现平滑的负载均衡。
package loadbalancer
import (
"errors"
"sync"
"time"
"xun-go/com/xun/load-banlance/server"
)
// WeightNode 服务器节点负载信息
type WeightNode struct {
Idx int // 服务器节点在列表中的索引
IpAddr string // 服务器实例IP地址
CurWeight int // 当前权重
Weight int // 初始化时设置的权重
}
type WeightedRoundRobin struct {
nodes []*server.Node
totalWeight int
weightNodes []*WeightNode
mu sync.Mutex
}
// NewWeightedRoundRobin 创建并返回一个新的 WeightedRoundRobin 负载均衡策略实例。
func NewWeightedRoundRobin(nodes []*server.Node) *WeightedRoundRobin {
w := &WeightedRoundRobin{nodes: nodes}
// 初始化各个服务器的负载,获取 WeightedRoundRobin
for idx, node := range nodes {
if node.Status == 1 {
w.totalWeight += node.Weight
w.weightNodes = append(w.weightNodes, &WeightNode{
Idx: idx,
IpAddr: node.NodeIpAddr,
CurWeight: 0,
Weight: node.Weight,
})
}
}
// 启动后台任务定期检测服务器列表变化,更新各个服务器的负载
go w.DetectServers()
return w
}
// DetectServers 检测服务器列表,并更新负载均衡策略。可以用于后台任务定时检测服务器列表的变化。
func (w *WeightedRoundRobin) DetectServers() error {
for {
// 每10分钟检测一次
time.Sleep(10 * time.Minute)
w.mu.Lock()
w.totalWeight = 0
w.weightNodes = w.weightNodes[:0]
for idx, node := range w.nodes {
if node.Status == 1 {
// 加入存活的服务器
w.totalWeight += node.Weight
w.weightNodes = append(w.weightNodes, &WeightNode{
Idx: idx,
IpAddr: node.NodeIpAddr,
CurWeight: 0,
Weight: node.Weight,
})
}
}
if w.totalWeight == 0 {
return errors.New(time.Now().String() + " has no servers available!")
}
w.mu.Unlock()
}
return nil
}
// SelectServer 根据负载均衡策略选择一个服务器实例。
func (w *WeightedRoundRobin) SelectServer(servers []*server.Node) (*server.Node, error) {
w.mu.Lock()
defer w.mu.Unlock()
var bestNode *server.Node // 最佳服务器实例
var bestWeight int // 最佳服务器实例的负载
var bestIdx int // 最佳服务器实例在全局列表中的索引
// 遍历服务器的负载信息列表
for idx, node := range w.weightNodes {
if w.nodes[node.Idx].Status == 0 {
// 服务器不在线,不可用
continue
}
// 对均衡器中的所有后端服务增加自己的权重 Weight,起到平滑分配的作用
node.CurWeight += node.Weight
// 找到当前权重最大的服务器实例
if bestNode == nil || node.CurWeight > bestWeight {
bestNode = w.nodes[node.Idx]
bestIdx = idx
}
}
if bestNode == nil {
// 没有可用的服务器实例,启动后台任务检测服务器列表变化
go w.DetectServers()
return nil, errors.New(time.Now().String() + " has no servers available!")
}
// 更新当前负载
w.weightNodes[bestIdx].CurWeight -= w.totalWeight
return bestNode, nil
}
未完待续:之后会继续使用Hash策略以及节点的移除,节点异常不可用时的操作等。