微服务-如何做好集群中服务器的负载均衡

那些负载均衡的面试题

简单说一下什么是负载均衡?很多人最怕这种概念性问题

你们公司负载均衡用的什么?

为什么用这种?

它的优缺点

有更好的选择吗?

你说这5联问,谁受得了啊,丛浅到深,一环扣一环,简直不要了,别怕,仔细阅读本文,这些问题都会迎刃而解。

什么是负载均衡?

俗话解释一下负载均衡:你要在10个餐厅中选一个吃午餐,那么你选的这个过程就是负载均衡的过程,(面试也是可以这么说的)。
正规的行话:负载均衡指的是在一个集群中通过某种硬件设备或者软件算法来选择集群中的一台机器处理当前请求,以达到大量请求的分散给后端集群不同机器处理,从而提升高并发能力和容灾能力。
百度百科:负载均衡建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性

软硬件负载均衡详解

目前负载均衡总的来说分为三大类:1 硬件设备负载均衡,2 软件算法负载均衡,3 基于DNS的负载均衡 分别介绍一下这三大类的不同和优缺点。

硬件负载均衡解决方案是直接在服务器和外部网络间安装负载均衡设备,这种设备通常称之为负载均衡器,由于专门的设备完成专门的任务,独立于操作系统,整体性能得到大量提高,加上多样化的负载均衡策略,智能化的流量管理,可达到最佳的负载均衡需求,其主要应用在大型服务器集群中,比如F5负载均衡器。

软件负载均衡指的是在服务器的操作系统上安装负载均衡软件,从此服务器发出的请求经软件负载均衡算法路由到后端集群的某一台机器上。

DNS负载均衡一般用于地理位置上的负载均衡,比如你的网站在全国范围内都有海量用户,那么当不同用户访问网站域名时经过DNS判断返回给不同地理位置的用户的不同IP,从而达到就近访问,流量分担,提升用户体验。

他们的优缺点是什么呢?

硬件负载均衡一般只是关注网络流量的负载,至于后端服务器的状态等他不操心,而且成本贵,往往也是单点,但它也有优点,就是性能好,处理能力强,与操作系统无关性。

软件负载均衡比较灵活,可调整性大,与软件算法实现有关系,能够关注应用服务器的状态做汇总统计试别的能力,性价比较高,但受软件安装的服务器性能影响,同时也没硬件的性能好,DNS负载均衡也属于软件负载均衡的一种。

本文主要分析的也是软件负载均衡。

常用的负载均衡算法和实现原理

负载均衡中间件现在很多,大家最熟悉的,也是最出名的就属Nginx了,其次也有很多,比如百度前段时间开源了bfe(百度统一前端),是百度7层流量转发平台,还有apache,各种微服务中间件中的负载均衡算法等

我们主要分析下这些中间件负载均衡策略是怎么实现的?用的什么算法,重点来了

  1. Random 随机
  2. Round Robin 轮询
  3. Weighted Round Robin 加权轮询
  4. Least Connections 最少连接
  5. Latency-Aware 延迟感知(最小延迟,也就是说那台机器性能最好,就用那台)
  6. Source Hashing 源地址散列
  7. Consistency hash 一致性散列(一般在分布式缓存中比较常见 )

随机策略指的是在后端集群机器的IP列表中根据随机数选择一个IP作为此次请求的应答者,当随机算法足够好,足够公平时,在海量请求下,最终后端集群各个机器承载的流量是均衡, 随机策略会导致配置较低的机器Down机,从而可能引起雪崩,一般采用随机算法时建议后端集群机器配置最好同等的,随机策略的性能取决与随机算法的性能。

轮询策略指的是在集群中对所有机器编号,假设10台机器,从0-9,请求来临时从0号机器开始,后续每来一次请求对编号加1,这样一直循环,上面的随机策略其实最后就变成轮询了,这两种策略都不关心机器的负载和运行情况,而且对变量操作会引入锁操作,性能也会下会下降。

加权轮询策略指的是回给后端集群每台机器都分配一个权重,权重高得会承担更多的流量,相反权重低的分配的流量也会少,这种策略允许后端集群机器配置差异化,假设有3台机器(a,b,c),他们的权重分别是(7,2,1),那么10次请求a机器承担7次,b机器承担2次,c机器承担1次,但是这种承担法到底怎么分配呢?有两种情况如下,我们可以看到第一种请求在a的时候,bc完全空闲,而第二种情况相对均匀一些,Nginx的加权轮询策略采用的就是第二种情况

  1. (aaaaaaa,bb,c)
  2. (aabaabaaca)

最少连接策略会关注后端集群各个服务器当前的连接数,选择一个最少连接数的机器应答当前请求,这种策略实际上关注各个服务器的负载情况,选择负载最低的机器处理请求,尽可能的提高各个机器的利用率,相对来说比较灵活和智能,实现上也会复杂一些。

延迟感知策略和最少连接是一样的思想,延迟感知追求极致的性能或者说用户体验,总是挑选能够最快的返回执行结果的机器来访问,但坏处是当都所有客户端都认为某台服务器最快时,那么所有请求都发送这台服务反而可能造成服务压力过大,性能降低。

源地址散列策略能够让同一客户端的请求或者同一用户的请求总是请求在后端同一台机器上,这种算法根据客户端IP求出Hash值然后对端集群总数求余得到值就是服务器集合的下标,一般这种算法用于缓存命中,或者同一会话请求等,但这种算法也有一定的缺点,某一用户访问量(黑产)非常高时可能造成服务端压力过大或者后端服务Down掉,那么客户端就会无法访问,所以也需要一定的降级策略。

一致性散列是在源地址散列的基础上发展得来的,什么意思呢?后端集群有是个3台机器(a,b,c),客户端经过散列对服务器总数取余后总是请求到a机器,那么当后端集群新增或者减少一台机器时,客户端散列后对服务器总数取余后就不再是原来的那台机器了,这样原来所有的请求散列后对应的后台机器都发生了变化,一致性散列就是解决这种问题的.

实现一个负载均衡算法

我们挑选上面一种策略用代码来实现一下,以便让大家更深入的理解,选择一个面试常问的策略,1、加权轮询算法,这个也比较多,Nginx中默认的算法

加权轮询算法每台服务器有三个权重:初始配置的权重,当前权重,有效权重,其中初始配置权重和有效权重是不变的,默认情况有效权重等于初始配置权重,当配置文件的初始配置权重改变时,会触发有效权重改变,只有当前权重是动态变化的。

每次请求到来时都从服务器列表中选择一个当前权重最高的,之后将选择出来的服务器当前权重减去所有服务器权重的和重新赋值给该服务器当前权重,这总算法通过不断递减当前权重使得所有服务器都有机会服务请求,比较平滑,代码实现如下

首先定义一个结构体,加权轮询算法的核心要素必须有服务器初始配置权重,当前权重(权重在实际运行时可能发生变化)

type SeverWeight struct {
   //配置的权重
   ConfigWeight int
   //当前权重
   CurrentWeight int
   //有效权重(值等于ConfigWeight,不过该字段是用一个配置属性,供前端修改使用)
   EffectiveWeight int
   //服务器ip
   Ip string
}
//加权轮询算法
type WeightedRoundRobin struct {
   //机器ip和对应的权重
   IpAndWeightedConfig map[string]int
   //服务器和权重信息
   SwSlice []*SeverWeight
}

根据配置信息创建负责均衡对象,初始化各个字段的值

//初始化加权轮询对象
func NewWeightedRoundRobin(iwc map[string]int) *WeightedRoundRobin {
   if iwc == nil {
      return nil
   }
   SwSlice := make([]*SeverWeight, 0)
   for k, v := range iwc {
      sw := &SeverWeight{ConfigWeight: v, CurrentWeight: 0,
                                 EffectiveWeight: v, Ip: k}
      SwSlice = append(SwSlice, sw)
   }
   return &WeightedRoundRobin{IpAndWeightedConfig: iwc, SwSlice: SwSlice}
}

这个方法是核心,调用这个方法来决定选择哪个服务器提供服务,方法的核心逻辑是选择当前权重最大的服务器提供服务,当前权重不断在变化,每次当前权重的值都等于当前值加上有效值减去所有服务器的有效权重和(这个算法就是不断递减当前服务器的当前权重值,使得按照均匀的变化让所有服务器都能提供服务)

func (wrr *WeightedRoundRobin) Select() (sw *SeverWeight) {
   total := 0 //统计所有服务器权重和
   for _, v := range wrr.SwSlice { //遍历服务器
      //当前权重加上有效权重
      v.CurrentWeight  = v.EffectiveWeight
      total  = v.EffectiveWeight
      //当配置值修改的时候的,有效权重循序渐进的增加
      if v.EffectiveWeight < v.ConfigWeight {
         v.EffectiveWeight  
      }
      //把权重最大的赋值给sw(sw是需要返回的对象)
      if sw == nil || v.CurrentWeight > sw.CurrentWeight {
         sw = v
      }
   }
   //当前返回对象的权重-所有服务器权重和
   sw.CurrentWeight = sw.CurrentWeight - total
   return sw
}

我们再来看一下执行的测试结果,根据测试结果相信大家就能够明白了,根据下面结果我们确实能够看到返回的服务器IP是均匀的,比较平滑,不会让权重低的服务器一直等待。

func TestNewWeightedRoundRobin(t *testing.T) {
   //服务器ip和权重配置 
   config :=map[string]int{"10.1": 7, "10.2": 2, "10.3": 1}
   wrr := NewWeightedRoundRobin(config)
   //发送10次请求
   for i := 0; i < 10; i   {
      sw := wrr.Select()
      t.Log(sw.Ip)//打印每次请求IP
   }
}
//结果:[10.1,10.1,10.2,10.1,10.1,10.3,10.1,10.1,10.2,10.1]

整个代码我已提交到github上,大家可以github上下载下来实际运行一下,加深理解,我得github地址如下:

https://github.com/sunpengwei1992/go_common/blob/master/algorithm/load_balance.go

任何一种算法深入研究后都能引出一堆问题来,都可以单独写一篇文章出来,本篇重点是在让大家知道这些算法,以至于见到后不会陌生,需要大家在工作中不断探索,不断升级自己的认知,提高思维能力。