热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Go:自适应负载均衡算法原理和实现

负载均衡是很重要

背景

在选择负载均衡算法时,我们希望满足以下要求:

  1. 具备分区和机房调度亲和性
    • 每次选择的节点尽量是负载最低的
    • 每次尽可能选择响应最快的节点
  2. 无需人工干预故障节点
    • 当一个节点有故障时,负载均衡算法可以自动隔离该节点
    • 当故障节点恢复时,能够自动恢复对该节点的流量分发

基于这些考虑,go-zero
选择了 p2c+EWMA
算法来实现。

算法的核心思想

p2c

p2c (Pick Of 2 Choices)
二选一: 在多个节点中随机选择两个节点。

go-zero
中的会随机的选择3次,如果其中一次选择的节点的健康条件满足要求,就中断选择,采用这两个节点。

EWMA

EWMA (Exponentially Weighted Moving-Average)
指数移动加权平均法: 是指各数值的加权系数随时间呈指数递减,越靠近当前时刻的数值加权系数就越大,体现了最近一段时间内的平均值。

  • 公式:

    EWMA公式
  • 变量解释:

    • Vt
      : 代表的是第 t
      次请求的 EWMA值
    • Vt-1
      : 代表的是第 t-1
      次请求的 EWMA值
    • β
      : 是一个常量

EWMA 算法的优势

  1. 相较于普通的计算平均值算法,EWMA
    不需要保存过去所有的数值,计算量显著减少,同时也减小了存储资源。
  2. 传统的计算平均值算法对网络耗时不敏感, 而 EWMA
    可以通过请求频繁来调节 β
    ,进而迅速监控到网络毛刺或更多的体现整体平均值。
    • 当请求较为频繁时, 说明节点网络负载升高了, 我们想监测到此时节点处理请求的耗时(侧面反映了节点的负载情况), 我们就相应的调小β
      β
      越小,EWMA值
      就越接近本次耗时,进而迅速监测到网络毛刺;
    • 当请求较为不频繁时, 我们就相对的调大β值
      。这样计算出来的 EWMA值
      越接近平均值

β计算

go-zero
采用的是牛顿冷却定律中的衰减函数模型计算 EWMA
算法中的 β
值:

牛顿冷却定律中的衰减函数

其中 Δt
为两次请求的间隔,e
k
为常数

gRPC 中实现自定义负载均衡器

  1. 首先我们需要实现 google.golang.org/grpc/balancer/base/base.go/PickerBuilder
    接口, 这个接口是有服务节点更新的时候会调用接口里的Build
    方法

    type PickerBuilder interface {
        // Build returns a picker that will be used by gRPC to pick a SubConn.
        Build(info PickerBuildInfo) balancer.Picker
    }

  2. 还要实现 google.golang.org/grpc/balancer/balancer.go/Picker
    接口。这个接口主要实现负载均衡,挑选一个节点供请求使用

    type Picker interface {
      Pick(info PickInfo) (PickResult, error)
    }

  3. 最后向负载均衡 map
    中注册我们实现的负载均衡器

go-zero 实现负载均衡的主要逻辑

  1. 在每次节点更新,gRPC
    会调用 Build
    方法,此时在 Build
    里实现保存所有的节点信息。
  2. gRPC
    在获取节点处理请求时,会调用 Pick
    方法以获取节点。go-zero
    Pick
    方法里实现了 p2c
    算法,挑选节点,并通过节点的 EWMA值
    计算负载情况,返回负载低的节点供 gRPC
    使用。
  3. 在请求结束的时候 gRPC
    会调用 PickResult.Done
    方法,go-zero
    在这个方法里实现了本次请求耗时等信息的存储,并计算出了 EWMA值
    保存了起来,供下次请求时计算负载等情况的使用。

负载均衡代码分析

  1. 保存服务的所有节点信息

    我们需要保存节点处理本次请求的耗时、EWMA
    等信息,go-zero
    给每个节点设计了如下结构:

    type subConn struct {
        addr     resolver.Address
        conn     balancer.SubConn
        lag      uint64 // 用来保存 ewma 值
        inflight int64  // 用在保存当前节点正在处理的请求总数
        success  uint64 // 用来标识一段时间内此连接的健康状态
        requests int64  // 用来保存请求总数
        last     int64  // 用来保存上一次请求耗时, 用于计算 ewma 值
        pick     int64  // 保存上一次被选中的时间点
    }

  2. p2cPicker
    实现了 balancer.Picker
    接口,conns
    保存了服务的所有节点信息

    type p2cPicker struct {
      conns []*subConn  // 保存所有节点的信息 
      r     *rand.Rand
      stamp *syncx.AtomicDuration
      lock  sync.Mutex
    }

  3. gRPC
    在节点有更新的时候会调用 Build
    方法,传入所有节点信息,我们在这里把每个节点信息用 subConn
    结构保存起来。并归并到一起用 p2cPicker
    结构保存起来

    func (b *p2cPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
      ......
      var conns []*subConn
      for conn, connInfo := range readySCs {
        conns = append(conns, &subConn{
          addr:    connInfo.Address,
          conn:    conn,
          success: initSuccess,
        })
      }
      return &p2cPicker{
        conns: conns,
        r:     rand.New(rand.NewSource(time.Now().UnixNano())),
        stamp: syncx.NewAtomicDuration(),
      }
    }

  4. 随机挑选节点信息,在这里分了三种情况:

    主要实现代码如下:

    switch len(p.conns) {
      case 0: // 没有节点,返回错误
        return emptyPickResult, balancer.ErrNoSubConnAvailable
      case 1: // 有一个节点,直接返回这个节点
        chosen = p.choose(p.conns[0], nil)
      case 2: // 有两个节点,计算负载,返回负载低的节点
        chosen = p.choose(p.conns[0], p.conns[1])
      default: // 有多个节点,p2c 挑选两个节点,比较这两个节点的负载,返回负载低的节点
        var node1, node2 *subConn
        // 3次随机选择两个节点
        for i := 0; i < pickTimes; i++ {
          a := p.r.Intn(len(p.conns))
          b := p.r.Intn(len(p.conns) - 1)
          if b >= a {
            b++
          }
          node1 = p.conns[a]
          node2 = p.conns[b]
          // 如果这次选择的节点达到了健康要求, 就中断选择
          if node1.healthy() && node2.healthy() {
            break
          }
        }
        // 比较两个节点的负载情况,选择负载低的
        chosen = p.choose(node1, node2)
      }

    1. 只有一个服务节点,此时直接返回供 gRPC
      使用即可
    2. 有两个服务节点,通过 EWMA值
      计算负载,并返回负载低的节点返回供 gRPC
      使用
    3. 有多个服务节点,此时通过 p2c
      算法选出两个节点,比较负载情况,返回负载低的节点供 gRPC
      使用
  5. load
    计算节点的负载情况

    上面的 choose
    方法会调用 load
    方法来计算节点负载。

    计算负载的公式是: load = ewma * inflight

    在这里简单解释下:ewma
    相当于平均请求耗时,inflight
    是当前节点正在处理请求的数量,相乘大致计算出了当前节点的网络负载。

    func (c *subConn) load() int64 {
      // 通过 EWMA 计算节点的负载情况; 加 1 是为了避免为 0 的情况
      lag := int64(math.Sqrt(float64(atomic.LoadUint64(&c.lag) + 1)))
      load := lag * (atomic.LoadInt64(&c.inflight) + 1)
      if load == 0 {
        return penalty
      }
      return load
    }

  6. 请求结束,更新节点的 EWMA
    等信息

    func (p *p2cPicker) buildDoneFunc(c *subConn) func(info balancer.DoneInfo) {
      start := int64(timex.Now())
      return func(info balancer.DoneInfo) {
        // 正在处理的请求数减 1
        atomic.AddInt64(&c.inflight, -1)
        now := timex.Now()
        // 保存本次请求结束时的时间点,并取出上次请求时的时间点
        last := atomic.SwapInt64(&c.last, int64(now))
        td := int64(now) - last
        if td < 0 {
          td = 0
        }
        // 用牛顿冷却定律中的衰减函数模型计算EWMA算法中的β值
        w := math.Exp(float64(-td) / float64(decayTime))
        // 保存本次请求的耗时
        lag := int64(now) - start
        if lag < 0 {
          lag = 0
        }
        olag := atomic.LoadUint64(&c.lag)
        if olag == 0 {
          w = 0
        }
        // 计算 EWMA 值
        atomic.StoreUint64(&c.lag, uint64(float64(olag)*w+float64(lag)*(1-w)))
        success := initSuccess
        if info.Err != nil && !codes.Acceptable(info.Err) {
          success = 0
        }
        osucc := atomic.LoadUint64(&c.success)
        atomic.StoreUint64(&c.success, uint64(float64(osucc)*w+float64(success)*(1-w)))

        stamp := p.stamp.Load()
        if now-stamp >= logInterval {
          if p.stamp.CompareAndSwap(stamp, now) {
            p.logStats()
          }
        }
      }
    }

    1. 把节点正在处理请求的总数减1
    2. 保存处理请求结束的时间点,用于计算距离上次节点处理请求的差值,并算出 EWMA
      中的 β值
    3. 计算本次请求耗时,并计算出 EWMA值
      保存到节点的 lag
      属性里
    4. 计算节点的健康状态保存到节点的 success
      属性中



往期推荐
  • 新书推荐:用 Gin 框架构建分布式应用


我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。


坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio



推荐阅读
  • 本文介绍了在Python中使用gRPC的方法示例,分享给大家,具体如下:使用ProtocolBuffers的跨平台RPC系统。安装使用pi ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • Python瓦片图下载、合并、绘图、标记的代码示例
    本文提供了Python瓦片图下载、合并、绘图、标记的代码示例,包括下载代码、多线程下载、图像处理等功能。通过参考geoserver,使用PIL、cv2、numpy、gdal、osr等库实现了瓦片图的下载、合并、绘图和标记功能。代码示例详细介绍了各个功能的实现方法,供读者参考使用。 ... [详细]
  • WebSocket与Socket.io的理解
    WebSocketprotocol是HTML5一种新的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • JavaWeb中读取文件资源的路径问题及解决方法
    在JavaWeb开发中,读取文件资源的路径是一个常见的问题。本文介绍了使用绝对路径和相对路径两种方法来解决这个问题,并给出了相应的代码示例。同时,还讨论了使用绝对路径的优缺点,以及如何正确使用相对路径来读取文件。通过本文的学习,读者可以掌握在JavaWeb中正确找到和读取文件资源的方法。 ... [详细]
  • 微信官方授权及获取OpenId的方法,服务器通过SpringBoot实现
    主要步骤:前端获取到code(wx.login),传入服务器服务器通过参数AppID和AppSecret访问官方接口,获取到OpenId ... [详细]
  • 微服务之总体架构篇
    一、单体架构存在的问题缺点:1、难以维护:当单体应用业务不断迭代后代码量非常臃肿,模整个项目非常复杂,每次更改代码都可能带来新的bug;2、部署项目麻烦:庞大之后项目部署效率 ... [详细]
  • golang反射,golang反射性能
    本文目录一览:1、关于反射2、尝试用golan ... [详细]
  • k8s入坑之路(14)scheduler调度 kubelet管理及健康检查
    kubelet主要功能Pod管理在kubernetes的设计中,最基本的管理单位是pod,而不是container。pod是kubernetes在容器上的一层封装,由一组运行在同一 ... [详细]
  • ASP.NET CORE 简介
    ASP.NETCore是一个跨平台的高性能开源框架,用于生成启用云且连接Internet的新式应用。使用ASP.NETCore,您可以:生成Web ... [详细]
  • 本文介绍了机器学习手册中关于日期和时区操作的重要性以及其在实际应用中的作用。文章以一个故事为背景,描述了学童们面对老先生的教导时的反应,以及上官如在这个过程中的表现。同时,文章也提到了顾慎为对上官如的恨意以及他们之间的矛盾源于早年的结局。最后,文章强调了日期和时区操作在机器学习中的重要性,并指出了其在实际应用中的作用和意义。 ... [详细]
  • 本文讨论了如何使用IF函数从基于有限输入列表的有限输出列表中获取输出,并提出了是否有更快/更有效的执行代码的方法。作者希望了解是否有办法缩短代码,并从自我开发的角度来看是否有更好的方法。提供的代码可以按原样工作,但作者想知道是否有更好的方法来执行这样的任务。 ... [详细]
author-avatar
mobiledu2502855757
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有