个人免费网站建站运营,建设银行个人网上银行网站加载,建设网站的分析,手机网站源码教程一、基础概念
1.什么是服务限流#xff1f;
限流在日常生活中也很常见#xff0c;比如节假日你去一个旅游景点#xff0c;为了不把景点撑爆#xff0c;管理部门通常会在外面设置拦截#xff0c;限制景点的进入人数#xff08;等有人出来之后#xff0c;再放新的人进去…一、基础概念
1.什么是服务限流
限流在日常生活中也很常见比如节假日你去一个旅游景点为了不把景点撑爆管理部门通常会在外面设置拦截限制景点的进入人数等有人出来之后再放新的人进去。
对应到计算机中限流是从用户访问压力的角度来考虑如何应对故障保护系统不会在过载的情况下出现问题这就就需要限流。
限流只允许系统能够承受的访问量进来超出系统访问能力的请求将被丢弃。比如要搞活动秒杀等通常都会限流。 2.限流的策略
说到限流有个关键问题就是你根据什么策略进行限制
限流一般需要结合容量规划和压测来进行。当外部请求接近或者达到系统的最大阈值时触发限流采取其他的手段进行降级保护系统不被压垮。常见的降级策略包括延迟处理、拒绝服务、随机拒绝等。
限流的目的是通过对并发访问进行限速相关的策略一般是一旦达到限制的速率那么就会触发相应的限流行为。一般来说触发的限流行为如下。 拒绝服务。把多出来的请求拒绝掉。一般来说好的限流系统在受到流量暴增时会统计当前哪个客户端来的请求最多直接拒掉这个客户端这种行为可以把一些不正常的或者是带有恶意的高并发访问挡在门外。 服务降级。关闭或是把后端服务做降级处理。这样可以让服务有足够的资源来处理更多的请求。降级有很多方式一种是把一些不重要的服务给停掉把 CPU、内存或是数据的资源让给更重要的功能一种是不再返回全量数据只返回部分数据。 因为全量数据需要做 SQL Join 操作部分的数据则不需要所以可以让 SQL 执行更快还有最快的一种是直接返回预设的缓存以牺牲一致性的方式来获得更大的性能吞吐。 特权请求。所谓特权请求的意思是资源不够了我只能把有限的资源分给重要的用户比如分给权利更高的 VIP 用户。在多租户系统下限流的时候应该保大客户的所以大客户有特权可以优先处理而其它的非特权用户就得让路了。 延时处理。在这种情况下一般会有一个队列来缓冲大量的请求这个队列如果满了那么就只能拒绝用户了如果这个队列中的任务超时了也要返回系统繁忙的错误了。使用缓冲队列只是为了减缓压力一般用于应对短暂的峰刺请求。 弹性伸缩。动用自动化运维的方式对相应的服务做自动化的伸缩。这个需要一个应用性能的监控系统能够感知到目前最繁忙的 TOP 5 的服务是哪几个。 然后去伸缩它们还需要一个自动化的发布、部署和服务注册的运维系统而且还要快越快越好。否则系统会被压死掉了。当然如果是数据库的压力过大弹性伸缩应用是没什么用的这个时候还是应该限流。 二、限流的架构设计
我们在一些系统中都可以看到这样的设计比如我们的数据库访问的连接池还有我们的线程池还有 Nginx 下的用于限制瞬时并发连接数的 limit_conn 模块限制每秒平均速率的 limit_req 模块还有限制 MQ 的生产速等等。 1.限流的核心思想
限流一般都是系统内实现的常见的限流方式可以分为两类基于请求限流和基于资源限流。
1.基于请求限流
基于请求限流指从外部访问的请求角度考虑限流常见的方式有限制总量、限制时间量。
1.限制总量
限制总量的方式是限制某个指标的累积上限常见的是限制当前系统服务的用户总量例如某个直播间限制总用户数上限为 100 万超过 100 万后新的用户无法进入某个抢购活动商品数量只有 100 个限制参与抢购的用户上限为 1 万个1 万以后的用户直接拒绝。
2.限制时间量
限制时间量指限制一段时间内某个指标的上限例如1 分钟内只允许 10000 个用户访问每秒请求峰值最高为 10 万。 无论是限制总量还是限制时间量共同的特点都是实现简单但是当达到阀值后直接抛弃请求未免过于暴力可以采用延时处理的机制如设计一个延时队列进行延时处理并返回限流错误码并提示用户系统繁忙中不要在进行请求了或者提示收到了请求请稍后查看结果等避免延时队列撑爆。 在实践中面临的主要问题还有比较难以找到合适的阈值例如系统设定了 1 分钟 10000 个用户但实际上 6000 个用户的时候系统就扛不住了也可能达到 1 分钟 10000 用户后其实系统压力还不大但此时已经开始丢弃用户访问了。 即使找到了合适的阈值基于请求限流还面临硬件相关的问题。例如一台 32 核的机器和 64 核的机器处理能力差别很大阈值是不同的可能有的技术人员以为简单根据硬件指标进行数学运算就可以得出来实际上这样是不可行的64 核的机器比 32 核的机器业务处理性能并不是 2 倍的关系可能是 1.5 倍甚至可能是 1.1 倍。
为了找到合理的阈值通常情况下可以采用性能压测来确定阈值但性能压测也存在覆盖场景有限的问题可能出现某个性能压测没有覆盖的功能导致系统压力很大另外一种方式是逐步优化即先设定一个阈值然后上线观察运行情况发现不合理就调整阈值。
基于上述的分析根据阈值来限制访问量的方式更多的适应于业务功能比较简单的系统例如负载均衡系统、网关系统、抢购系统等。 2.基于资源限流
基于请求限流是从系统外部考虑的而基于资源限流是从系统内部考虑的即找到系统内部影响性能的关键资源对其使用上限进行限制。常见的内部资源有连接数、文件句柄、线程数、请求队列等。 例如采用 Netty 来实现服务器每个进来的请求都先放入一个队列业务线程再从队列读取请求进行处理队列长度最大值为 10000队列满了就拒绝后面的请求
也可以根据 CPU 的负载或者占用率进行限流当 CPU 的占用率超过 80% 的时候就开始拒绝新的请求。
基于资源限流相比基于请求限流能够更加有效地反映当前系统的压力但实践中设计也面临两个主要的难点如何确定关键资源如何确定关键资源的阈值。通常情况下这也是一个逐步调优的过程即设计的时候先根据推断选择某个关键资源和阈值然后测试验证再上线观察如果发现不合理再进行优化。
2.静态限流的解决方案
1.计数器方式
最简单的限流算法就是维护一个计数器 Counter当一个请求来时就做加一操作当一个请求处理完后就做减一操作。如果这个 Counter 大于某个数了我们设定的限流阈值那么就开始拒绝请求以保护系统的负载了。通常这个计数器 Counter可以基于进程内实现也可以基于三方存储如redis实现。
这个算法足够的简单粗暴。 计数器方式的缺陷 2.队列算法
在这个算法下请求的速度可以是波动的而处理的速度则是非常均速的。这个算法其实有点像一个 FIFO 的算法。 在上面这个 FIFO 的队列上我们可以扩展出一些别的玩法。
一个是有优先级的队列处理时先处理高优先级的队列然后再处理低优先级的队列。 如下图所示只有高优先级的队列被处理完成后才会处理低优先级的队列。 有优先级的队列可能会导致低优先级队列长时间得不到处理。为了避免低优先级的队列被饿死一般来说是分配不同比例的处理时间到不同的队列上于是我们有了带权重的队列。
如下图所示。有三个队列的权重分布是 3:2:1这意味着我们需要在权重为 3 的这个队列上处理 3 个请求后再去权重为 2 的队列上处理 2 个请求最后再去权重为 1 的队列上处理 1 个请求如此反复。 队列流控是以队列的的方式来处理请求。如果处理过慢那么就会导致队列满而开始触发限流。
但是这样的算法需要用队列长度来控制流量在配置上比较难操作。如果队列过长导致后端服务在队列没有满时就挂掉了。一般来说这样的模型不能做 push而是 pull 方式会好一些。 3.漏斗算法 Leaky Bucket
漏斗算法可以参看 Wikipedia 的相关词条 Leaky Bucket。
下图是一个漏斗算法的示意图 。 我们可以看到就像一个漏斗一样进来的水量就好像访问流量一样而出去的水量就像是我们的系统处理请求一样。当访问流量过大时这个漏斗中就会积水如果水太多了就会溢出。
一般来说这个“漏斗”是用一个队列来实现的当请求过多时队列就会开始积压请求如果队列满了就会开拒绝请求。很多系统都有这样的设计比如 TCP。当请求的数量过多时就会有一个 sync backlog 的队列来缓冲请求或是 TCP 的滑动窗口也是用于流控的队列。 我们可以看到漏斗算法其实就是在队列请求中加上一个限流器来让 Processor 以一个均匀的速度处理请求。 如果外部请求超出当前阈值则会在容器里积蓄一直到溢出系统并不关心溢出的流量。 从出口处限制请求速率即按照一定的速率去取消息并不存在计数器法的临界问题请求曲线始终是平滑的。
无法应对突发流量相当于一个空桶固定处理线程。 4.令牌桶算法 Token Bucket 令牌桶算法的设计思想 假设一个大小恒定的桶这个桶的容量和设定的阈值有关桶里放着很多令牌通过一个 固定的速率往里边放入令牌如果桶满了就把令牌丢掉最后桶中可以保存的最大令牌数永远不会超过桶的大小。当有请求进入时就尝试从桶里取走一个令牌如果桶里是空的那么这个请求就会被拒绝。 关于令牌桶算法主要是有一个中间人。在一个桶内按照一定的速率放入一些 token然后处理程序要处理请求时需要拿到 token才能处理如果拿不到则不处理。
下面这个图很清楚地说明了这个算法。 从理论上来说令牌桶的算法和漏斗算法不一样的是漏斗算法中处理请求是以一个常量和恒定的速度处理的而令牌桶算法则是在流量小的时候“攒钱”流量大的时候可以快速处理当流量大时可以快速将队列里面的请求消费掉去消费能力取决于令牌的投放速率。比如队列里面堆积了100个请求漏斗算法可能每次均匀处理10个请求但令牌桶的算法取决于当前投放的令牌数如果当前令牌桶已经堆积了50个令牌就会将队列里的50个请求直接发送到后端服务然后继续堆积令牌数。 然而我们可能会问Processor 的处理速度因为有队列的存在所以其总是能以最大处理能力来处理请求这也是我们所希望的方式。
因此令牌桶和漏斗都是受制于 Processor 的最大处理能力。无论令牌桶里有多少令牌也无论队列中还有多少请求。总之Processor 在大流量来临时总是按照自己最大的处理能力来处理的。
但是试想一下如果我们的 Processor 只是一个非常简单的任务分配器比如像 Nginx 这样的基本没有什么业务逻辑的网关那么它的处理速度一定很快不会有什么瓶颈而其用来把请求转发给后端服务那么在这种情况下这两个算法就有不一样的情况了。
漏斗算法会以一个稳定的速度转发而令牌桶算法平时流量不大时在“攒钱”流量大时可以一次发出队列里有的请求而后就受到令牌桶的流控限制。
另外令牌桶还可能做成第三方的一个服务这样可以在分布式的系统中对全局进行流控这也是一个很好的方式。 3.动态限流的解决方案
上面的算法有个不好的地方就是需要设置一个确定的限流值。这就要求我们每次发布服务时都做相应的性能测试找到系统最大的性能值。
当然性能测试并不是很容易做的。有关性能测试的方法请参看我在 CoolShell 上的这篇文章《性能测试应该怎么做》。虽然性能测试比较不容易但是还是应该要做的。
然而在很多时候我们却并不知道这个限流值或是很难给出一个合适的值。其基本会有如下的一些因素 实际情况下很多服务会依赖于数据库。所以不同的用户请求会对不同的数据集进行操作。就算是相同的请求可能数据集也不一样比如现在很多应用都会有一个时间线 Feed 流不同的用户关心的主题人人不一样数据也不一样。 而且数据库的数据是在不断变化的可能前两天性能还行因为数据量增加导致性能变差。在这种情况下我们很难给出一个确定的一成不变的值因为关系型数据库对于同一条 SQL 语句的执行时间其实是不可预测的NoSQL 的就比 RDBMS 的可预测性要好。 不同的 API 有不同的性能。我们要在线上为每一个 API 配置不同的限流值这点太难配置也很难管理。 而且现在的服务都是能自动化伸缩的不同大小的集群的性能也不一样所以在自动化伸缩的情况下我们要动态地调整限流的阈值这点太难做到了。
基于上述这些原因我们限流的值是很难被静态地设置成恒定的一个值。
我们想使用一种动态限流的方式。这种方式不再设定一个特定的流控值而是能够动态地感知系统的压力来自动化地限流。
这方面设计的典范是 TCP 协议的拥塞控制的算法。TCP 使用 RTT - Round Trip Time 来探测网络的延时和性能从而设定相应的“滑动窗口”的大小以让发送的速率和网络的性能相匹配。这个算法是非常精妙的我们完全可以借鉴在我们的流控技术中。
我们记录下每次调用后端请求的响应时间然后在一个时间区间内比如过去 10 秒的请求计算一个响应时间的 P90 或 P99 值也就是把过去 10 秒内的请求的响应时间排个序然后看 90% 或 99% 的位置是多少。这样我们就知道有多少请求大于某个响应时间。如果这个 P90 或 P99 超过我们设定的阈值那么我们就自动限流。 这个设计中有几个要点。 你需要计算的一定时间内的 P90 或 P99。在有大量请求的情况下这个非常地耗内存也非常地耗 CPU因为需要对大量的数据进行排序。 解决方案有两种一种是不记录所有的请求采样就好了另一种是使用一个叫蓄水池的近似算法。关于这个算法这里我不就多说了《编程珠玑》里讲过这个算法你也可以自行 Google英文叫 Reservoir Sampling。 这种动态流控需要像 TCP 那样你需要记录一个当前的 QPS. 如果发现后端的 P90/P99 响应太慢那么就可以把这个 QPS 减半然后像 TCP 一样走慢启动的方式直接到又开始变慢然后减去 1/4 的 QPS再慢启动然后再减去 1/8 的 QPS…… 这个过程有点像个阻尼运行的过程然后整个限流的流量会在一个值上下做小幅振动。这么做的目的是如果后端扩容伸缩后性能变好系统会自动适应后端的最大性能。 这种动态限流的方式实现起来并不容易。大家可以看一下 TCP 的算法。TCP 相关的一些算法我写在了 CoolShell 上的《TCP 的那些事下》这篇文章中。你可以用来做参考来实现。
在《左耳听风》的作者创业中的 Ease Gateway 的产品中实现了这个算法。