拒绝宕机!一文详解分布式限流方案(附代码实现) 您所在的位置:网站首页 java接口限流怎么实现的 拒绝宕机!一文详解分布式限流方案(附代码实现)

拒绝宕机!一文详解分布式限流方案(附代码实现)

2024-05-21 06:47| 来源: 网络整理| 查看: 265

👉导读

随着微服务的流行,服务之间的依赖性和调用关系变得越来越复杂,服务的稳定性变得尤为重要。业务场景中经常会涉及到瞬时流量冲击,可能会导致请求响应超时,甚至服务器被压垮、宕机不可用。出于对系统本身和上下游服务的保护,我们通常会对请求进行限流处理,快速拒绝超出配置上限的请求,保证系统或上下游服务系统的稳定。合理策略能有效应对流量冲击,确保系统可用性和性能。本文详细介绍了几种限流算法,比较各个算法的优缺点,给出了限流算法选型的一些建议,同时对业务上常用的分布式限流也提出一些解决方案。

一个好的限流设计必须要考虑到业务的特性和需求,同时具备以下六点:

多级限流:除了主备复制的限流服务,可以考虑实现多级限流策略。例如,可以在应用层、服务层和数据层都设置限流,这样可以更好地防止系统过载。动态阈值调整:我们可以根据系统的实时负载情况动态调整限流策略。例如,当系统负载较低时,我们可以放宽限流策略;当系统负载较高时,我们可以收紧限流策略。灵活维度:限流策略应该能够根据不同的业务场景进行调整。除了接口,设备,ip,账户 id 等维度外,我们还可以考虑更细粒度的限流。例如,我们可以根据用户的行为模式进行限流,这样可以更好地防止恶意用户的攻击。解耦性:限流应该作为一个基础服务,与具体的业务逻辑分离。这样,当业务逻辑发生变化时,不需要修改限流服务的代码,只需要调整限流策略即可。容错性:限流服务应该具有高可用性,但是如果出现问题,业务应该有备选方案(熔断、降级)。这可能包括使用备用的限流服务,或者根据业务的敏感性决定是否放行请求。监控和报警:对限流策略应进行实时监控,并设置报警机制。当限流策略触发时,可立即收到报警,以便我们可以及时地处理问题。01、背景

保护高并发服务稳定主要有三把利器:缓存、降级和限流。

缓存:缓存是一种提高数据读取性能的技术,通过在内存中存储经常访问的数据,可以减少对数据库或者其他存储系统的访问,从而提高系统的响应速度。缓存可以应用在多个层次,例如浏览器缓存、CDN 缓存、反向代理缓存、应用缓存等。降级:降级是在系统压力过大或者部分服务不可用的情况下,暂时关闭一些非核心的服务,以保证核心服务的正常运行。降级可以在多个层次进行,例如页面降级、功能降级、服务降级等。限流:限流是一种控制系统处理请求的速率的技术,以防止系统过载。限流可以通过多种算法实现,例如令牌桶算法、漏桶算法等。

这三把“利器”各有其特点,通常会结合使用,以达到最佳的效果。例如,可以通过缓存来减少数据库的访问,通过降级来应对系统故障,通过限流来防止系统过载。在设计高并发系统时,需要根据系统的具体需求和特点,合理地使用这些技术。接下来本文会主要介绍一些业界常用的限流方法。

02、限流知识概述

限流是一种对请求或并发数进行限制的关键技术手段,旨在保障系统的正常运行。当服务资源有限、处理能力有限时,限流可以对调用服务的上游请求进行限制,以防止自身服务因资源耗尽而停止服务。

在限流中,有两个重要的概念需要了解:

阈值:指在单位时间内允许的请求量。例如,将 QPS(每秒请求数)限制为500,表示在1秒内最多接受500次请求。通过设置合适的阈值,可以控制系统的负载,避免过多的请求导致系统崩溃或性能下降。拒绝策略:用于处理超过阈值的请求的策略。常见的拒绝策略包括直接拒绝、排队等待等。直接拒绝会立即拒绝超过阈值的请求,而排队等待则将请求放入队列中,按照一定的规则进行处理。选择合适的拒绝策略可以平衡系统的稳定性和用户体验。

通过合理设置阈值和选择适当的拒绝策略,限流技术可以帮助系统应对突发的请求量激增、恶意用户访问或请求频率过高的情况,保障系统的稳定性和可用性。限流方案根据限流范围,可分为 单机限流和分布式限流;其中单机限流依据算法,又可分为 固定窗口、滑动窗口、漏桶和令牌桶限流等常见四种。本文将对上述限流方案进行详细介绍。

03、限流基本算法

3.1 固定窗口限流

3.1.1 算法介绍

固定窗口算法是一种简单直观的限流算法,其原理是将时间划分为固定大小的窗口,在每个窗口内限制请求的数量或速率。具体实现时,可以使用一个计数器来记录当前窗口内的请求数,并与预设的阈值进行比较。固定窗口算法的原理如下:

将时间划分固定大小窗口,例如每秒一个窗口。在每个窗口内,记录请求的数量。当有请求到达时,将请求计数加一。如果请求计数超过了预设的阈值(比如3个请求),拒绝该请求。窗口结束后,重置请求计数。

3.1.2 代码实现

代码语言:javascript复制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) } }

执行结果:

3.1.3 优缺点

优点:

实现简单:固定窗口算法的实现相对简单,易于理解和部署。稳定性较高:对于突发请求能够较好地限制和控制,稳定性较高。易于实现速率控制:固定窗口算法可以很容易地限制请求的速率,例如每秒最多允许多少个请求。

缺点:

请求分布不均匀:固定窗口算法中,窗口内的请求分布可能不均匀,导致某些窗口内的请求数量超过阈值,而其他窗口内的请求较少。无法应对突发流量:固定窗口算法的窗口大小是固定的,无法灵活地应对突发流量。请求的不公平性:窗口结束时的请求计数重置可能导致请求的不公平性。例如,在窗口结束前的最后一秒内,请求计数已满,而在窗口开始时的第一秒内,请求计数为零。

固定窗口算法适用于对请求速率有明确要求且流量相对稳定的场景,但对于突发流量和请求分布不均匀的情况,可能需要考虑其他更灵活的限流算法。

3.2 滑动窗口限流

3.2.1 算法介绍

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

窗口大小:确定一个固定的窗口大小,例如1秒。请求计数:在窗口内,每次有请求到达时,将请求计数加1。限制条件:如果窗口内的请求计数超过了设定的阈值,即超过了允许的最大请求数,就拒绝该请求。窗口滑动:随着时间的推移,窗口会不断滑动,移除过期的请求计数,以保持窗口内的请求数在限制范围内。动态调整:在滑动窗口算法中,我们可以根据实际情况调整窗口的大小。当遇到下一个时间窗口之前,我们可以根据当前的流量情况来调整窗口的大小,以适应流量的变化。

3.2.2 代码实现

代码语言:javascript复制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) } }

执行结果:

3.2.3 优缺点

优点:

灵活性:滑动窗口算法可以根据实际情况动态调整窗口的大小,以适应流量的变化。这种灵活性使得算法能够更好地应对突发流量和请求分布不均匀的情况。实时性:由于滑动窗口算法在每个时间窗口结束时都会进行窗口滑动,它能够更及时地响应流量的变化,提供更实时的限流效果。精度:相比于固定窗口算法,滑动窗口算法的颗粒度更小,可以提供更精确的限流控制。

缺点:

内存消耗:滑动窗口算法需要维护一个窗口内的请求时间列表,随着时间的推移,列表的长度会增长。这可能会导致较大的内存消耗,特别是在窗口大小较大或请求频率较高的情况下。算法复杂性:相比于简单的固定窗口算法,滑动窗口算法的实现较为复杂。它需要处理窗口滑动、请求计数和过期请求的移除等逻辑,可能需要更多的代码和计算开销。

从代码的角度可以发现,滑动窗口算法实际上是颗粒度更小的固定窗口算法,它可以在一定程度上提高限流的精度和实时性,并不能从根本上解决请求分布不均匀的问题。算法受限于窗口的大小和时间间隔,特别是在极端情况下,如突发流量过大或请求分布极不均匀的情况下,仍然可能导致限流不准确。因此,在实际应用中,要采用更复杂的算法或策略来进一步优化限流效果。

3.3 漏桶限流

3.3.1 算法介绍

尽管滑动窗口算法可以提供一定的限流效果,但它仍然受限于窗口的大小和时间间隔。在某些情况下,突发流量可能会导致窗口内的请求数超过限制。为了更好地平滑请求的流量,漏桶限流算法可以作为滑动窗口算法的改进。算法的原理很简单:它维护一个固定容量的漏桶,请求以不定的速率流入漏桶,而漏桶以固定的速率流出。如果请求到达时,漏桶已满,则会触发拒绝策略。可以从生产者-消费者模式去理解这个算法,请求充当生产者,每个请求就像一滴水,当请求到达时,它被放入一个队列(漏桶)中。而漏桶则充当消费者,以固定的速率从队列中消费请求,就像从桶底的孔洞中不断漏出水滴。消费的速率等于限流阈值,例如每秒处理2个请求,即500毫秒消费一个请求。漏桶的容量就像队列的容量,当请求堆积超过指定容量时,会触发拒绝策略,即新到达的请求将被丢弃或延迟处理。算法的实现如下:

漏桶容量:确定一个固定的漏桶容量,表示漏桶可以存储的最大请求数。漏桶速率:确定一个固定的漏桶速率,表示漏桶每秒可以处理的请求数。请求处理:当请求到达时,生产者将请求放入漏桶中。漏桶流出:漏桶以固定的速率从漏桶中消费请求,并处理这些请求。如果漏桶中有请求,则处理一个请求;如果漏桶为空,则不处理请求。请求丢弃或延迟:如果漏桶已满,即漏桶中的请求数达到了容量上限,新到达的请求将被丢弃或延迟处理。

3.3.2 代码实现

代码语言:javascript复制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(3, 4) // 模拟请求 for i := 1; i 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.0, 3.0) for i := 1; i


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有