kaiyun.ccm 分布式限流方案的探索与实践

发布于:25-02-15 播放次数:

随着微服务的普及,服务之间的依赖关系和通话关系变得越来越复杂,服务的稳定性变得尤为重要。即时的交通冲击通常参与业务方案,这可能会导致对超时的要求,甚至服务器也被压碎,停机时间不可用。为了保护系统本身以及上游和下游服务,我们通常会限制请求以快速拒绝超过配置限制的请求,从而确保系统或上游和下游服务系统的稳定性。合理的策略可以有效地处理交通冲击,并确保系统的可用性和性能。本文详细介绍了几种当前限制算法,比较了每种算法的优点和缺点,提供了一些选择当前限制算法的建议,并为业务中常用的分布式电流限制提供了一些解决方案。

1个背景

有三个主要的工具可以保护高并发服务的稳定性:缓存,降级和当前限制。

缓存:缓存是一种改善数据阅读性能的技术。通过将经常访问的数据存储在内存中,可以减少对数据库或其他存储系统的访问,从而提高系统的响应速度。缓存可以在多个级别上应用,例如浏览器缓存,CDN缓存,反向代理缓存,应用程序缓存等。

降级:降级是在系统承受太大压力或某些服务不可用以确保核心服务的正常操作时暂时关闭某些非核心服务。降级可以在多个级别上进行,例如页面降级,功能降低,服务降级等。

当前限制:当前限制是一种控制系统处理以防止系统超载的速率的技术。当前的限制可以通过各种算法实现,例如令牌存储算法,漏水存储算法等。

这三个“武器”具有自己的特征,通常可以组合使用以取得最佳效果。例如,可以通过缓存来减少数据库访问,可以通过降级来处理系统故障,并且可以通过当前限制来防止系统过载。在设计高并发系统时,这些技术需要根据系统的特定需求和特征进行合理使用。接下来,本文将主要介绍该行业中一些常用的当前限制方法。

2当前极限知识的概述

当前限制是限制请求或并发次数的关键技术手段,旨在确保系统的正常操作。当服务资源有限并且处理能力有限时,流量限制可能会限制上游请求以致电服务,以防止由于资源耗尽而停止服务。

在当前的限制中,有两个重要的概念需要理解:

阈值:指单元时间内允许的请求金额。例如,将QPS(每秒请求)限制为500,这意味着最多可在1秒内接受500个请求。通过设置适当的阈值,可以控制系统的负载,以避免导致系统崩溃或性能退化的过多请求。

拒绝政策:用于处理超过阈值的请求的政策。常见的拒绝策略包括直接拒绝,排队等待等。直接拒绝将立即拒绝超过阈值的请求,而排队等待将将请求放入队列并根据某些规则进行处理。选择正确的拒绝策略可以平衡系统的稳定性和用户体验。

通过合理设定阈值并选择适当的拒绝政策,当前的限制技术可以帮助系统应对突然的请求,恶意用户访问或过度请求频率,并确保系统的稳定性和可用性。根据当前限制范围,当前限制方案可以分为单机电流限制和分布式电流限制;其中,单机电流限制可以分为四种常见类型,包括固定的窗口,滑动窗口,漏水桶和代币的基于算法的当前限制。本文将详细介绍上述当前限制方案。

3基本电流限制算法3.1固定窗口电流限制3.1.1算法简介

固定的窗口算法是一种简单而直观的电流限制算法。它的原则是将时间分为固定尺寸的窗口,从而限制每个窗口中的请求的数量或速率。在特定的实现中,可以使用计数器记录当前窗口中的请求数,并将其与预设阈值进行比较。固定窗口算法的原理如下:

将时间分为固定尺寸的窗口,例如每秒一个窗口。

在每个窗口中,请记录请求的数量。

当请求到达时,请添加请求计数一个。

如果请求计数超过预设阈值(例如3个请求),则拒绝该请求。

窗口结束后,重置请求计数。

3.1.2代码实现

type FixedWindowLimiter struct {
   windowSize  time.Duration // 窗口大小
   maxRequests int           // 最大请求数
   requests    int           // 当前窗口内的请求数
   lastReset   int64         // 上次窗口重置时间(秒级时间戳)
   resetMutex  sync.Mutex    // 重置锁
}

func NewFixedWindowLimiter(windowSize time.Duration, maxRequests int) *FixedWindowLimiter {
   return &FixedWindowLimiter{
      windowSize:  windowSize,
      maxRequests: maxRequests,
      lastReset:   time.Now().Unix(),
   }
}

func (limiter *FixedWindowLimiter) AllowRequest() bool {
   limiter.resetMutex.Lock()
   defer limiter.resetMutex.Unlock()

   // 检查是否需要重置窗口
   if time.Now().Unix()-limiter.lastReset >= int64(limiter.windowSize.Seconds()) {
      limiter.requests = 0
      limiter.lastReset = time.Now().Unix()
   }

   // 检查请求数是否超过阈值
   if limiter.requests >= limiter.maxRequests {
      return false
   }

   limiter.requests++
   return true
}

func main() {
   limiter := NewFixedWindowLimiter(1*time.Second, 3// 每秒最多允许3个请求
   for i := 0; i < 15; i++ {
      now := time.Now().Format("15:04:05")
      if limiter.AllowRequest() {
         fmt.Println(now + " 请求通过")
      } else {
         fmt.Println(now + " 请求被限流")
      }
      time.Sleep(100 * time.Millisecond)
   }
}

执行结果:

lua unpack_lua unpack_lua unpack

3.1.3优点和缺点

优势:

简单实现:固定窗口算法的实现相对简单,易于理解和部署。

高稳定性:可以更好地限制和控制爆发请求,并且具有很高的稳定性。

易于实现的速率控制:固定的窗口算法可以轻松限制请求率,例如每秒允许多少请求。

缺点:

不均匀的请求分发:在固定的窗口算法中,窗口中请求的分布可能不均匀,从而导致某些窗口中的请求数超过阈值,而其他窗口中的请求较少。

无法应对破裂流量:固定窗口算法的窗口大小是固定的,并且不可能灵活地应对破裂的流量。

请求不平等:在窗口末尾重置请求计数可能会导致请求不平等。例如,在窗口结束之前的最后一秒钟内,请求计数已满,并且在窗口启动时的第一秒内,请求计数为零。

lua unpack_lua unpack_lua unpack

固定的窗口算法适用于明确要求请求率且流量相对稳定的方案,但是对于破裂流量和请求分配不均匀的情况,可能需要考虑其他更灵活的电流限制算法。

3.2滑动窗口电流限制3.2.1算法简介

上面已经解释说,当遇到时间窗口中的临界突变时,固定的窗口算法可能无法灵活地应对流量的变化。例如,在时间窗口结束时,如果突然出现了大量请求,则固定的窗口算法可能会导致请求被拒绝,即使下一个时间窗口中的请求不多。在这种情况下,我们需要一种更灵活的算法来处理突发流量和请求的不均匀分布 - 滑动窗口算法。该算法是对固定窗口算法的改进,该算法通过动态调整窗口的大小来更好地适应流量的变化。与固定的窗口算法不同,滑动窗口算法可以在遇到下一个时间窗口以更好地控制请求速率之前调整窗口的大小。该算法的原理如下:

窗口大小:确定固定的窗口大小,例如1秒。

请求计数:在窗口中,每次请求到达时,请求计数添加1。

限制:如果窗口中的请求计数超过设定阈值,即超过允许的最大请求数,则拒绝请求。

窗口滑动:随着时间的流逝,窗口将连续滑动,删除过期的请求计数,以使窗口中的请求数保持在限制内。

动态调整:在滑动窗口算法中,我们可以根据实际情况调整窗口的大小。在遇到下一个时间窗口之前,我们可以根据当前的流量情况调整窗口的大小,以适应流量的变化。

3.2.2代码实现

package main

import (
   "fmt"
   "sync"
   "time"
)

type SlidingWindowLimiter struct {
   windowSize   time.Duration // 窗口大小
   maxRequests  int           // 最大请求数
   requests     []time.Time   // 窗口内的请求时间
   requestsLock sync.Mutex    // 请求锁
}

func NewSlidingWindowLimiter(windowSize time.Duration, maxRequests int) *SlidingWindowLimiter {
   return &SlidingWindowLimiter{
      windowSize:  windowSize,
      maxRequests: maxRequests,
      requests:    make([]time.Time, 0),
   }
}

func (limiter *SlidingWindowLimiter) AllowRequest() bool {
   limiter.requestsLock.Lock()
   defer limiter.requestsLock.Unlock()

   // 移除过期的请求
   currentTime := time.Now()
   for len(limiter.requests) > 0 && currentTime.Sub(limiter.requests[0]) > limiter.windowSize {
      limiter.requests = limiter.requests[1:]
   }

   // 检查请求数是否超过阈值
   if len(limiter.requests) >= limiter.maxRequests {
      return false
   }

   limiter.requests = append(limiter.requests, currentTime)
   return true
}

func main() {
   limiter := NewSlidingWindowLimiter(500*time.Millisecond, 2// 每秒最多允许4个请求
   for i := 0; i < 15; i++ {
      now := time.Now().Format("15:04:05")
      if limiter.AllowRequest() {
         fmt.Println(now + " 请求通过")
      } else {
         fmt.Println(now + " 请求被限流")
      }
      time.Sleep(100 * time.Millisecond)
   }
}

执行结果:

lua unpack_lua unpack_lua unpack

3.2.3优点和缺点

优势:

灵活性:滑动窗口算法可以根据实际条件动态调整窗口的大小,以适应流量变化。这种灵活性使算法可以更好地应对爆发流量和请求的不均匀分布。

实时:由于滑动窗口算法在每个时间窗口的末尾执行窗口滑动云开·全站体育app登录,因此它可以更及时地响应流量的变化,并提供更实时的当前限制效果。

精度:与固定窗口算法相比,滑动窗口算法具有较小的粒度,并且可以提供更准确的电流限制控制。

缺点:

内存消耗:滑动窗口算法需要维持窗口中的请求时间列表,并且列表的长度随时间而增加。这可能会导致大量的内存消耗,尤其是当窗口大小较大或请求频率更高时。

算法复杂性:与简单的固定窗口算法相比开yun体育app官网网页登录入口,滑动窗口算法的实现更为复杂。它需要逻辑,例如窗口滑动,请求计数和删除过期的请求,这可能需要更多的代码和计算开销。

lua unpack_lua unpack_lua unpack

从代码的角度来看,我们可以发现滑动窗口算法实际上是一种固定的窗口算法,具有较小的粒度。它可以在一定程度上提高当前限制的准确性和真实性,并且无法从根本上解决不均匀请求分布的问题。该算法受窗口的大小和时间间隔的限制,尤其是在极端情况下,例如爆发流量过多或请求分布极为不平衡,这仍然可能导致当前限制不准确。因此,在实际应用中,应使用更复杂的算法或策略来进一步优化当前的限制效果。

3.3泄漏的水桶当前限制3.3.1算法简介

尽管“滑动窗口算法”可以提供一定的当前限制效果,但它仍然受窗口的大小和时间间隔的限制。在某些情况下,破裂流量可能导致窗口内的请求数超过限制。为了更好地平滑所需的流量,可以将泄漏的水桶电流限制算法用作滑动窗口算法的改进。该算法的原理很简单:它保持了固定容量的泄漏桶,要求以不确定的速率流入泄漏桶中,并且泄漏桶以固定的速度流出。如果请求到达时泄漏的存储桶已满,则会触发拒绝策略。该算法可以从生产者消费者模式中理解,并且该请求是生产者。每个请求就像一滴水。当请求到达时,将其放置在队列中(泄漏存储桶)。泄漏的水桶充当消费者,以固定的速度从队列中消费请求,就像水滴不断从水桶底部的孔中泄漏一样。消费率等于当前极限阈值,例如,每秒处理2个请求,即500毫秒中消费一个请求。泄漏的水桶的容量就像队列的容量一样。当请求累积超过指定能力时,否定拒绝策略会触发,即新到达的请求将被丢弃或处理。该算法实现如下:

泄漏存储桶容量:确定固定的泄漏桶容量,表明泄漏存储桶可以存储的最大请求数。

泄漏桶率:确定固定的泄漏液率,表明泄漏桶可以每秒处理的请求数量。

请求处理:当请求到达时,生产者将请求放入泄漏的存储桶中。

泄漏存储桶:泄漏存储桶以固定的速率从泄漏存储桶中消耗了请求,并处理这些请求。如果泄漏的存储桶中有请求,将处理请求;如果泄漏的存储桶为空kaiyun.ccm,则不会处理请求。

请求丢弃或延迟:如果泄漏的存储桶已满,也就是说,泄漏的存储桶中的请求数已达到容量限制,并且新到达的请求将被丢弃或延迟。

lua unpack_lua unpack_lua unpack

3.3.2代码实现

package main

import (
   "fmt"
   "time"
)

type LeakyBucket struct {
   rate       float64 // 漏桶速率,单位请求数/秒
   capacity   int     // 漏桶容量,最多可存储请求数
   water      int     // 当前水量,表示当前漏桶中的请求数
   lastLeakMs int64   // 上次漏水的时间戳,单位秒
}

func NewLeakyBucket(rate float64, capacity int) *LeakyBucket {
   return &LeakyBucket{
      rate:       rate,
      capacity:   capacity,
      water:      0,
      lastLeakMs: time.Now().Unix(),
   }
}

func (lb *LeakyBucket) Allow() bool {
   now := time.Now().Unix()
   elapsed := now - lb.lastLeakMs

   // 漏水,根据时间间隔计算漏掉的水量
   leakAmount := int(float64(elapsed) / 1000 * lb.rate)
   if leakAmount > 0 {
      if leakAmount > lb.water {
         lb.water = 0
      } else {
         lb.water -= leakAmount
      }
   }

   // 判断当前水量是否超过容量
   if lb.water > lb.capacity {
      lb.water-- // 如果超过容量,减去刚刚增加的水量
      return false
   }

   // 增加水量
   lb.water++

   lb.lastLeakMs = now
   return true
}

func main() {
   // 创建一个漏桶,速率为每秒处理3个请求,容量为4个请求
   leakyBucket := NewLeakyBucket(34)

   // 模拟请求
   for i := 1; i <= 15; i++ {
      now := time.Now().Format("15:04:05")
      if leakyBucket.Allow() {
         fmt.Printf(now+"  第 %d 个请求通过\n", i)
      } else {
         fmt.Printf(now+"  第 %d 个请求被限流\n", i)
      }
      time.Sleep(200 * time.Millisecond) // 模拟请求间隔
   }
}

执行结果:

lua unpack_lua unpack_lua unpack

3.3.3优点和缺点

优势:

光滑的流量:泄漏桶算法可以平滑爆发流动,从而使输出流动更稳定,从而避免了系统的突然增加。

简单而易于实现:泄漏桶算法的原理简单且相对易于实现。

有效防止超负荷:通过控制流出流,泄漏的存储算法可以有效防止系统过载。

缺点:

爆发流量的处理不够灵活:虽然漏水算法可以使破裂的流量平滑,但在某些情况下,我们可能希望能够迅速处理破裂的流量。在这种情况下,泄漏桶算法可能不够灵活。

流速无法动态调整:漏水算法的流出率是固定的,并且不能根据系统的实际情况进行动态调整。

这可能导致流量浪费:如果输入流量小于泄漏存储桶的流出速率,则将浪费泄漏存储桶的流出速率。

如果输入流量继续大于漏水存储桶的流出率,则漏水的存储桶将保持满足并丢弃新请求,这可能会导致服务质量的退化。

3.4令牌桶当前限制3.4.1算法简介

令牌存储算法是实现当前限制的一个共同想法,该算法用于限制请求率。它确保系统仍然可以在高负载条件下提供稳定的服务,并防止破裂流量使系统超载。最常用的Google Java开发工具包,Guava当前的限制工具类Ratelimiter是代币存储桶的实现。令牌桶算法基于一个令牌存储桶的概念,在该概念中,以固定速率生成令牌并将其放置在桶中。每个令牌代表请求的许可。当请求到达时,需要从令牌桶中获得令牌。如果令牌存储桶中的令牌不足,则该请求将受到限制或丢弃。令牌存储算法的实现步骤如下:

初始化一个令牌存储桶,包括存储桶的容量和代币产生的速率。

继续以固定的速率生成令牌,并将其放入令牌桶中,直到桶满为止。

当请求到达时,请尝试从令牌存储桶中获得令牌。

如果令牌存储桶中有足够的令牌,请求通过,将一个令牌从令牌存储桶中删除。

如果令牌存储桶中的令牌不足,则该请求将受到限制或丢弃。

lua unpack_lua unpack_lua unpack

3.4.2代码实现

package main

import (
   "fmt"
   "sync"
   "time"
)

// TokenBucket 表示一个令牌桶。
type TokenBucket struct {
   rate       float64    // 令牌添加到桶中的速率。
   capacity   float64    // 桶的最大容量。
   tokens     float64    // 当前桶中的令牌数量。
   lastUpdate time.Time  // 上次更新令牌数量的时间。
   mu         sync.Mutex // 互斥锁,确保线程安全。
}

// NewTokenBucket 创建一个新的令牌桶,给定令牌添加速率和桶的容量。
func NewTokenBucket(rate float64, capacity float64) *TokenBucket {
   return &TokenBucket{
      rate:       rate,
      capacity:   capacity,
      tokens:     capacity, // 初始时,桶是满的。
      lastUpdate: time.Now(),
   }
}

// Allow 检查是否可以从桶中移除一个令牌。如果可以,它移除一个令牌并返回 true。
// 如果不可以,它返回 false。
func (tb *TokenBucket) Allow() bool {
   tb.mu.Lock()
   defer tb.mu.Unlock()

   // 根据经过的时间计算要添加的令牌数量。
   now := time.Now()
   elapsed := now.Sub(tb.lastUpdate).Seconds() 
   tokensToAdd := elapsed * tb.rate            

   tb.tokens += tokensToAdd
   if tb.tokens > tb.capacity {
      tb.tokens = tb.capacity // 确保令牌数量不超过桶的容量。
   }

   if tb.tokens >= 1.0 {
      tb.tokens--
      tb.lastUpdate = now
      return true
   }

   return false
}

func main() {
   tokenBucket := NewTokenBucket(2.03.0)

   for i := 1; i <= 10; i++ {
      now := time.Now().Format("15:04:05")
      if tokenBucket.Allow() {
         fmt.Printf(now+"  第 %d 个请求通过\n", i)
      } else { // 如果不能移除一个令牌,请求被拒绝。
         fmt.Printf(now+"  第 %d 个请求被限流\n", i)
      }
      time.Sleep(200 * time.Millisecond)
   }
}

执行结果:

lua unpack_lua unpack_lua unpack

3.4.3优点和缺点

优势:

光滑的流量:令牌桶算法可以平滑爆发流,因此爆发流在一段时间内均匀分布,避免了系统对系统的突然峰值的影响。

灵活性:令牌桶算法可以通过调整令牌的生成速率和存储桶大小来灵活地控制流量。

允许爆发流量:由于令牌存储桶可以积累一定数量的令牌,如果存储桶中有足够的令牌,那么当流量突然增加时,可以处理此爆发流量。

缺点:

实施综合体:与当前一些当前的限制算法(例如漏水算法)相比,令牌桶算法的实现稍复杂,需要维护代币的产生和消耗。

需要准确的时间控制:令牌桶算法需要根据时间生成令牌,因此需要精确的时间控制。如果系统的时间控制不准确,则可能会影响当前限制的效果。

可能会有资源浪费:如果系统的流量继续低于代币产生的速率,则存储桶中的令牌可能会连续积累,从而导致浪费资源。

3.5四种基本算法的比较

lua unpack_lua unpack_lua unpack

4个分布式电流极限

独立的当前限制是指单个服务器的情况,该服务器通过限制单个服务器在单位时间内处理的请求数来防止服务器过载。上面已经介绍了当前的限制算法,它们的优势是简单的实施,高效率和明显的结果。随着微服务体系结构的普及,系统服务通常部署在多个服务器上。目前,需要分布式当前限制以确保整个系统的稳定性。接下来,本文将介绍几种常见的分布式当前限制技术解决方案:

4.1基于集中化的当前限制方案4.1.1解决方案原理

通过集中式当前限制器控制所有服务器请求。实施方法:

选择一个集中式组件,例如 - 雷迪斯。

定义当前限制规则,例如:您可以设置每秒允许的最大请求数(QPS),并将此值存储在REDIS中。

对于每个请求,服务器都需要首先从Redis请求令牌。

如果获得令牌,则意味着可以处理请求;如果未获得令牌,则意味着该请求在流中受到限制,并且可以返回错误消息或以后再试。

4.1.2代码实现

package main

import (
   "context"
   "fmt"
   "go.uber.org/atomic"
   "sync"

   "git.code.oa.com/pcg-csd/trpc-ext/redis"
)

type RedisClient interface {
   Do(ctx context.Context, cmd string, args ...interface{}) (interface{}, error)
}

// Client 数据库
type Client struct {
   client RedisClient // redis 操作
   script string      // lua脚本
}

// NewBucketClient 创建redis令牌桶
func NewBucketClient(redis RedisClient) *Client {
   helper := redis
   return &Client{
      client: helper,
      script`
         -- 令牌桶限流脚本
         -- KEYS[1]: 桶的名称
         -- ARGV[1]: 桶的容量
         -- ARGV[2]: 令牌产生速率
         
         local bucket = KEYS[1]
         local capacity = tonumber(ARGV[1])
         local tokenRate = tonumber(ARGV[2])
         
         local redisTime = redis.call('TIME')
         local now = tonumber(redisTime[1])
         
         local tokens, lastRefill = unpack(redis.call('hmget', bucket, 'tokens', 'lastRefill'))
         tokens = tonumber(tokens)
         lastRefill = tonumber(lastRefill)
         
         if not tokens or not lastRefill then
            tokens = capacity
            lastRefill = now
         else
            local intervalsSinceLast = (now - lastRefill) * tokenRate
            tokens = math.min(capacity, tokens + intervalsSinceLast)
         end
         
         if tokens < 1 then
            return 0
         else
            redis.call('hmset', bucket, 'tokens', tokens - 1, 'lastRefill', now)
            return 1
         end
      `
,
   }
}

// 获取令牌,获取成功则立即返回true,否则返回false
func (c *Client) isAllowed(ctx context.Context, key string, capacity int64, tokenRate int64) (bool, error) {
   result, err := redis.Int(c.client.Do(ctx, "eval", c.script, 1, key, capacity, tokenRate))
   if err != nil {
      fmt.Println("Redis 执行错误:", err)
      return false, err
   }
   return result == 1, nil
}

// 调用检测
func main() {
   c := NewBucketClient(redis.GetPoolByName("redis://127.0.0.1:6379"))
   gw := sync.WaitGroup{}
   gw.Add(120)
   count := atomic.Int64{}
   for i := 0; i < 120; i++ {
      go func(i int) {
         defer gw.Done()
         status, err := c.isAllowed(context.Background(), "test"10010)
         if status {
            count.Add(1)
         }
         fmt.Printf("go %d status:%v error: %v\n", i, status, err)
      }(i)
   }
   gw.Wait()
   fmt.Printf("allow %d\n\n", count.Load())
}

执行结果:

lua unpack_lua unpack_lua unpack

4.1.3现有问题

性能瓶颈:由于所有请求都需要通过REDIS,REDIS可能会成为整个系统的性能瓶颈。要解决此问题,请考虑使用Redis簇以提高性能或使用更高的性能硬件。

单点故障:如果REDIS失败,则整个系统的当前限制功能将受到影响。要解决此问题,请考虑使用Redis的主奴隶复制或前哨模式来实现高可用性。

网络带宽:REDIS是基于网络通信的内存数据库,因此网络带宽是其性能的关键因素。如果网络带宽受到限制,则要求的传输速度可能会减慢,这将影响REDIS的性能。

lua unpack_lua unpack_lua unpack

REDIS官方绩效测试图

4.2基于负载平衡的分布式电流限制方案4.2.1解决方案原理

可以看出,集中式电流限制方案具有单点故障的高风险,并且带宽瓶颈相对严重。在此基础上,本文设计了一种基于本地缓存独立电流限制和负载平衡的新分布式电流限制解决方案。具体计划如下:

使用负载平衡器或分布式服务发现(由Polaris完成)将请求平均分发给每台计算机。这样可以确保每台机器可以处理请求的一部分。

维护每台计算机上机器的当前限制状态,以实现本地缓存的单机电流限制的逻辑。令牌桶电流限制算法用于在每台机器上独立执行当前限制控制。每秒处理的请求数,令牌存储桶中的令牌数量等。根据当前限制状态,当前限制判决是在到达请求时做出的。

准备相应的动态调整计划,并且可以根据每台机器的实际负载条件动态调整当前限制参数。例如,如果机器的CPU或内存使用率太高,则可以降低机器的当前极限阈值并减少机器的请求处理量。相反,如果机器的资源利用率较低,请增加该机器的当前极限阈值,并增加该机器的请求处理量。

4.2.2现有问题

本地缓存:该解决方案对本地缓存有很高的要求,您需要选择风险点,例如基于业务逻辑的本地缓存和缓存容量限制的淘汰策略。

当前限制精度:本地高速缓存独立流的准确性受每个服务实例的资源和配置的限制。这可能会导致当前的限制策略无法准确适应整个系统的流量变化,也无法灵活地调整当前限制规则。

单点要求负载平衡器。

动态缩放的适应性:当系统需要动态缩放或缩放时,该解决方案可能需要其他配置和调整,以确保新连接或删除的服务实例可以正确参与当前限制和请求余额。

4.3基于分布式协调服务的当前限制4.3.1解决方案原理

使用分布式协调服务,例如Zookeeper或ETCD,以实现当前限制。每个服务器将从分布式协调服务中申请令牌,并且只能处理获得令牌的请求。基本计划:

初始化令牌存储桶:在Zookeeper中创建一个节点,该节点的数据表示令牌的数量。最初,将数据设置为令牌存储桶的容量。

申请令牌:当请求到达时,服务器首先申请了Zookeeper的令牌。这可以通过获取节点的分布式锁定,然后将节点的数据减少1来实现。如果操作成功,则意味着已应用令牌并可以处理请求;如果操作失败,则意味着令牌已被用完,需要拒绝或等待请求。

发布令牌:处理请求后,服务器需要向Zookeeper发布令牌。这可以通过获取节点的分布式锁,然后将节点的数据添加1来实现。

补充令牌:您可以设置一个定时任务,以在Zookeeper中定期补充令牌令牌。可以根据系统的负载条件动态调整补品的频率和数量。

4.3.2现有问题

该解决方案的优点是它可以达到精确的全局电流局限性并避免单点故障。但是,该解决方案的缺点是它在实施中很复杂,并且对Zookeeper的性能有很高的要求。如果Zookeeper无法处理大量令牌请求和发布操作,则可能会成为系统中的瓶颈。

5摘要

简而言之,没有最好的解决方案,只有合适的解决方案。选择合适的当前限制解决方案时,我们需要考虑各种因素,包括系统要求,现有技术堆栈,系统负载条件以及基础系统的性能。了解每个解决方案的工作原理和功能,以便您可以在实际应用中做出最佳选择。

良好的当前局限性设计必须考虑业务的特征和需求,还具有以下六个:

多层电流限制:除了当前的主要和备份复制的限制服务外,还可以考虑多级当前限制策略。例如,可以在应用程序层,服务层和数据层设置当前限制,这可以更好地防止系统过载。

动态阈值调整:我们可以根据系统的实时负载情况动态调整当前限制策略。例如,当系统负载较低时,我们可以放松当前的限制策略。当系统负载较高时,我们可以收紧当前的限制策略。

灵活的维度:当前的限制策略应能够根据不同的业务场景进行调整。除了接口,设备,IP,帐户ID和其他维度外,我们还可以考虑更细粒度的电流限制。例如,我们可以根据用户的行为模式限制当前,这可以更好地防止恶意用户的攻击。

解耦:当前限制应用作基本服务,并与特定的业务逻辑分开。这样,当业务逻辑更改时,无需修改当前限制服务的代码,而只需调整当前的限制策略。

容忍度:当前的限制服务应高度可用,但是如果出现问题,企业应该有替代方案(断路,降级)。这可能包括使用替代流限制服务,或决定是根据业务敏感性发布请求。

监视和警报:应对当前限制策略进行实时监控,并设置警报机制。当触发当前的限制策略时,可以立即收到警报,以便我们可以及时处理问题。

当前限制是确保系统稳定有效运行的重要手段,但这不是唯一的解决方案。我们还需要考虑其他系统设计和优化方法,例如负载平衡,缓存,异步处理等(面对爆炸量,扩展始终是最好的方法,除了昂贵!)。只有彼此合作,我们才能构建一个系统,不仅可以处理高度并发请求,还可以确保服务质量。

lua unpack_lua unpack_lua unpack