单机和分布式场景下-有哪些流控方案 (单机分布式一体化)

教程大全 2025-07-08 14:00:09 浏览

单机和分布式场景下,有哪些流控方案?

2020-06-24 11:35:42不同的场景下所需的流控算法不尽相同,那应该如何选择适用的流控方案呢?本文分享单机及分布式流控场景下,简单窗口、滑动窗口、漏桶、令牌桶、滑动日志等几种流控算法的思路和代码实现,并总结了各自的复杂度和适用场景。较长,同学们可收藏后再看。

不同的场景下所需的流控算法不尽相同,那应该如何选择适用的流控方案呢?本文分享单机及分布式流控场景下,简单窗口、滑动窗口、漏桶、令牌桶、滑动日志等几种流控算法的思路和代码实现,并总结了各自的复杂度和适用场景。较长,同学们可收藏后再看。

一 流控的场景

流控的意义其实无需多言了。最常用的场景下,流控是为了保护下游有限的资源不被流量冲垮,保证服务的可用性,一般允许流控的阈值有一定的弹性,偶尔的超量访问是可以接受的。

有的时候,流控服务于收费模式,比如某些云厂商会对调用 API 的频次进行计费。既然涉及到钱,一般就不允许有超出阈值的调用量。

这些不同的场景下,适用的流控算法不尽相同。大多数情况下,使用 Sentinel 中间件已经能很好地应对,但 Sentinel 也并不是万能的,需要思考其他的流控方案。

二 接口定义

为了方便,以下所有的示例代码实现都是基于 Throttler 接口。

Throttler 接口定义了一个通用的方法用于申请单个配额。

当然你也可以定义一个 tryAcquire(String key, int permits) 签名的方法用于一次申请多个配额,实现的思路是一样的。

有些流控算法需要为每个 key 维护一个 Throttler 实例。

public interface Throttler { /** * 尝试申请一个配额 * * @param key申请配额的key * @return 申请成功则返回true,否则返回false */ boolean tryAcquire(String key);  }

三 单机流控

1 简单窗口

简单窗口是我自己的命名,有些地方也叫做固定窗口,主要是为了跟后面的滑动窗口区分。

流控是为了限制指定时间间隔内能够允许的访问量,因此,最直观的思路就是基于一个给定的时间窗口,维护一个计数器用于统计访问次数,然后实现以下规则:

代码实现 SimpleWindowThrottler

/** * 毫秒为单位的时间窗口 */  private final long windowInMs;  /** * 时间窗口内最大允许的阈值 */  private final int threshold;  /** * 最后一次成功请求时间 */  private long lastReqTime = System.currentTimeMillis();  /** * 计数器 */  private long counter;  public boolean tryAcquire(String key) { long now = System.currentTimeMillis(); // 如果当前时间已经超过了上一次访问时间开始的时间窗口,重置计数器,以当前时间作为新窗口的起始值 if (now - lastReqTime > windowInMs) {#1 counter = 0; lastReqTime = now;#2 } if (counter < threshold) {#3 counter++;#4 return true; } else { return false; }  }

另外一种常见的场景是根据不同的 key 来做流控,每个 key 有单独的时间窗口、阈值配置,因此需要为每个 key 维护一个单独的限流器实例。

切换到多线程环境

在现实应用中,往往是多个线程来同时申请配额,为了比较简洁地表达算法思路,示例代码里面都没有做并发同步控制。

以简单窗口的实现为例,要转换为多线程安全的流控算法,一种直接的办法是将 tryAcquire 方法设置为 synchronized。

当然一种感觉上更高效的办法也可以是修改读写变量的类型:

private volatile long lastReqTime = System.currentTimeMillis();  private LongAdder counter = new LongAdder();

不过这样其实并不真正“安全”,设想以下的场景,两个线程 A、线程 B 前后脚尝试获取配额,#1 位置的判断条件满足后,会同时走到 #2 位置修改 lastReqTime 值,线程 B 的赋值会覆盖线程 A,导致时间窗口起始点向后偏移。同样的,位置 #3 和 #4 也会构成竞争条件。当然如果对流控的精度要求不高,这种竞争也是能接受的。

临界突变问题

简单窗口的流控实现非常简单,以 1 分钟允许 100 次访问为例,如果流量均匀保持 200 次/分钟的访问速率,系统的访问量曲线大概是这样的(按分钟清零):

但如果流量并不均匀,假设在时间窗口开始时刻 0:00 有几次零星的访问,一直到 0:50 时刻,开始以 10 次/秒的速度请求,就会出现这样的访问量图线:

在临界的 20 秒内(0:50~1:10)系统承受的实际访问量是 200 次,换句话说,最坏的情况下,在窗口临界点附近系统会承受 2 倍的流量冲击,这就是简单窗口不能解决的临界突变问题。

2 滑动窗口

如何解决简单窗口算法的临界突变问题?既然一个窗口统计的精度低,那么可以把整个大的时间窗口切分成更细粒度的子窗口,每个子窗口独立统计。同时,每过一个子窗口大小的时间,就向右滑动一个子窗口。这就是滑动窗口算法的思路。

如上图所示,将一分钟的时间窗口切分成 6 个子窗口,每个子窗口维护一个独立的计数器用于统计 10 秒内的访问量,每经过 10s,时间窗口向右滑动一格。

回到简单窗口出现临界跳变的例子,结合上面的图再看滑动窗口如何消除临界突变。如果 0:50 到 1:00 时刻(对应灰色的格子)进来了 100 次请求,接下来 1:00~1:10 的 100 次请求会落到黄色的格子中,由于算法统计的是 6 个子窗口的访问量总和,这时候总和超过设定的阈值 100,就会拒绝后面的这 100 次请求。

代码实现(参考 Sentinel)

Sentinel 提供了一个轻量高性能的滑动窗口流控算法实现,看代码的时候可以重点关注这几个类:

1)功能插槽 StatisticSlot 负责记录、统计不同纬度的 runtime 指标监控信息,例如 RT、QPS 等。

Sentinel 内部使用了 slot chain 的责任链设计模式,每个功能插槽 slot 有不同的功能(限流、降级、系统保护),通过 ProcessorSlotChain 串联在一起。

参考官方 Wiki:工作主流程

2)StatisticSlot 使用 StatisticNode#addPassRequest 记录允许的请求数,包含秒和分钟两个维度。

3)具体记录用到的是 Metric 接口,对应实现类 ArrayMetric,背后真正的滑动窗口数据结构是LeapArray 。

4)LeapArray 内部维护了滑动窗口用到的关键属性和结构,包括:

a)总窗口大小 intervalInMs,滑动子窗口大小 windowLengthInMs,采样数量sampleCount:

sampleCount = intervalInMs / windowLengthInMs

当前实现默认为 2,而总窗口大小默认是 1s,也就意味着默认的滑动窗口大小是 500ms。可以通过调整采样数量来调整统计的精度。

b)滑动窗口的数组 array,数组中每个元素以 WindowWrap 表示,其中包含:

记录请求的逻辑说白了,就是根据当前时间获取所属的滑动窗口,然后将该窗口的统计值 +1 即可。但实际上,获取当前所属的时间窗口这一步隐含了不少细节,详细的实现可以从LeapArray#currentWindow 中找到,源码的注释写得很详细,这里就不多提了。

这里借助一张其他同学画的图表述以上的流程:

以上的流程基于 3.9.21 版本的源码,早先版本的 Sentinel 内部版本实现不尽相同,使用了一个叫SentinelRollingNumber 的数据结构,但原理是类似的。

精度问题

现在思考这么一个问题:滑动窗口算法能否精准地控制任意给定时间窗口 T 内的访问量不大于 N?

答案是否定的,还是将 1 分钟分成 6 个 10 秒大小的子窗口的例子,假设请求的速率现在是 20 次/秒,从 0:05 时刻开始进入,那么在 0:05~0:10 时间段内会放进 100 个请求,同时接下来的请求都会被限流,直到 1:00 时刻窗口滑动,在 1:00~1:05 时刻继续放进 100 个请求。如果把 0:05~1:05 看作是 1 分钟的时间窗口,那么这个窗口内实际的请求量是 200,超出了给定的阈值 100。

如果要追求更高的精度,理论上只需要把滑动窗口切分得更细。像 Sentinel 中就可以通过修改单位时间内的采样数量 sampleCount 值来设置精度,这个值一般根据业务的需求来定,以达到在精度和内存消耗之间的平衡。

平滑度问题

使用滑动窗口算法限制流量时,我们经常会看到像下面一样的流量曲线。

突发的大流量在窗口开始不久就直接把限流的阈值打满,导致剩余的窗口内所有请求都无法通过。在时间窗口的单位比较大时(例如以分为单位进行流控),这种问题的影响就比较大了。在实际应用中我们要的限流效果往往不是把流量一下子掐断,而是让流量平滑地进入系统当中。

3 漏桶

滑动窗口无法很好地解决平滑度问题,再回过头看我们对于平滑度的诉求,当流量超过一定范围后,我们想要的效果不是一下子切断流量,而是将流量控制在系统能承受的一定的速度内。假设平均访问速率为 v, 那我们要做的流控其实是流速控制,即控制平均访问速率 v ≤ N / T。

在网络通信中常常用到漏桶算法来实现流量整形。漏桶算法的思路就是基于流速来做控制。想象一下上学时经常做的水池一边抽水一边注水的应用题,把水池换成水桶(还是底下有洞一注水就开始漏的那种),把请求看作是往桶里注水,桶底漏出的水代表离开缓冲区被 服务器 处理的请求,桶口溢出的水代表被丢弃的请求。在概念上类比:

假设起始时刻桶是空的,每次访问都会往桶里注入一单位体积的水量,那么当我们以小于等于 N/T 的速度往桶里注水时,桶内的水就永远不会溢出。反之,一旦实际注水速度超过漏水速度,桶里就会产生越来越多的积水,直到溢出为止。同时漏水的速度永远被控制在 N/T 以内,这就实现了平滑流量的目的。

漏桶算法的访问速率曲线如下:

附上一张网上常见的漏桶算法原题图:

代码实现 LeakyBucketThrottler

/** * 当前桶内剩余的水 */  private long left;  /** * 上次成功注水的时间戳 */  private long lastInjectTime = System.currentTimeMillis();  /** * 桶的容量 */  private long capacity;  /** * 一桶水漏完的时间 */  private long duration;  /** * 桶漏水的速度,即 capacity / duration */  private double velocity;  public boolean tryAcquire(String key) { long now = System.currentTimeMillis(); // 当前剩余的水 = 之前的剩余水量 - 过去这段时间内漏掉的水量 // 过去这段时间内漏掉的水量 = (当前时间-上次注水时间) * 漏水速度 // 如果当前时间相比上次注水时间相隔太久(一直没有注水),桶内的剩余水量就是0(漏完了) left = Math.max(0, left - (long)((now - lastInjectTime) * velocity)); // 往当前水量基础上注一单位水,只要没有溢出就代表可以访问 if (left + 1 <= capacity) { lastInjectTime = now; left++; return true; } else { return false; }  }

漏桶的问题

漏桶的优势在于能够平滑流量,如果流量不是均匀的,那么漏桶算法与滑动窗口算法一样无法做到真正的精确控制。极端情况下,漏桶在时间窗口 T 内也会放进相当于 2 倍阈值 N 的流量。

设想一下,如果访问量相比窗口大小 N 大很多,在窗口(0~T)一开始的 0 时刻就直接涌进来,使得漏桶在时间 t( 0≈t

虽然可以通过限制桶大小的方式使得访问量控制在 N 以内,但这样做的副作用是流量在还未达到限制条件就被禁止。

还有一个隐含的约束是,漏桶漏水的速度最好是一个整数值(即容量 N 能够整除时间窗口大小 T),否则在计算剩余水量时会有些许误差。

4 令牌桶

漏桶模型中,请求来了是往桶里注水,如果反一下,把请求放行变成从桶里抽水,对应的,把注水看作是补充系统可承受流量的话,漏桶模型就变成了令牌桶模型。

理解漏桶之后,再看令牌桶就很简单了,抄一段令牌桶的原理:

令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。

代码实现 TokenBucketThrottler

令牌桶与漏桶本质上是一样的,因此漏桶的代码稍微改下就可以变成令牌桶。

long now = System.currentTimeMillis();  left = Math.min(capacity, left + (long)((now - lastInjectTime) * velocity));  if (left - 1 > 0) { lastInjectTime = now; left--; return true;  } else { return false;  }

生产环境中使用令牌桶的话,可以考虑借助 Guava 中提供的 RateLimiter。它的实现是多线程安全的,调用 RateLimiter#acquire 时,如果剩余令牌不足,会阻塞线程一段时间直至有足够的可用令牌(而不是直接拒绝,这在某些场景下很有用)。除去默认的 SmoothBursty 策略外,RateLimiter 还提供了一种叫 SmoothWarmingUp 的策略,支持设置一个热身期,热身期内,RateLimiter 会平滑地将放令牌的速率加大,直致最大速率。设计这个的意图是为了满足那种资源提供方需要热身时间,而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存) 。RateLimiter 有一个缺点是只支持 QPS 级别。

漏桶、令牌桶的区别

虽然两者本质上只是反转了一下,不过在实际使用中,适用的场景稍有差别:

1)漏桶:用于控制网络中的速率。在该算法中,输入速率可以变化,但输出速率保持恒定。常常配合一个 FIFO 队列使用。

想象一下,漏桶的破洞是固定大小的,因此漏水的速率是可以保持恒定的。

2)令牌桶:按照固定速率往桶中添加令牌,允许输出速率根据突发大小而变化。

举个例子,一个系统限制 60 秒内的最大访问量是 60 次,换算速率是 1 次/秒,如果在一段时间内没有访问量,那么对漏桶而言此刻是空的。现在,一瞬间涌入 60 个请求,那么流量整形后,漏桶会以每秒 1 个请求的速度,花上 1 分钟将 60 个请求漏给下游。换成令牌桶的话,则是从令牌桶中一次性取走 60 个令牌,一下子塞给下游。

5 滑动日志

一般情况下,上述的算法已经能很好地用于大部分实际应用场景了,很少有场景需要真正完全精确的控制(即任意给定时间窗口T内请求量不大于 N )。如果要精确控制的话,我们需要记录每一次用户请求日志,当每次流控判断时,取出最近时间窗口内的日志数,看是否大于流控阈值。这就是滑动日志的算法思路。

设想某一个时刻 t 有一个请求,要判断是否允许,我们要看的其实是过去 t – N 时间段内是否有大于等于 N 个请求被放行,因此只要系统维护一个队列 q,里面记录每一个请求的时间,理论上就可以计算出从 t – N 时刻开始的请求数。

考虑到只需关心当前时间之前最长 T 时间内的记录,因此队列 q 的长度可以动态变化,并且队列中最多只记录 N 条访问,因此队列长度的最大值为 N。

滑动日志与滑动窗口非常像,区别在于滑动日志的滑动是根据日志记录的时间做动态滑动,而滑动窗口是根据子窗口的大小,以子窗口维度滑动。

伪代码实现

算法的伪代码表示如下:

# 初始化  counter = 0  q = []  # 请求处理流程  # 1.找到队列中第一个时间戳>=t-T的请求,即以当前时间t截止的时间窗口T内的最早请求  t = now  start = findWindowStart(q, t)  # 2.截断队列,只保留最近T时间窗口内的记录和计数值  q = q[start, q.length - 1] counter -= start  # 3.判断是否放行,如果允许放行则将这次请求加到队列 q 的末尾  if counter < threshold push(q, t) counter++ # 放行  else # 限流

findWindowStart 的实现依赖于队列 q 使用的数据结构,以简单的数组为例,可以使用二分查找等方式。后面也会看到使用其他数据结构如何实现。

如果用数组实现,一个难点可能是如何截断一个队列,一种可行的思路是使用一组头尾指针 head和 tail 分别指向数组中最近和最早的有效记录索引来解决, findWindowStart 的实现就变成在tail 和 head 之间查找对应元素。

复杂度问题

虽然算法解决了精确度问题,但代价也是显而易见的。

首先,我们要保存一个长度最大为 N 的队列,这意味着空间复杂度达到 O(N),如果要针对不同的 key 做流控,那么空间上会占用更多。当然,可以对不活跃 key 的队列进行复用来降低内存消耗。

其次,我们需要在队列中确定时间窗口,即通过 findWindowStart 方法寻找不早于当前时间戳 t – N 的请求记录。以二分查找为例,时间复杂度是 O(logN)。

四 分布式流控

现实中的应用服务往往是分布式部署的,如果共用的资源(例如数据库)或者依赖的下游服务有流量限制,那么分布式流控就要派上用场了。

虽然可以给每台应用服务器平均分配流控配额,把问题转换为单机流控,但如果碰到流量不均匀、机器宕机、临时扩缩容等场景,这种做法的效果不佳。

分布式环境下做流控的核心算法思路其实与单机流控是一致的,区别在于需要实现一种同步机制来保证全局配额。同步机制的实现可以有中心化和去中心化两种思路:

1)中心化:配额由一个中心系统统一管控,应用进程通过向中心系统申请的方式获取流控配额。

2)去中心化:应用进程独立保存和维护流控配额状态,集群内周期性异步通讯以保持状态一致。

去中心化方案在生产环境中没有见过,因此下文只讨论中心化流控的思路。

1 接入层入口流控

应用接入的网络架构中,在应用服务器之前往往有一层 LVS 或 Nginx 做统一入口,可以在这一层做入口的流控。本质上这就是单机流控的场景。

以 Nginx 为例,Nginx 提供了 ngx_http_limit_req_module 模块用于流控,底层使用的是漏桶算法。

一个 Nginx 流控配置的示例如下,表示每个 IP 地址每秒只能请求 10 次 /login/ 接口。

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;  server { location /login/ { limit_req zone=mylimit; proxy_pass}  }

Nginx 的流控指令还支持更多配置,比如说配置 limit_req 指令时加上 burst 和 nodelay 参数来允许一定程度的突发,或者结合 geo 和 map 指令来实现黑白名单流控,具体可以参考 Nginx 官方文档:Rate Limiting with NGINX and NGINX Plus(。

如果自带的模块不能满足,那就上自定义的 lua 模块吧,参考 OpenResty 提供的 Lua 限流模块lua-resty-limit-traffic。

2 TokenServer 流控

这里借用了 Sentinel 中的 TokenServer 叫法,Sentinel 集群流控的介绍可以参考官方文档:Sentinel集群流控(集群流控)。

这类流控的思路是找一个 TokenServer 来专门来管控流控配额,包括统计总调用量,判断单个请求是否允许等等,应用服务器作为客户端与 TokenServer 通信来获取配额。因为流控的逻辑在TokenServer 内部统一处理,因此单机流控中讨论的算法同样适用。

很自然地能想到,这类流控非常依赖于 TokenServer 的性能和可用性。

性能方面,单点的 TokenServer 很容易成为瓶颈,查 Sentinel 源码,其中使用了 Netty 来做网络通信,数据包采用自定义格式,其他性能优化能找到的不多。

可用性方面,就像 Sentinel 官方文档中讲的,若在生产环境使用 TokenServer 集群限流,必须要解决以下问题:

目前 Sentinel 的 TokenServer 默认并没有实现这些能力,需要定制或增加其他系统来实现,例如,采用一种分布式一致性协议来做集群选举,或者借助一组 monitor 来监控状态,实现成本还是挺高的。

3 存储式流控

存储式流控的思想是通过一个存储系统来保存流控的计数值等统计信息,应用从存储中获取统计信息,然后将最新的请求信息再写入存储中。存储系统可以选择现成的 Mysql 数据库或者 Redis 缓存等,一般从性能出发选择缓存的比较多。这里选择 Tair 和 Redis 做例子。

Tair 流控

比较简单,直接上代码实现。

public boolean tryAcquire(String key) { // 以秒为单位构建tair的key String wrappedKey = wrapKey(key); // 每次请求+1,初始值为0,key的有效期设置5s Result result = tairManager.incr(NAMESPACE, wrappedKey, 1, 0, 5); return result.isSuccess() && result.getValue() <= threshold;  }  private String wrapKey(String key) { long sec = System.currentTimeMillis() / 1000L; return key + ":" + sec;  }

是不是感觉太简单了点?得益于 Tair 的高性能,这种方式可以很好地支撑大流量。

这种 Tair 流控的方案实际上用的简单窗口的思路,每个 key 以每秒为一个时间窗口做 QPS 控制(QPM/QPD 原理类似)。关键在于用到了 Tair 的这个 API:

当然这种方式也有缺点:

打个比方,不同机器时间稍微差个 10ms,在时间窗口的间隔点上的统计就会产生比较大的误差,比如说在同一时刻,一台机器时间是 0.990,一台是 1.000,两者调用 incr 时操作的 key 不一样,精度自然就会受影响。

Redis 流控

Redis 支持丰富的数据结构,性能也不错,其“单进程”模型方便同步控制,因此非常适合用来做分布式流控的存储。

1)简单窗口实现

使用 Redis 实现简单窗口流控的思路跟使用 Tair 是一致的。Redis 也提供了 INCR 命令用于计数,同时 Redis 的“单进程”模型也提供了很好的并发保护。Redis 的官方文档就写了如何使用INCR 来实现 Rate Limiter,我这里稍作翻译了下:

以简单窗口为例,最简单直接的实现如下:

FUNCTION LIMIT_API_CALL(ip)  ts = CURRENT_UNIX_TIME()  keyname = ip+":"+ts  current = GET(keyname)  IF current != NULL AND current > 10 THEN ERROR "too many requests per second"  ELSE MULTI INCR(keyname,1) EXPIRE(keyname,10) EXEC PERFORM_API_CALL()  END

实现上与上述的 Tair 类似,也是对每个 key 以秒为单位维护一个计数器,差别在于因为 Redis 没有提供原子的 INCR + EXPIRE 指令,所以在 INCR 之后需要再调用一次 EXPIRE 来设置 key 的有效期。同时在外层以 MULTI 和 EXEC 包裹以保证事务性。

如果不想每次都调用 EXPIRE,可以考虑第二种方式:

FUNCTION LIMIT_API_CALL(ip):  current = GET(ip)  IF current != NULL AND current > 10 THEN ERROR "too many requests per second"  ELSE value = INCR(ip) IF value == 1 THEN EXPIRE(ip,1) END PERFORM_API_CALL()  END

计数器的有效期在第一次 INCR 时设置为 1s,因此不需要对 key 进行额外处理。

不过需要注意的是,这种方式存在一种隐藏的竞争条件。如果客户端在第一次调用了 INCR 后,由于应用崩溃或其他原因没有调用 EXPIRE,计数器会一直存在。

针对方式二的这个问题,可以用 lua 脚本解决:

local current  current = redis.call("incr",KEYS[1])  if tonumber(current) == 1 then redis.call("expire",KEYS[1],1)  end

第三种方式是通过 Redis 的 list 结构来实现。更复杂一些但可以记录下每次的请求。

FUNCTION LIMIT_API_CALL(ip)  current = LLEN(ip)  IF current > 10 THEN ERROR "too many requests per second"  ELSE IF EXISTS(ip) == FALSE#1 MULTI RPUSH(ip,ip) EXPIRE(ip,1) EXEC ELSE RPUSHX(ip,ip) END PERFORM_API_CALL()  END

这里也有一个隐含的竞争条件,在执行到 EXIST 判断这一行(#1 位置)时,两个客户端的 EXIST命令可能都会返回 false,因此 MULTI/EXEC 块里的命令会被执行两次,不过这种情况很少出现,不太会影响计数器的准确性。

上述的几种方式还可以进一步优化,因为 INCR 和 RPUSH 这些命令都会返回操作后的计数器值,所以可以使用 set-then-get 的方式获取计数器值。

将简单窗口改造成滑动窗口也是类似的思路,把单一的 key 换成一个 hash 结构,hash 里面为每个子窗口保存一个计数值,在统计时,将同个 hash 中所有子窗口的计数值相加即可。

2)令牌桶/漏桶实现

用 Redis 实现令牌桶或者漏桶也非常简单。以令牌桶为例,在实现上,可以用两个 key 分别存储每个用户的可用 token 数和上次请求时间,另一种可能更好的办法是使用 Redis 的 hash 数据结构。

下图的示例是一个用户 user_1 当前在 Redis 中保存的流控配额数据:令牌桶中当前剩余 2 个 token,最近一次访问的时间戳是 1490868000。

当收到一个新请求时,Redis 客户端要执行的操作与我们在单机流控算法中看到的一样。首先,从对应 hash 中获得当前配额数据(HGETALL),根据当前时间戳、上次请求的时间戳和 token 填充速度计算要填充的 token 数;然后,判断是否放行,更新新的时间戳和 token 数(HMSET)。

一个示例如下:​​

同样的,如果要求比较高的精度,这里必须要对客户端的操作做并发控制。

不做同步控制可能导致的问题示例:桶里只有一个 token,两个客户端同时请求时出现并发冲突,结果是请求都会放行。

lua 代码示例如下:

local tokens_key = KEYS[1]  local timestamp_key = KEYS[2]  local rate = tonumber(ARGV[1])  local capacity = tonumber(ARGV[2])  local now = tonumber(ARGV[3])  local requested = tonumber(ARGV[4])  local fill_time = capacity/rate  local ttl = math.floor(fill_time*2)  local last_tokens = tonumber(redis.call("get", tokens_key))  if last_tokens == nil then last_tokens = capacity  end  local last_refreshed = tonumber(redis.call("get", timestamp_key))  if last_refreshed == nil then last_refreshed = 0  end  local delta = math.max(0, now-last_refreshed)  local filled_tokens = math.min(capacity, last_tokens+(delta*rate))  local allowed = filled_tokens >= requested  local new_tokens = filled_tokens  if allowed then new_tokens = filled_tokens - requested  end  redis.call("setex", tokens_key, ttl, new_tokens)  redis.call("setex", timestamp_key, ttl, now)  return { allowed, new_tokens }

3)滑动日志实现

得益于 Redis 的 Sorted Set 结构,实现滑动日志变得异常简单。流程大致如下:

a)每个用户有一个对应的 Sorted Set 记录请求日志。

其中每个元素的 key 和 value 可以是相同的,即请求的时间戳。

Sorted Set 可以根据时间窗口大小设置有效期,比如时间窗口为 1s 时设置过期时间 5s,在请求量不大时可以节省 Redis 服务器内存。

b)当收到一个新的用户请求时,首先通过 ZREMRANGEBYSCORE 命令删除 Sorted Set 中过期的元素,这里的过期即:

请求时间戳 t < 当前时间戳 now – 时间窗口大小 interval

c)使用 ZADD 将当前请求添加到 Set 中。

d)使用 ZCOUNT 获取当前剩余 Set 大小,判断是否需要流控。

FUNCTION LIMIT_API_CALL(ip):  current = GET(ip)  IF current != NULL AND current > 10 THEN ERROR "too many requests per second"  ELSE value = INCR(ip) IF value == 1 THEN EXPIRE(ip,1) END PERFORM_API_CALL()  END

另一个 JS 实现的代码示例:

由于滑动日志算法的空间复杂度较其他算法高,使用滑动日志算法时,需要注意监控 Redis 内存的使用量。

4)并发控制

上面的几种算法都提到了不做并发控制可能带来的竞态条件,但额外的并发控制必然会带来性能下降,通常需要在精度和性能之间做取舍。Redis 流控的并发控制常见的有几类:

最好通过性能测试来决定使用哪一种方式。

4 扩展的一些思考

分布式流控带来了网络通信、加锁同步等开销,会对性能带来一定影响。同时分布式环境的可靠性也会带来更多挑战。如何设计一个高性能、高可靠性的分布式流控系统?这可能是个涉及到整个系统方方面面的大话题。

分享一下个人的一些思考,欢迎讨论:

1)根据实际诉求,合理搭配不同层的多级流控是个不错的方式,尽量把流量拦在外层。例如常见的接口层 Nginx 流控 + 应用层流控。

2)选择一个合适的缓存系统保存流控的动态数据,这个一般跟着公司的统一技术架构走。

3)将流控的静态配置放到配置中心(例如 Diamond)。

4)设计时要考虑分布式流控不可用的情况(例如缓存挂掉),必要时切到单机流控,使用 Sentinel 成熟可靠。

5)很多时候对精度的要求没那么高,因为一般都会允许一定的突发量。这时候可以做一些性能的优化。性能的最大瓶颈在于每次请求都会访问一次缓存,我之前在设计时就采用了一种折中的办法:

五 总结

分布式流控的算法其实是单机流控的延伸,算法本质是一样的。这里按我的个人理解总结了上述几种流控算法的复杂度和适用场景。


LED电子显示屏系统的分类

有哪些流控方案

LED是发光二极管Light Emitting Diode 的英文缩写。 LED显示屏是由发光二极管排列组成的一显示器件。 它采用低电压扫描驱动,具有如下优点:1、耗电省、2、使用寿命长、3、成本低、4、亮度高、5、视角大、6、可视距离远、7、规格品种多。 LED显示产品系列:A、单色、彩色条形显示屏、B、计算机控制数码显示屏、C、单色图文显示屏、D、三色(红、绿、黄)图文显示屏、E、点阵和数码混合显示屏(证券屏)、F、双基色(红、绿)多媒体视频同步显示屏、G、三基色(红、绿、蓝)多媒体视频同步显示屏LED显示屏分类:按显示颜色分为:单红色、单绿色、红绿双基色、 红绿蓝三色按使用功能分为:图文显示屏、多媒体视频显示屏、行情显示屏、条形显示屏按使用环境分为:室内显示屏、室外显示屏、半户外显示屏按发光点直径分为:φ3.0、φ3.7、φ4.8、φ5.0、φ8.0、ph8、ph10、ph16、ph20等。 基本发光点非行情类LED显示屏中,室内LED显示屏按采用的LED单点直径可分为Φ3mm、Φ3.75mm、Φ5mm、Φ8mm、和Φ10mm等显示屏;室外LED显示屏按采用的象素直径可分为Φ16mm、Φ19mm、Φ22mm和Φ26mm等LED显示屏。 行情类LED显示屏中按采用的数码管尺寸可分2.0cm(0.8inch)、2.5cm(1.0inch)、3.0cm(1.2inch)、4.6cmm(1.8inch)、5.8cm(2.3inch)、7.6cm(3inch)等LED显示屏。

显示颜色:LED显示屏按显示颜色分为单基色LED显示屏(含伪彩色LED显示屏),双基色LED显示屏和全彩色(三基色)LED显示屏。 按灰度级又可分为16、32、64、128、256级灰度LED显示屏等。 显示性能:LED显示屏按显示性能分为文本LED显示屏、图文LED显示屏,计算机视频LED显示屏,电视视频LED显示屏和行情LED显示屏等。 行情LED显示屏一般包括证券、利率、期货等用途的LED显示屏。

SQL server 2000 和 2005有什么区别?

数据库管理10个最重要的特点特点 描述数据库镜像通过新数据库镜像方法,将记录档案传送性能进行延伸。 您将可以使用数据库镜像,通过将自动失效转移建立到一个待用服务器上,增强您SQL服务器系统的可用性。 在线恢复使用SQL2005版服务器,数据库管理人员将可以在SQL服务器运行的情况下,执行恢复操作。 在线恢复改进了SQL服务器的可用性,因为只有正在被恢复的数据是无法使用的,而数据库的其他部分依然在线、可供使用。 在线检索操作在线检索选项可以在指数数据定义语言(DDL)执行期间,允许对基底表格、或集簇索引数据和任何有关的检索,进行同步修正。 例如,当一个集簇索引正在重建的时候,您可以对基底数据继续进行更新、并且对数据进行查询。 快速恢复新的、速度更快的恢复选项可以改进SQL服务器数据库的可用性。 管理人员将能够在事务日志向前滚动之后,重新连接到正在恢复的数据库。 安全性能的提高SQL Server 2005包括了一些在安全性能上的改进,例如数据库加密、设置安全默认值、增强密码政策、缜密的许可控制、以及一个增强型的安全模式。 新的SQL Server Management StudioSQL Server 2005引入了SQL Server Management Studio,这是一个新型的统一的管理工具组。 这个工具组将包括一些新的功能,以开发、配置SQL Server数据库,发现并修理其中的故障,同时这个工具组还对从前的功能进行了一些改进。 专门的管理员连接SQL Server 2005将引进一个专门的管理员连接,即使在一个服务器被锁住,或者因为其他原因不能使用的时候,管理员可以通过这个连接,接通这个正在运行的服务器。 这一功能将能让管理员,通过操作诊断功能、或Transact—SQL指令,找到并解决发现的问题。 快照隔离我们将在数据库层面上提供一个新的快照隔离(SI)标准。 通过快照隔离,使用者将能够使用与传统一致的视野观看数据库,存取最后执行的一行数据。 这一功能将为服务器提供更大的可升级性。 数据分割数据分割 将加强本地表检索分割,这使得大型表和索引可以得到高效的管理。 增强复制功能对于分布式数据库而言,SQL Server 2005提供了全面的方案修改(DDL)复制、下一代监控性能、从甲骨文(Oracle)到SQL Server的内置复制功能、对多个超文本传输协议(http)进行合并复制,以及就合并复制的可升级性和运行,进行了重大的改良。 另外,新的对等交易式复制性能,通过使用复制,改进了其对数据向外扩展的支持。 有关开发的10个最重要的特点特点 描述 框架主机使用SQL Server 2005,开发人员通过使用相似的语言,例如微软的Visual C# 和微软的Visual Basic,将能够创立数据库对象。 开发人员还将能够建立两个新的对象——用户定义的类和集合。 XML 技术在使用本地网络和互联网的情况下,在不同应用软件之间散步数据的时候,可扩展标记语言(XML)是一个重要的标准。 SQL Server 2005将会自身支持存储和查询可扩展标记语言文件。 2.0 版本从对SQL类的新的支持,到多活动结果集(MARS),SQL Server 2005中的将推动数据集的存取和操纵,实现更大的可升级性和灵活性。 增强的安全性SQL Server 2005中的新安全模式将用户和对象分开,提供fine-grain access存取、并允许对数据存取进行更大的控制。 另外,所有系统表格将作为视图得到实施,对数据库系统对象进行了更大程度的控制。 Transact-SQL 的增强性能SQL Server 2005为开发可升级的数据库应用软件,提供了新的语言功能。 这些增强的性能包括处理错误、递归查询功能、关系运算符PIVOT, APPLY, ROW_NUMBER和其他数据列排行功能,等等。 SQL 服务中介SQL服务中介将为大型、营业范围内的应用软件,提供一个分布式的、异步应用框架。 通告服务通告服务使得业务可以建立丰富的通知应用软件,向任何设备,提供个人化的和及时的信息,例如股市警报、新闻订阅、包裹递送警报、航空公司票价等。 在SQL Server 2005中,通告服务和其他技术更加紧密地融合在了一起,这些技术包括分析服务、SQL Server Management Studio。 Web服务使用SQL Server 2005,开发人员将能够在数据库层开发Web服务,将SQL Server当作一个超文本传输协议(HTTP)侦听器,并且为网络服务中心应用软件提供一个新型的数据存取功能。 报表服务利用SQL Server 2005, 报表服务可以提供报表控制,可以通过Visual Studio 2005发行。 全文搜索功能的增强SQL SERVER 2005将支持丰富的全文应用软件。 服务器的编目功能将得到增强,对编目的对象提供更大的灵活性。 查询性能和可升级性将大幅得到改进,同时新的管理工具将为有关全文功能的运行,提供更深入的了解。 有关商业智能特征的10个最重要的特点特点 描述分析服务SQL SERVER 2005的分析服务迈入了实时分析的领域。 从对可升级性性能的增强、到与微软Office软件的深度融合,SQL SERVER 2005将帮助您,将商业智能扩展到您业务的每一个层次。 数据传输服务(DTS)DTS数据传输服务是一套绘图工具和可编程的对象,您可以用这些工具和对象,对从截然不同来源而来的数据进行摘录、传输和加载(ETL),同时将其转送到单独或多个目的地。 SQL SERVER 2005将引进一个完整的、数据传输服务的、重新设计方案,这一方案为用户提供了一个全面的摘录、传输和加载平台。 数据挖掘我们将引进四个新的数据挖掘运算法,改进的工具和精灵,它们会使数据挖掘,对于任何规模的企业来说,都变得简单起来。 报表服务在SQL SERVER 2005中,报表服务将为在线分析处理(OLAP)环境提供自我服务、创建最终用户特别报告、增强查询方面的开发水平,并为丰富和便于维护企业汇报环境,就允许升级方面,提供增进的性能。 集群支持通过支持容错技术移转丛集、增强对多重执行个体的支持、以及支持备份和恢复分析服务对象和数据,分析服务改进了其可用性。 主要运行指标主要运行指标(KPIs)为企业提供了新的功能,使其可以定义图表化的、和可定制化的商业衡量标准,以帮助公司制定和跟踪主要的业务基准。 可伸缩性和性能并行分割处理,创建远程关系在线分析处理(ROLAP)或混合在线分析处理(HOLAP)分割,分布式分割单元,持续计算,和预制缓存等特性,极大地提升了SQL Server 2005中分析服务的可伸缩性和性能。 单击单元当在一个数据仓库中创建一个单元时,单元向导将包括一个可以单击单元检测和建议的操作。 预制缓存预制缓存将MOLAP等级查询运行与实时数据分析合并到一起,排除了维护在线分析处理存储的需要。 显而易见,预制缓存将数据的一个更新备份进行同步操作,并对其进行维护,而这些数据是专门为高速查询而组织的、它们将最终用户从超载的相关数据库分离了出来。 与Microsoft Office System集成在报表服务中,由报表服务器提供的报表能够在Microsoft SharePoint门户服务器和Microsoft Office System应用软件的环境中运行,Office System应用软件其中包括Microsoft Word和Microsoft Excel。 您可以使用SharePoint功能,订阅报表、建立新版本的报表,以及分发报表。 您还能够在Word或Excel软件中打开报表,观看超文本连接标示语言(HTML)版本的报表。

什么是sdwan实施方案?

SD-WAN解决了小型企业、中型企业和分布式企业的独特需求 - 提供智能、强大的安全性,对于没有大量 IT 资源的组织来说很容易成功管理。

寻求 SD-WAN 运营优势的企业发现,他们还需要添加分支机构安全性来实施混合 WAN 架构,使其一切正常。

我们提供一系列适合分支机构、零售店和医疗诊所的设备,并且可以提供 SD-WAN 和一流的安全性。 这使得它比其他需要公司在分支机构部署多个盒子或与多个供应商合作的专业产品更具成本效益和更易于使用。

SD-WAN 是网络安全设备的标准配置。 除了网络功能和集中管理外,我们的解决方案还包括强大的 VPN、强大的加密和防火墙服务,这些服务不是 SD-WAN 所特别需要的,但对于在整个组织中保持共同的安全状态是必不可少的。

SD-WAN服务使我们的客户无需技术人员即可在远程站点轻松设置和安装。 只需插入它,它就会从云中的模板下载其配置。

SD-WAN后台统计多个 WAN 选项的性能(包括抖动、延迟和数据包丢失),以自动为每种类型的分支机构流量选择最佳 Internet 连接。 使用 SD-WAN 动态路径选择,管理员可以为每个连接和流量类型定义可接受的最低性能级别。

本文版权声明本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请联系本站客服,一经查实,本站将立刻删除。

发表评论

热门推荐