开发高并发系统之限流设计最全总结

什么是限流?

在开发高并发系统时,有很多手段来保护系统,如缓存、降级和限流等。缓存目的是提升系统访问速度和增大系统处理能力,可谓是抗高并发流量的银弹。而降级是当服务出问题或者影响到核心流程的性能,需要暂时屏蔽掉,待高峰过去或者问题解决后再打开的场景。而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页)等。因此,需有一 种手段来限制这些场景下的并发/请求量,这种手段就是限流。

目的

一般来讲,我们限流主要是通过对并发访问/请求进行限速或者一段时间内请求的速率限制来保护系统。一旦达到我们限制的速率,则可以有以下一些处理方式:

  1. 拒绝服务(定向到错误页或告知资源没有了)
  2. 排队或等待(比如秒杀、评论、下单)
  3. 降级(返回兜底数据或默认数据)

高并发系统常见的限流手段

限制总并发数(比如数据库连接池、线程池)

限制瞬时并发数(如Nginx的limit_conn模块,用来限制瞬时并发连接数)

限制时间窗口内的平均速率(如Guava的RateLimiter、Nginx的limit_req模块,用来限制每秒的平均速率)

限制远程接口调用速率、限制MQ的消费速率等。

另外,还可以根据网络连接数、网络流量、CPU或内存负载等来限流。

从我前面的介绍来看,限流的手段有很多的,在真正需要用到上面的方式来进行限流还需要掌握限流方面的知识。下面我会一一总结。从限流算法、应用级限流、分布式限流、接入层限流来详细学习限流技术手段。

一、限流算法

限流算法,就是我们进行限流时,根据这个限流算法,能够使用不同的限流算法,达到我们限流的一些目的。

常见的限流算法有:令牌桶、漏桶。计数器也可以用来进行粗暴限流实现。

令牌桶算法

令牌桶算法,是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。

描述如下:
• 假设限制2r/s(这个意思是每秒处理2个请求), 则按照500毫秒的固定速率往桶中添加令牌。
• 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
• 当一 个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
• 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么在缓冲区等待)。


漏桶算法

漏桶作为计量工具时,可以用于流量整形和流量控制,漏桶容量如下:

一个固定容量的漏桶,按照常量固定速率流出水滴。
如果桶是空的,则不需流出水滴
可以以任意速率流入水滴到漏桶

如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

两者的区别

• 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时,则拒绝新的请求。
• 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。
• 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持(一次拿3个令牌,或4个令牌),并允许一 定程度的突发流量。
• 漏桶限制的是常量流出速率(即流出速率是一 个固定常量值,比如都是1的速率流出,而不能一 次是1, 下次又是2), 从而平滑突发流入速率。
• 令牌桶允许一 定程度的突发,而漏桶主要目的是平滑流入速率。
• 两个算法实现可以一 样,但是方向是相反的,对于相同的参数得到的限流效果是一 样的。

另外,有时我们还使用计数器来进行限流,主要用来限制总并发数,比如数据库连接池大小、线程池大小、秒杀并发数都是计数器的用法。只要全局总请求数或者一 定时间段的总请求数达到设定阅值,则进行限流。这是一 种简单粗暴的总数量限流,而不是平均速率限流。

二、应用级别的限流

1. 限流总并发/连接I请求数

对于一个应用级别的系统,一般都会有一个极限并发数,也可以理解为一个TPS/QPS 阈值,如果超过了这个,则系统会不响应请求,或者相应的特别慢,因此我们最好进行过载保护,防止系统被击垮。

如果你使用过Tomcat, Connector其中一 种配置中有如下几个参数。
• acceptCount: 如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接;
• maxConnections: 瞬时最大连接数,超出的会排队等待;
• maxThreads: Tomcat能启动用来处理请求的最大线程数,如果请求处理一直远远大于最大线程数,则会引起响应变慢甚至会僵死。

这些配置都可以在Tomcat官方文档找到,举一反三,我们也可以去应用在Mysql、Redis中去找到对应的配置。

2. 限流总资源数

总资源主要就是数据资源的访问,比如数据库连接、线程等,可能会有很多线程进来,这时候可以去优化连接池的配置去控制限流。

3.限流某个接口的总并发/请求数

如果接口可能会有突发访问情况,但又担心访问量太大造成崩溃,如抢购业务,那么这个时候就需要限制这个接口的总并发/请求数/总请求数了。

之前我总结的使用hystrix去控制线程隔离,使用线程池去限流,或者我们可以使用原子类去控制最大接口并发数或者使用Semaphore去限流。


这种方式适合对可降级业务或者需要过载保护的服务进行限流,如抢购业务,超出限额,要么让用户排队,要么告诉用户没货了,这对用户来说是可以接受的。

如果只是要控制最大请求数,使用计数器的方式,暴力控制就可以了,这种没有什么请求速率处理,平滑过渡。

4 限流某个接口的时间窗请求数

即一 个时间窗口内的请求数,如想限制某个接口/服务每秒/每分钟/每天的请求数/调用量。

如一些基础服务会被很多其他系统调用,比如商品详情页服务会调用基础商品服务调用,但是更新量比较大有可能将基础服务打挂。这时,我们要对每秒/每分钟的调用量进行限速,
这种方式我还没实现过,可以贴出来大家参考下:


使用Guava的Cache来存储计数器,过期时间设置为2秒(保证能记录l秒内的计数)。然后,我们获取当前时间戳,取秒数来作为key进行计数统计和限流,这种方式简单粗暴,但应付刚才说的场景够用了。

5.平滑限流某个接口的请求数

之前的限流方式都不能很好地应对突发请求,即瞬间请求可能都被允许,从而导致一 些问题。因此 ,在一 些场景中需要对突发请求进行整形,整形为平均速率请求处理 (比如5r/s, 则每隔 200毫秒处理一个请求 ,平滑了速率)。这个时候有两种算法满足我们的场景:令牌桶和漏桶算法。Guava框架提供了令牌桶算法实现,可直接拿来使用。还提供了一个类,可以模拟漏桶算法来使用。

我后面会专门更新一篇文章来介绍。

三、分布式限流

什么是分布式限流呢?当应用为单点应用时,只要应用进行了限流,那么应用所依赖的各种服务也都得到了保护。但线上业务出于各种原因考虑,多是分布式系统,单节点的限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。

所以我们可以一句话概括:分布式限流就是保证分布式节点的分布式环境下多个节点的限流。

分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使用Redis+Lua或者Nginx+Lua技术进行实现,通过这两种技术可以实现高并发和高性能。

四、接入层限流

接入层通常指请求流量的入口,该层的主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、降级、限流、A/B测试、服务质量监控等。一般我们理解的就是nginx做控制。

对于Nginx接入层限流可以使用Nginx自带的两个模块:连接数限流模块ngx _ http limit conn_ module和漏桶算法实现的请求限流模块ngx_http limit_req module。还可以使用OpenResty提供的Lua限流模块lua-resty-limit-traffic应对更复杂的限流场景。

limit_conn用来对某个key对应的总的网络连接数进行限流,可以按照如IP、域名维度进行限流。

limit_req用来对某个key对应的请求的平均速率进行限流,有两种用法:平滑模式(delay)和允许突发模式(nodelay)。

可以看下两种用法的使用:

一、ngx_http_limit_conn_module

limit_conn是对某个key对应的总的网络连接数进行限流。可以按照IP来限制IP维度的总连接数,或者按照服务域名来限制某个域名的总连接数。但是,记住不是每个请求连接都会被计数器统计,只有那些被Nginx处理的且已经读取了整个请求头的请求连接才会被计数器统计。

具体配置如下,在nginx的配置文件中做如下配置:


• limit_conn: 要配置存放key和计数器的共享内存区域和指定key的最大连接数。此处指定的最大连接数是1, 表示Nginx最多同时并发处理l个连接。
• limit_conn_zone: 用来配置限流key及存放key对应信息的共享内存区域大小。此处的key是"server_name作为key来限制域名级别的最大连接数。
• limit_conn_status: 配置被限流后返回的状态码,默认返回503。
• limit_ conn _log_level: 配置记录被限流后的日志级别,默认error级别。

limt_conn可以限流某个key的总并发/请求数,key可以根据需要变化。这个key可以使用IP,也可以使用域名维度去进行限流。

使用方式如下:
limit—conn_zone $binary_remote—addr zone= perip :10m; 根据ip
在要限流的location中添加限流逻辑:
location /limit {
limit—conn perip 2;
echo "123";
}

定义域名维度的限流区域,根据域名
limit—conn_zone $server_name zone= perserver:10m;
在要限流的location中添加限流逻辑:
location /limit {
limit—conn perserver 2;
echo "123";
}
即允许每个域名最大并发请求连接数为2。这样配置可以实现服务器最大连接数限制。

二、ngx_http_limit_req_module

limit_req是漏桶算法实现,用于对指定key对应的请求进行限流,比如,按照IP维度限制请求速率。配置示例如下:



• limit_req: 配置限流区域、桶容量(突发容量,默认为0)、是否延迟模式(默认延迟)。
• limit_req_zone: 配置限流key、存放key对应信息的共享内存区域大小、固定请求速率。此处指定的key是"$binary—remote_addr ", 表示IP地址。固定请求速率使用rate参数配置,支持lOr/s和60r/m, 即每秒10个请求和每分钟60个请求。不过,最终都会转换为每秒的固定请求速率(1Oris为每100毫秒处理一 个请求, 60r/m为每1000毫秒处理一 个请求)。
• limit_conn_status: 配置被限流后返回的状态码,默认返回503。
• limit_conn_log_level: 配置记录被限流后的日志级别,默认级别为error。

limit_req的主要执行过程如下:

(1)请求进入后首先判断最后一 次请求时间相对当前时间 (第一次是0) 是否需要限流,如果需要限流,则执行步骤2, 否则执行步骤3。

(2)如果没有配置桶容量(burst), 则桶容量为 o,按照固定速率处理请求。如果请求被限流,则直接返回相应的错误码(默认为503)。
如果配置了桶容量(burst>O)及延迟模式(没有配置nodelay)。如果桶满了,则新进入的请求被限流。如果没有满,则请求会以固定平均速率被处理(按照固定速率并根据需要延迟处理请求,延迟使用休眠实现)。

如果配置了桶容量(burst>O)及非延迟模式(配置了nodelay), 则不会按照固定速率处理请求,而是允许突发处理请求。如果桶满了,则请求被限流,直接返回相应的错误码。

(3) 如果没有被限流,则正常处理请求。

(4) Nginx会在相应时机选择一 些 ( 3 个节点)限流key 进行过期处理,进行内存回收。

暂时限流的内容就总结到这里,后续会更新一些更具体的实现,包括限流算法、应用级限流和分布式限流的实现,这些毕竟零散,要怎么综合在一起也不方便,我会尽量总结。

参考:
《亿级流量网站架构核心技术》
分布式服务限流实战
漏桶、令牌桶算法原理与简单实现

你可能感兴趣的:(开发高并发系统之限流设计最全总结)