负载均衡原理及Golang实现

负载均衡能够将客户端流量平均分配到服务器阵列,解决大量并发访问服务问题,是分布式场景下服务治理的重要手段。

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,
	}
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Go 中,可以使用 select 语句实现负载均衡。具体地,可以将请求分发给多个处理器进行处理,以实现负载均衡的效果。以下是一个简单的示例代码: ```go func main() { numWorkers := 3 jobs := make(chan int, 5) results := make(chan int, 5) for i := 0; i < numWorkers; i++ { go worker(i, jobs, results) } for i := 0; i < 5; i++ { jobs <- i } close(jobs) for i := 0; i < 5; i++ { fmt.Println(<-results) } } func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Println("worker", id, "processing job", j) time.Sleep(time.Second) results <- j * 2 } } ``` 在上述代码中,我们首先创建了三个处理器(即 worker),并将它们分别放入 goroutine 中运行。然后,我们创建了两个通道,jobs 用于传递任务,results 用于传递任务处理结果。接下来,我们往 jobs 通道中发送了 5 个任务,然后关闭了 jobs 通道,以便告诉 worker 程序已经发送完毕。最后,我们从 results 通道中读取处理结果并打印出来。 在 worker 函数中,我们使用 range 关键字迭代 jobs 通道中的任务。每当有新的任务到来时,worker 就会输出该任务的编号,并将处理结果发送到 results 通道中。由于使用了 time.Sleep(time.Second) 语句,因此每个任务的处理时间为 1 秒钟。 在主函数中,我们使用 select 语句来实现负载均衡。具体地,select 会等待 jobs 和 results 通道中的数据,并分别将它们分发给空闲的 worker 进行处理。当某个 worker 完成一个任务时,会将处理结果发送到 results 通道中,然后再次进入 select 语句等待新的任务。 总之,通过使用 select 语句实现负载均衡,可以使得多个处理器能够协同处理任务,提高系统的并发处理能力和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值