1. 介绍

Sentinel-Go 是一个流量操控库,专心于供给优秀的流量操控解决方案,而底层完成和规划考虑一般是非常有价值的内容。

令牌桶和漏桶算法是流量操控中常用的两种算法,它们能够被用来平滑流量、约束恳求速率等。冷启动算法在体系启动时能够协助体系快速到达安稳状况,这些算法在流量操控中都有着重要的作用。

经过深入探讨 Sentinel-Go 的底层完成和规划考虑,读者能够更好地了解流量操控背面的原理和机制。同时,工程实践方面的经验共享也能够协助读者更好地运用流量操控解决方案到实践的业务场景中。

2. 流控规矩

在正式开始前先了解下 Sentinel-Go 的流控规矩的具体参数,字段意义运用注释标示 00000000000000000

type Rule struct {
   // 资源名,即规矩的作用方针。
   Resource               string                 `json:"resource"`
   // 当时流量操控器的Token核算战略。Direct表明直接运用Threshold 作为阈值;
   // WarmUp表明运用预热方法核算Token的阈值
   // MemoryAdaptive表明运用内存自适应方法核算Token的阈值
   TokenCalculateStrategy TokenCalculateStrategy `json:"tokenCalculateStrategy"`
   // 操控行为,Reject表明直接回绝,Throttling表明匀速排队
   ControlBehavior        ControlBehavior        `json:"controlBehavior"`
   // 表明流控阈值;假如字段 StatIntervalInMs 是1000(也便是1秒),
   // 那么Threshold就表明QPS,流量操控器也就会根据资源的QPS来做流控。
   Threshold        float64          `json:"threshold"`
   // 调用联系限流战略,CurrentResource表明运用当时规矩的resource做流控;
   // AssociatedResource表明运用相关的resource做流控,相关的resource在字段 RefResource 定义;
   RelationStrategy RelationStrategy `json:"relationStrategy"`
   // 相关的resource
   RefResource      string           `json:"refResource"`
   // 匀速排队的最大等候时刻,该字段仅仅对操控行为是匀速排队时收效
   MaxQueueingTimeMs uint32 `json:"maxQueueingTimeMs"`
   // 预热的时刻长度,该字段仅仅对Token核算战略是WarmUp时收效;
   WarmUpPeriodSec   uint32 `json:"warmUpPeriodSec"`
   // 预热的因子,默许是3,该值的设置会影响预热的速度,该字段仅仅对Token核算战略是WarmUp时收效
   WarmUpColdFactor  uint32 `json:"warmUpColdFactor"`
   // 规矩对应的流量操控器的独立核算结构的核算周期。假如StatIntervalInMs是1000,也便是核算QPS。
   StatIntervalInMs uint32 `json:"statIntervalInMs"`
   // 内存低运用率时的限流阈值,该字段仅在Token核算战略是MemoryAdaptive时收效
   LowMemUsageThreshold  int64 `json:"lowMemUsageThreshold"`
   // 内存高运用率时的限流阈值,该字段仅在Token核算战略是MemoryAdaptive时收效
   HighMemUsageThreshold int64 `json:"highMemUsageThreshold"`
   // 内存低水位符号字节巨细,该字段仅在Token核算战略是MemoryAdaptive时收效
   MemLowWaterMarkBytes  int64 `json:"memLowWaterMarkBytes"`
   // 内存高水位符号字节巨细,该字段仅在Token核算战略是MemoryAdaptive时收效
   MemHighWaterMarkBytes int64 `json:"memHighWaterMarkBytes"`
}

3. 流量操控

流量操控是一种用于办理和调节体系、服务或网络中流动的数据量的技术手段。经过流量操控,能够约束数据的传输速率、保证体系资源的安稳性和高可用性,防止体系因瞬时的流量高峰而产生毛病或崩溃。在 Sentinel-Go 中,流量操控的实质是经过监控资源的核算目标,根据 Token 核算战略来核算资源的可用 Token,进而根据流量操控战略对流量进行调控,以到达保证体系安稳性和高可用性的目的。

具体来说,在 Sentinel-Go 中,流量操控的完成流程一般如下:

  1. 监控资源的核算目标:经过监控资源的运用情况、恳求频率等目标,对体系的流量进行实时监控和核算。
  2. 根据 Token 核算战略核算可用 Token:根据事先设定的 Token 核算战略,核算资源当时可用的 Token 数量,即资源的阈值。
  3. 流量操控战略:根据设定的流量操控战略,比方令牌桶算法、漏桶算法等,对流量进行操控,约束恳求速率或流量巨细,保证体系在承受范围内运转。
  4. 体系保证与高可用性:经过流量操控,防止体系被突发的高流量冲垮,保证体系的高可用性和安稳性。

经过对资源的实时监控、Token 核算和流量操控战略的灵活运用,Sentinel-Go 能够供给强壮的流量操控才能,保证体系在各种情况下的安稳运转。这种流量操控机制关于繁忙的网络环境和复杂的体系架构非常重要,能够有效地防止体系崩溃和服务不可用的情况产生。

3.1 流量操控战略

流量操控战略由规矩中的TokenCalculateStrategy (Token核算战略)和ControlBehavior (操控行为)两个字段一起决议。两个字段能够按实践场景按需组合运用,不同的组合对应的流量操控效果是不同的。

go-sentinel流量操控(三): 流量操控的完成原理

在Sentinel-Go中将Token核算战略和操控行为笼统为两个interface,在初始化时会根据流控规矩创立对应的流量操控器,其中流量操控器中包含了下面两个接口的完成以及核算结构。

  • TrafficShapingCalculator:核算流量操控的实践阈值(Token)
  • TrafficShapingChecker:根据实践阈值与核算结构中的目标进行流量操控

Sentinel-Go供给三种Token核算战略以及两种操控流量的行为

Token核算战略

  • 固定的限流阈值: 运用流控规矩中的限流阈值,该方法是默许的流量操控方法,当QPS超越任意规矩的阈值后,新的恳求就会被当即回绝,这种方法适用于对体系处理才能确切已知的情况下,比方经过压测确认了体系的准确水位时
  • WarmUp(预热) :该方法首要用于体系长期处于低水位的情况下,当流量突然添加时,直接把体系拉升到高水位或许瞬间把体系压垮。经过”冷启动”,让经过的流量缓慢添加,在必定时刻内逐步添加到阈值上限,给冷体系一个预热的时刻,防止冷体系被压垮的情况
  • 内存自适应:该方法首要用于维护体系内存不会跟着流量的增加而无限增加,在内存的安全边界内动态调整限流阈值,尽或许的提升吞吐。

流控行为

  • 回绝:当流量超越阈值时后边的流量将直接回绝。这种行为简略粗犷能够很好的操控体系的负载,可是有损被回绝掉的流量不会被服务处理。
  • 匀速排队:当QPS超越阈值时后边的流量将会依照固定时刻依次排队经过,起到削峰填谷的效果。此行为是利用漏桶算法完成,合适用于距离性流量突发的场景,例如:消息队列。

3.2 Token核算战略

3.2.1 Direct(Thresold)

当流控规矩中TokenCalculateStrategy设置为Direct,则代表运用规矩中的Thresold当作阈值进行流控。 在Direct战略下Token不需求核算,运用规矩中装备的固定值即可。

func (d *DirectTrafficShapingCalculator) CalculateAllowedTokens(uint32, int32) float64 {
   return d.threshold
}

3.2.2 WarmUp(预热)

介绍

冷启动(TokenCalculateStrategy设置为WarmUp)在流控规矩中的运用,是为了解决体系长期处于低水位的情况下,当流量突然添加时或许导致体系压力骤增的问题。经过冷启动,体系能够以一个缓慢增加的速度逐步调整并提高限流阈值Token,给体系一个预热的时刻,防止冷体系在瞬间被压垮。

一般情况下,冷启动的进程能够经过一个 QPS 曲线来描述。在冷启动阶段,体系初始的经过率(QPS)会从一个较低的水平逐步上升,直至到达设定的限流阈值上限。这种逐步增加的进程能够有效地缓解体系在高流量条件下的冲击,使体系能够平稳过渡到高水位,并逐步适应新的流量需求。这种方法能够协助体系在面临流量激增时愈加安稳和可靠地运转,保证体系的高可用性和安稳性。

一般冷启动的进程体系允许经过的 QPS 曲线如下图所示:

go-sentinel流量操控(三): 流量操控的完成原理

原理

在Sentinel-Go中的WarmUp完成参考了Guava的SmoothRateLimited,实质是一个令牌桶算法。

依照令牌桶算法对应到Sentinel-Go中,担任出产令牌的出产者便是WarmUp的Token核算战略,担任消费令牌的消费者则是行为操控

go-sentinel流量操控(三): 流量操控的完成原理

在Sentinel-Go的WarmUp的令牌生成算法模型如下:

(这个图要从右向左进行了解,实质是要根据预热时刻、限流阈值等参数推导出令牌生成的时刻距离(y轴),然后能够知道每秒能生成多少个令牌,即可到达动态生成限流阈值的效果)

go-sentinel流量操控(三): 流量操控的完成原理

  • x轴:令牌桶容量
  • y轴:每个令牌生成的时刻距离
  • warningToken:令牌预警数量,即令牌桶中的剩下令牌数量到达预警值时,预热结束。
  • maxToken:令牌桶最大容量,当令牌桶到达容量后,生成的令牌将被丢掉
  • slope:斜率,用来核算当时令牌生成的时刻距离
  • warmUpPeriodSec:体系冷启动的时刻,单位秒

假定规矩如下:阈值设置为100,冷启动时刻为10S,冷却因子默许为3,滑动时刻窗口核算周期为1000毫秒

惯例的令牌桶算法:有一个担任出产令牌的出产者以及担任获取令牌的消费者,当令牌桶满的时分出产者出产的令牌直接丢掉,不然将令牌存储到令牌桶中。当有流量经过时令牌消费者先从令牌桶中获取令牌,假如获取成功则放行当时流量,假如令牌桶中没有令牌则将当时流量回绝。

{
   TokenCalculateStrategy: flow.WarmUp,
   ControlBehavior:        flow.Reject,
   Threshold:              100,
   WarmUpPeriodSec:        10,
   WarmUpColdFactor:       3,
   StatIntervalInMs:       1000,
}

在初始化WarmUp核算战略func中,会核算对应的warningToken=500, maxToken=1000, slope=0.00004

warningToken := uint64((float64(rule.WarmUpPeriodSec) * rule.Threshold) / float64(rule.WarmUpColdFactor-1))
maxToken := warningToken + uint64(2*float64(rule.WarmUpPeriodSec)*rule.Threshold/float64(1.0+rule.WarmUpColdFactor))
slope := float64(rule.WarmUpColdFactor-1.0) / rule.Threshold / float64(maxToken-warningToken)

初始化后,对应参数如下:

go-sentinel流量操控(三): 流量操控的完成原理

接下来重点看下核算Token的算法:

func (c *WarmUpTrafficShapingCalculator) CalculateAllowedTokens(_ uint32, _ int32) float64 {
   // 获取滑动时刻窗口前一个核算周期的QPS,依托于底层的核算结构
   metricReadonlyStat := c.BoundOwner().boundStat.readOnlyMetric
   previousQps := metricReadonlyStat.GetPreviousQPS(base.MetricEventPass)
   // 同步令牌桶中的令牌(包含生成和丢掉)
   c.syncToken(previousQps)
   // 原子获取令牌桶中的令牌数
   restToken := atomic.LoadInt64(&c.storedTokens)
   if restToken < 0 {
      restToken = 0
   }
   // 假如桶中令牌数>=令牌预警线(500),代表还在冷启动阶段
   if restToken >= int64(c.warningToken) {
      // 核算桶中令牌和预警线的差值(也便是还有多少个令牌可用)
      aboveToken := restToken - int64(c.warningToken)
      // 动态核算出每秒允许经过的QPS阈值
      warningQps := math.Nextafter(1.0/(float64(aboveToken)*c.slope+1.0/c.threshold), math.MaxFloat64)
      return warningQps
   } else {
   // 假如桶中令牌数<令牌预警线,则阐明冷启动现已结束,直接返回规矩中的阈值
      return c.threshold
   }
}

假定令牌桶中有1000(restToken)个令牌,slope=0.00004,warningToken=500, 此时在预热期间。那么aboveToken=500。

核算每秒经过的QPS阈值,首先看这段公式的:经过斜率算出当时token的生成时刻距离0.03毫秒(也便是求出X轴=1000时,Y轴的的数值=0.03)

(float64(aboveToken)*c.slope+1.0/c.threshold=500*0.00004+1/100=0.03

然后经过当时token生成时刻距离核算出每秒可经过的QPS(token阈值)=33.33

math.Nextafter(1.0/(float64(aboveToken)*c.slope+1.0/c.threshold), math.MaxFloat64)

在syncToken中首要是更新令牌桶中令牌的数量:

func (c *WarmUpTrafficShapingCalculator) syncToken(passQps float64) {
   // 获取当时时刻(毫秒)
   currentTime := util.CurrentTimeMillis()
   // 获取当时时刻(秒)
   currentTime = currentTime - currentTime%1000
   // 终究填充令牌桶时刻
   oldLastFillTime := atomic.LoadUint64(&c.lastFilledTime)
   // 假如当时时刻小于终究填充时刻,阐明呈现了时刻回拨,则不需求填充/丢掉令牌
   // 假如当时时刻等于终究填充时刻,阐明在同一秒内现已同步过令牌桶了,防止重复填充/丢掉令牌
   if currentTime <= oldLastFillTime {
      return
   }
   // 获取当时桶中的令牌数量
   oldValue := atomic.LoadInt64(&c.storedTokens)
   // 初始化/生成令牌
   newValue := c.coolDownTokens(currentTime, passQps)
   // 利用cas存储最新的令牌数量,防止并发不安全问题。
   if atomic.CompareAndSwapInt64(&c.storedTokens, oldValue, newValue) {
      // 终究桶中令牌数=桶中令牌数-现已经过的QPS
      if currentValue := atomic.AddInt64(&c.storedTokens, int64(-passQps)); currentValue < 0 {
         atomic.StoreInt64(&c.storedTokens, 0)
      }
      // 更新终究更新令牌桶的时刻
      atomic.StoreUint64(&c.lastFilledTime, currentTime)
   }
}

在coolDownTokens中初始化令牌桶以及填充令牌:

func (c *WarmUpTrafficShapingCalculator) coolDownTokens(currentTime uint64, passQps float64) int64 {
   oldValue := atomic.LoadInt64(&c.storedTokens)
   newValue := oldValue
   // 假如令牌桶中的令牌数量小于令牌预警线
   // 初始化时桶中令牌=0必定小于warningToken
   // 预热结束后,令牌桶中的数量也会小于预警线
   if oldValue < int64(c.warningToken) {
     // 填充令牌=桶中令牌数+每秒应该生成的令牌数100
      newValue = int64(float64(oldValue) + (float64(currentTime)-float64(atomic.LoadUint64(&c.lastFilledTime)))*c.threshold/1000.0)
   } else if oldValue > int64(c.warningToken) {
   // 假如令牌数量大于预警线,阐明应该在预热期间 
   // 可是假如经过的恳求数(消费的令牌数)小于冷却数量,阐明并没有真实的开始预热
   // 则需求填充令牌,让桶中令牌维持在maxToken
      if passQps < float64(uint32(c.threshold)/c.coldFactor) {
         newValue = int64(float64(oldValue) + float64(currentTime-atomic.LoadUint64(&c.lastFilledTime))*c.threshold/1000.0)
      }
   }
   // 当时生成的令牌小于最大令牌数
   if newValue <= int64(c.maxToken) {
      return newValue
   } else {
   // 假如但前令牌大雨最大令牌,则丢掉剩余令牌,让桶中一直最多拥有maxToken
      return int64(c.maxToken)
   }
}

总结

在Sentinel-Go中的WarmUp模型实质是令牌桶算法,在初始化时将令牌桶中”填满”,预热期间扣减令牌,用剩下的令牌与斜率推导出每秒能够扣减的令牌数量,然后动态得出预热期间的限流阈值。

3.2.3 内存自适应

介绍

当流控规矩中TokenCalculateStrategy设置为MemoryAdaptive,则代表运用内存自适应方法核算Token(限流阈值)。流量巨细会间接影响体系内存运用率,当体系内存运用率过大时必定程度上也会影响吞吐量与恳求耗时,在内存自适应中能够根据体系的内存运用情况动态的核算限流阈值,然后让体系愈加平稳的运转

原理

在Sentinel-Go中运用内存自适应流控的规矩如下:(需求指定低和高水位内存的字节数以及内存低和高水位时对应的限流阈值)

{
   Resource:               resName,
   TokenCalculateStrategy: flow.MemoryAdaptive,
   ControlBehavior:        flow.Reject,
   StatIntervalInMs:       1000,
   LowMemUsageThreshold:   1000, // 低水位限流阈值
   HighMemUsageThreshold:  100,  // 高水位限流阈值
   MemLowWaterMarkBytes:  1024,  // 低水位内存字节
   MemHighWaterMarkBytes: 2048,  // 高水位内存字节
},

实质上Sentinel-Go将内存划分为三个区域,分别是内存高水位区域,低水位区域以及阈值动态动摇区域。当内存运用在高水位或低水位区域时则运用对应的水位阈值,假如内存在阈值的动摇区域(高水位字节减低水位字节),那么则需求动态核算流控阈值。

go-sentinel流量操控(三): 流量操控的完成原理

Token核算逻辑也相对简略,只需求动态的获取内存已运用的字节(运用工具库gopsutil),然后去对比当时内存运用的字节在哪一个区域内,假如在动态的动摇区域内则简略的数学公式核算出对应的动态流控阈值

	(float64(m.highMemUsageThreshold-m.lowMemUsageThreshold)/float64(m.memHighWaterMark-m.memLowWaterMark))*float64(mem-m.memLowWaterMark) + float64(m.lowMemUsageThreshold)

总结

内存自适应算法中提早将内存的最低以及最高水位对应的流控阈值规划好,然后根据内存运用情况在“安全的”范围内去动态操控流控阈值

3.3 操控行为(流控行为

3.3.1 直接回绝

介绍

当流控规矩中ControlBehavior设置为Reject,则代表运用“回绝”的流控行为,当恳求流量大于限流阈值时则将超出阈值的恳求直接返回。

go-sentinel流量操控(三): 流量操控的完成原理

原理

在回绝的操控行为中完成相对简略,首要是依托于上文介绍的滑动时刻轮核算结构,有了核算结构能够很轻松的获取到已经过的恳求数,然后与限流阈值进行比对。

3.3.2 匀速排队

介绍

当流控规矩中ControlBehavior设置为Throttling,则代表运用“匀速排队”(也叫匀速器)的流控行为。与直接回绝的操控行为不同的是当恳求流量大于限流阈值时则将超出阈值的恳求依照必定的距离时刻匀速经过,对应的是漏桶算法。

go-sentinel流量操控(三): 流量操控的完成原理

原理

在Sentinel-Go中匀速排队的流控行为,实质上便是一个漏桶。漏桶的特色:流入速率不固定,可是流出速率是固定的。当时流量+已经过的流量大于阈值时将当时流量放入漏桶中,然后在漏桶里对流量依照固定的时刻距离排队经过。

排队的进程如下:

go-sentinel流量操控(三): 流量操控的完成原理

匀速排队的完成代码,在注释中讲解了细节的完成:

func (c *ThrottlingChecker) DoCheck(_ base.StatNode, batchCount uint32, threshold float64) *base.TokenResult {
   // 获取当时时刻(单位纳秒,便利更准确的核算排队时刻)
   curNano := int64(util.CurrentTimeNano())
   // 核算每个流量排队的时刻距离,这儿假定当时流量(batchCount=1),流控阈值(threshold=100),核算周期:单位纳秒(statIntervalNs=1000000000)
   intervalNs := int64(math.Ceil(float64(batchCount) / threshold * float64(c.statIntervalNs)))
   // 终究一个流量排队经过的时刻
   loadedLastPassedTime := atomic.LoadInt64(&c.lastPassedTime)
   // 当时恳求估计的经过时刻=终究一个流量排队经过的时刻+排队的时刻距离
   expectedTime := loadedLastPassedTime + intervalNs
   // 假如估计经过的时刻小于当时时刻,则阐明不需求排队,直接放行。
   if expectedTime <= curNano {
      if swapped := atomic.CompareAndSwapInt64(&c.lastPassedTime, loadedLastPassedTime, curNano); swapped {
         // nil means pass
         return nil
      }
   }
   // 预估排队等候的时长=当时恳求估计的经过时刻-当时时刻
   estimatedQueueingDuration := atomic.LoadInt64(&c.lastPassedTime) + intervalNs - curNano
   // 假如预估排队时长大于设定的最大等候时长,则直接被回绝掉当时流量
   if estimatedQueueingDuration > c.maxQueueingTimeNs {
      return base.NewTokenResultBlockedWithCause(base.BlockTypeFlow, BlockMsgQueueing, rule, nil)
   }
   // 原子操作:得到当时流量的经过时刻(这儿首要防止并发导致lastPassedTime不是最新的)
   oldTime := atomic.AddInt64(&c.lastPassedTime, intervalNs)
   // 预估排队等候的时长
   estimatedQueueingDuration = oldTime - curNano
   if estimatedQueueingDuration > c.maxQueueingTimeNs {
      // 假如大于了设定的最大等候时长,这儿减去排队距离,因为不需求排队了,直接回绝当时流量。
      atomic.AddInt64(&c.lastPassedTime, -intervalNs)
      return base.NewTokenResultBlockedWithCause(base.BlockTypeFlow, BlockMsgQueueing, rule, nil)
   }
   // 假如预估排队等候的时长大于0,则依照排队等候的时长进行sleep
   if estimatedQueueingDuration > 0 {
      return base.NewTokenResultShouldWait(time.Duration(estimatedQueueingDuration))
   } else {
      return base.NewTokenResultShouldWait(0)
   }
    }

总结

在匀速排队的操控行为中将超越阈值的流量,依照固定的时刻距离依次的排队等候。 等候的时刻是根据前一个流量的排队经过时刻+排队的时刻距离核算出来的,等候的动作是sleep。符合漏桶的特色,依照固定的速率放行流量。

4. 基于调用联系的流量操控

Sentinel-Go 支持相关流量操控战略。当两个资源之间具有资源争抢或许依赖联系的时分,这两个资源便具有了相关。比方对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。假如听任读写操作争抢资源,则争抢自身带来的开支会降低整体的吞吐量。可运用相关限流来防止具有相相关系的资源之间过度的争抢。

当流控规矩中RelationStrategy=AssociatedResource时则代表运用基于调用联系的流量操控

{
   Resource:               resName,
   TokenCalculateStrategy: flow.Direct,
   ControlBehavior:        flow.Reject,
   RelationStrategy:       flow.AssociatedResource,
   RefResource:            "db-write",
   Threshold:              10,
   StatIntervalInMs:       1000,
}

这种方法的 Token 核算战略和流控行为的完成方法都和上面介绍的相同,只不过在初始化Token核算战略和流控行为时绑定的是相关资源的核算结构。这样在核算阈值以及获取当时QPS等目标都是获取的相关资源的数据,那么就能够根据相关资源的目标数据进行流控了。

5. 总结

Sentinel-Go 在流量操控中供给了多种流量操控场景。完成的方法是经过组合Token核算战略和流控行为,并基于底层的滑动时刻窗口核算数据结构来进行流量操控(其中最有特色的是冷启动+匀速排队的组合方法,相当于令牌桶+漏桶的组合)本文经过对流量操控的完成细节进行详解,相信大家在实践运用中能够更好的选择对应的组合方法。

go-sentinel流量操控(三): 流量操控的完成原理