微服务网关(九)负载均衡
四大负载均衡策略
-
随机负载
随机挑选目标服务器IP
-
轮询负载
ABC三台服务器,以ABCABC的顺序依次轮询
-
加权轮询
给目标服务器设置访问权重值,按照权重轮询负载
-
一致性哈希轮询
固定的一个客户端IP请求访问下游服务器都是访问一个指定IP
随机负载均衡
主要逻辑
使用rand.Intn方法获取随机索引
代码主体
type RandomBalance struct {
//索引值
curIndex int
//包含IP地址的数组
rss []string
//观察主体(暂时不看,等到服务发现的时候再看)
conf LoadBalanceConf
}
//添加服务器IP
func (r *RandomBalance) Add(params ...string) error {
if len(params) == 0 {
return errors.New("param len 1 at least")
}
addr := params[0]
r.rss = append(r.rss, addr)
return nil
}
//获取随机索引,用于实现随机负载均衡,由之后的get方法封装
func (r *RandomBalance) Next() string {
if len(r.rss) == 0 {
return ""
}
r.curIndex = rand.Intn(len(r.rss))
return r.rss[r.curIndex]
}
轮询负载均衡
主要逻辑
索引值加一取余
代码主体
type RoundRobinBalance struct {
//索引值
curIndex int
//包含IP地址的数组
rss []string
//观察主体
conf LoadBalanceConf
}
func (r *RoundRobinBalance) Add(params ...string) error {
if len(params) == 0 {
return errors.New("param len 1 at least")
}
addr := params[0]
r.rss = append(r.rss, addr)
return nil
}
func (r *RoundRobinBalance) Next() string {
if len(r.rss) == 0 {
return ""
}
lens := len(r.rss) //5
if r.curIndex >= lens {
r.curIndex = 0
}
curAddr := r.rss[r.curIndex]
//简单的加一取余逻辑
r.curIndex = (r.curIndex + 1) % lens
return curAddr
}
加权负载均衡
-
Weight:初始化时对节点约定的权重
-
currentWeight:节点临时权重,每轮都会变化
currentWeight = currentWeight + effectiveWeight
-
effectiveWeight:节点有效权重,默认与Weight相同
如果一个节点发生故障,那么这个节点的effectiveWeight就会是Weight-1
例如A:B:C = 2:1:1,A节点故障一次,那么就将A节点的effectiveWeight - 1 = 1;直到为零,可以有效控制故障节点的移除
-
totalWeight:所有节点有效权重之和:sum(effectiveWeight)
主要逻辑
- currentWeight = currentWeight + effectiveWeight
- 选中最大的currentWeight节点为选中节点
- 选中节点的currentWeight = currentWeight - totalWeight
代码主体
type WeightRoundRobinBalance struct {
curIndex int
rss []*WeightNode
rsw []int
//观察主体
conf LoadBalanceConf
}
type WeightNode struct {
addr string
weight int //权重值
currentWeight int //节点当前权重
effectiveWeight int //有效权重
}
func (r *WeightRoundRobinBalance) Add(params ...string) error {
if len(params) != 2 {
return errors.New("param len need 2")
}
parInt, err := strconv.ParseInt(params[1], 10, 64)
if err != nil {
return err
}
node := &WeightNode{addr: params[0], weight: int(parInt)}
node.effectiveWeight = node.weight
r.rss = append(r.rss, node)
return nil
}
func (r *WeightRoundRobinBalance) Next() string {
total := 0
var best *WeightNode
for i := 0; i < len(r.rss); i++ {
w := r.rss[i]
//step 1 统计所有有效权重之和
total += w.effectiveWeight
//step 2 变更节点临时权重为的节点临时权重+节点有效权重
w.currentWeight += w.effectiveWeight
//step 3 有效权重默认与权重相同,通讯异常时-1, 通讯成功+1,直到恢复到weight大小
if w.effectiveWeight < w.weight {
w.effectiveWeight++
}
//step 4 选择最大临时权重点节点
if best == nil || w.currentWeight > best.currentWeight {
best = w
}
}
if best == nil {
return ""
}
//step 5 变更临时权重为 临时权重-有效权重之和
best.currentWeight -= total
return best.addr
}
一致性hash
-
单调性
通过hash函数运算出来的值都是唯一固定的值
-
平衡性
一致性hash中有两种元素,一个是数据元素,一个是服务器元素。数据元素是否均匀落地到目标元素便是k考虑的平衡性
-
分散性
考虑hash函数设计上是否有哈希散列性
主要逻辑
-
将对象映射到环空间上
假如有4个用户对象,通过hash函数,产生对应的key,通过单调hash函数,将值存储到环的对应位置上面
-
把server映射到环空间上
假如有3个服务器,通过hash函数,产生对应的key,通过单调hash函数,将值存储到环的对应位置上面
-
将对象映射到服务器上
上图映射在环上后,会以顺时针的顺序做映射关系,映射关系如图所示
-
添加和移除服务器
删除服务器B,添加服务器D,如图所示。那么对象c将会指向服务器A,对象a将会指向服务器D
优点:因为我们是一致性hash的算法,因此就保持了单调性,所以就会以最小的形式将对象映射到服务器上面,这么做对分布式集群来说是非常合适的,它避免了大量的数据迁移,减轻了服务器的压力。
一致性hash的平衡性
哈希算法并不能保证百分百的平衡性,当服务器比较少的时候,对象就不能均匀地映射到服务器上。因此引入虚拟节点的概念
虚拟节点
服务器节点具有几个拷贝的值,它们会按照一定的规则,均匀地分布到环上
代码主体
结构体
type ConsistentHashBanlance struct {
mux sync.RWMutex
hash Hash //环结构
replicas int //复制因子(虚拟节点)
keys UInt32Slice //已排序的节点hash切片
hashMap map[uint32]string //节点哈希和Key的map,键是hash值,值是节点key
conf LoadBalanceConf //观察主体
}
构造函数
func NewConsistentHashBanlance(replicas int, fn Hash) *ConsistentHashBanlance {
m := &ConsistentHashBanlance{
replicas: replicas,
hash: fn,
hashMap: make(map[uint32]string),
}
if m.hash == nil {
//最多32位,保证是一个2^32-1环
m.hash = crc32.ChecksumIEEE
}
return m
}
Add方法
添加节点
- 根据所有虚拟节点来逐一计算hash值
- 将所得的hash值存入已排序的节点hash切片(m.keys)中,同时将map中通过hash值对应上服务器节点IP
// Add 方法用来添加缓存节点,参数为节点key,比如使用IP
func (c *ConsistentHashBanlance) Add(params ...string) error {
if len(params) == 0 {
return errors.New("param len 1 at least")
}
addr := params[0]
c.mux.Lock()
defer c.mux.Unlock()
// 结合复制因子计算所有虚拟节点的hash值,并存入m.keys中,同时在m.hashMap中保存哈希值和key的映射
for i := 0; i < c.replicas; i++ {
hash := c.hash([]byte(strconv.Itoa(i) + addr))
c.keys = append(c.keys, hash)
c.hashMap[hash] = addr
}
// 对所有虚拟节点的哈希值进行排序,方便之后进行二分查找
sort.Sort(c.keys)
return nil
}
Get方法
获取对象顺时针所对应的最近服务器节点
- 通过二分法查找第一个大于数据对象的服务器节点索引值(顺时针的第一个节点)
- 如果查找结果大于服务器节点的最大索引,就返回第一个节点的索引值
- 根据索引返回节点
// Get 方法根据给定的对象获取最靠近它的那个节点
func (c *ConsistentHashBanlance) Get(key string) (string, error) {
if c.IsEmpty() {
return "", errors.New("node is empty")
}
hash := c.hash([]byte(key))
// 通过二分查找获取最优节点,第一个"服务器hash"值大于"数据hash"值的就是最优"服务器节点"
idx := sort.Search(len(c.keys), func(i int) bool { return c.keys[i] >= hash })
// 如果查找结果 大于 服务器节点哈希数组的最大索引,表示此时该对象哈希值位于最后一个节点之后,那么放入第一个节点中
if idx == len(c.keys) {
idx = 0
}
c.mux.RLock()
defer c.mux.RUnlock()
return c.hashMap[c.keys[idx]], nil
}
为反向代理插上负载均衡的翅膀
-
使用工厂方法进行拓展
通过设置工厂方法传入的参数,指定我们使用的对应负载均衡策略
-
使用接口统一封装
将方法统一,返回时使用统一的方法返回
代码主体
注:iota,这里会是按顺序0123
LbRandom->0
LbRoundRobin->1
LbWeightRoundRobin->2
LbConsistentHash->3
在使用iota时,需要注意以下几点:
- 每当定义一个const,iota的初始值为0
- 每当定义一个常量,就会自动累加1
- 直到下一个const出现,清零
- 如果中断iota自增,则必须显式恢复。且后续自增值按行序递增
- 自增默认是int类型,可以自行进行显示指定类型
- iota 可以参与运算
type LbType int
const (
LbRandom LbType = iota
LbRoundRobin
LbWeightRoundRobin
LbConsistentHash
)
func LoadBanlanceFactory(lbType LbType) LoadBalance {
switch lbType {
case LbRandom:
return &RandomBalance{}
case LbConsistentHash:
return NewConsistentHashBanlance(10, nil)
case LbRoundRobin:
return &RoundRobinBalance{}
case LbWeightRoundRobin:
return &WeightRoundRobinBalance{}
default:
return &RandomBalance{}
}
}
由于我们的目的是LoadBanlanceFactory方法返回统一的LoadBanlance这个接口
所以定义LoadBanlance接口
type LoadBalance interface {
Add(...string) error
Get(string) (string, error)
//后期服务发现补充
Update()
}
main.go使用时,传入常量即可获得负载均衡策略。接着便是添加节点,将策略传入反向代理并开启服务(反向代理的创建参考微服务网关(六)网络代理详解)
func main() {
//获得负载均衡策略
rb := load_balance.LoadBanlanceFactory(load_balance.LbWeightRoundRobin)
//添加节点
if err := rb.Add("http://127.0.0.1:2003/base", "10"); err != nil {
log.Println(err)
}
if err := rb.Add("http://127.0.0.1:2004/base", "20"); err != nil {
log.Println(err)
}
//将添加好节点的负载均衡策略传入反向代理
proxy := NewMultipleHostsReverseProxy(rb)
log.Println("Starting httpserver at " + addr)
//开启服务监听
log.Fatal(http.ListenAndServe(addr, proxy))
}
将策略传入反向代理NewMultipleHostsReverseProxy(反向代理的创建参考微服务网关(六)网络代理详解)
func NewMultipleHostsReverseProxy(lb load_balance.LoadBalance) *httputil.ReverseProxy {
//请求协调者
director := func(req *http.Request) {
//获取负载均衡策略中取得的下游地址(根据实际需求决定)
//基于客户端ip获得下游的服务(127.0.0.1)
nextAddr, err := lb.Get(req.RemoteAddr)
//基于客户端请求的URL获得下游的服务(/base/ba)
//nextAddr, err := lb.Get(req.URL.String())
if err != nil {
log.Fatal("get next addr fail")
}
target, err := url.Parse(nextAddr)
if err != nil {
log.Fatal(err)
}
targetQuery := target.RawQuery
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "user-agent")
}
}
//更改内容
modifyFunc := func(resp *http.Response) error {
//请求以下命令:curl 'http://127.0.0.1:2002/error'
if resp.StatusCode != 200 {
//获取内容
oldPayload, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
//追加内容
newPayload := []byte("StatusCode error:" + string(oldPayload))
resp.Body = ioutil.NopCloser(bytes.NewBuffer(newPayload))
resp.ContentLength = int64(len(newPayload))
resp.Header.Set("Content-Length", strconv.FormatInt(int64(len(newPayload)), 10))
}
return nil
}
//错误回调 :关闭real_server时测试,错误回调
//范围:transport.RoundTrip发生的错误、以及ModifyResponse发生的错误
errFunc := func(w http.ResponseWriter, r *http.Request, err error) {
//todo 如果是权重的负载则调整临时权重
http.Error(w, "ErrorHandler error:"+err.Error(), 500)
}
return &httputil.ReverseProxy{Director: director, Transport: transport, ModifyResponse: modifyFunc, ErrorHandler: errFunc}
}
gth", strconv.FormatInt(int64(len(newPayload)), 10))
}
return nil
}
//错误回调 :关闭real_server时测试,错误回调
//范围:transport.RoundTrip发生的错误、以及ModifyResponse发生的错误
errFunc := func(w http.ResponseWriter, r *http.Request, err error) {
//todo 如果是权重的负载则调整临时权重
http.Error(w, "ErrorHandler error:"+err.Error(), 500)
}
return &httputil.ReverseProxy{Director: director, Transport: transport, ModifyResponse: modifyFunc, ErrorHandler: errFunc}
}