当前位置: 首页 > 科技观察

慌了,面试居然问高并发系统怎么限流?

时间:2023-03-22 01:47:06 科技观察

缓存很容易理解。在大型高并发系统中,如果没有缓存,数据库分分钟炸掉,系统瞬间瘫痪。使用缓存不仅可以提高系统访问速度,增加并发访问量,而且是保护数据库和系统的有效途径。大型网站一般以“读”为主,缓存的使用很容易想到。在大型“写”系统中,缓存往往起着非常重要的作用。比如积累一些要批量写入的数据,内存中的缓存队列(生产和消费),以及HBase中写入数据的机制等,也都是利用缓存来提高系统的吞吐量或者实现系统保护措施。即使是消息中间件,你也可以把它看成是一个分布式的数据缓存。Downgrading服务降级是指当服务器压力急剧增加时,根据当前业务情况和流量,对部分服务和页面进行策略性降级,以释放服务器资源,保证核心任务的正常运行。降级往往会指定不同的级别,面对不同的异常级别进行不同的处理。按服务方式分:可以拒绝服务,可以延迟服务,有时也可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉一些模块。总之,服务降级需要根据不同的业务需求采用不同的降级策略。主要目的是虽然服务受损了,但聊胜于无。限流限流可以被认为是一种服务降级。限流就是限制系统的输入输出流量,达到保护系统的目的。一般来说,系统的吞吐量是可以衡量的。为了保证系统的稳定运行,一旦达到需要限流的阈值,就需要进行限流,并采取一些措施来完成限流的目的。例如:延迟处理、拒绝处理或部分拒绝处理等。限流算法常见的限流算法有:计数器、漏桶和令牌桶算法。CounterCounter是最简单粗暴的算法。例如,一个服务每秒最多只能处理100个请求。我们可以设置一个1秒的滑动窗口。窗口中有10个格子,每个格子为100毫秒,每100毫秒移动一次。每次移动都需要记录当前服务请求的数量。10次??的次数需要保存在内存中。可以用数据结构LinkedList来实现。每次网格移动时判断当前访问次数与LinkedList中上次访问次数的差值是否超过100,如果超过则需要限流。显然,滑动窗口的网格划分的越多,滑动窗口的滚动就会越流畅,限流的统计也会更加准确。示例代码如下://服务访问次数可以放在Redis中实现分布式系统的访问计数。longcounter=0L;//用LinkedList记录滑动窗口的10个格子。LinkedListll=newLinkedList();publicstaticvoidmain(String[]args){Countercounter=newCounter();counter.doCheck();}privatevoiddoCheck(){while(true){ll.addLast(counter);if(ll.size()>10){ll.removeFirst();}//比较最后一个和第一个,相差一秒if((ll.peekLast()-ll.peekFirst())>100){//Tolimitrate}Thread.sleep(100);}}漏桶算法漏桶算法是一种非常常用的限流算法,可以用来实现流量整形(TrafficShaping)和流量控制(TrafficPolicing).在维基百科上贴了一张示意图帮助大家理解:漏桶算法的主要概念如下:固定容量的漏桶以恒定固定的速率流出水滴;如果桶是空的,则不需要有水滴流出;它可以以任何速度流入滴到桶中;如果流入的水滴超过桶的容量,则流入的水滴溢出(丢弃),而桶的容量保持不变。漏桶算法实现起来比较容易,在单机系统中可以使用队列来实现(.redis中的TPLDataFlow是可选的方案)令牌桶算法令牌桶算法是一个桶,用一个固定的容量,以固定的速率向桶中添加令牌,令牌桶算法基本上可以用以下概念来描述:令牌将以固定的速率放入令牌桶中,例如每秒放入10个。桶中最多存放b个令牌,当桶满时,新加入的令牌将被丢弃或拒绝。当一个大小为n字节的数据包到达时,从桶中取出n个令牌,并将数据包发送出去网络。如果桶中的令牌少于n个,令牌将不会被删除,数据包将被节流(丢弃,或缓冲等待)。如下图所示:令牌算法是根据令牌释放的速率来控制输出速率,也就是上图中的tonetwork的速率。对于网络,我们可以理解为一个消息处理器,执行某个业务或者调用某个RPC。漏桶和令牌桶的比较令牌桶可以在运行时控制和调整数据处理的速率,处理某个时刻的突发流量。提高令牌的发放频率可以提高整体数据处理的速度,通过增加每次获得令牌的数量或者减慢令牌的发放速度,降低整体的数据处理速度。漏桶不好,因为它的流出速度是固定的,程序处理速度也是固定的。更多算法相关:算法聚合总体来说令牌桶算法比较好,但是实现比较复杂。限流算法实现GuavaGuava是谷歌的一个开源项目,包含了谷歌Java项目广泛依赖的几个核心库。其中,RateLimiter提供令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。1.RegularRate:创建一个ratelimiter,设置每秒放置的token数量:2.返回的RateLimiter对象可以保证1秒内不会给出超过2个token,并且是固定速率放置的。达到平滑输出的效果publicvoidtest(){/***创建一个限流器,设置每秒放置令牌数:2。速率为每秒2条消息。*返回的RateLimiter对象可以保证1秒内不超过2个token,固定费率投放。达到平滑输出的效果*/RateLimiterr=RateLimiter.create(2);while(true){/***acquire()获取一个token,返回获取这个token所需的时间。如果桶中没有令牌,则等待直到有令牌。*acquire(N)可以获取多个token。*/System.out.println(r.acquire());}}上面代码执行的结果如下图所示,基本上是0.5秒一个数据。只有拿到token后才能对数据进行处理,从而达到输出数据或者调用接口的流畅效果。acquire()的返回值是等待令牌的时间。如果需要处理一些突发流量,可以给这个返回值设置一个阈值,根据不同的情况进行处理,比如过期就丢弃。2.突发流量:突发流量可以更突发或更不突发。首先,让我们看一下突然乘法的例子。还是上面例子的流程,每秒2个datatoken。以下代码使用acquire方法指定参数。System.out.println(r.acquire(2));System.out.println(r.acquire(1));System.out.println(r.acquire(1));System.out.println(r.acquire(1))获得(1));获得类似于以下内容的输出。如果你想一次处理更多的数据,你需要更多的令牌。代码先获取2个token,然后0.5秒后没有获取到下一个token,或者1秒后,又恢复正常速度。这是一个有很多突发的例子。如果突发没有流量,则执行以下代码:System.out.println(r.acquire(1));Thread.sleep(2000);System.out.println(r.acquire(1));System.out.println(r.acquire(1));System.out.println(r.acquire(1));得到类似的结果如下:等待两秒后,令牌桶中累计有3个令牌,可以不花时间连续获得。处理突发其实就是单位时间内恒定的输出。这两种方法都使用RateLimiter子类SmoothBursty。另一个子类是SmoothWarmingUp,提供缓冲流输出方案。/***创建一个限流器,设置每秒放置令牌的数量:2。速率为每秒210条消息。*返回的RateLimiter对象可以保证1秒内不超过2个token,固定费率投放。达到平滑输出的效果*设置缓冲时间为3秒*/RateLimiterr=RateLimiter.create(2,3,TimeUnit.SECONDS);while(true){/***acquire()获取一个token,并返回本次获取此令牌所需的时间。如果桶中没有令牌,则等待直到有令牌。*acquire(N)可以获取多个token。*/System.out.println(r.acquire(1));System.out.println(r.acquire(1));System.out.println(r.acquire(1));System.out.println(r.acquire(1));}输出结果如下图所示。由于缓冲时间设置为3秒,令牌桶一开始不会在0.5秒内给出消息,而是会形成一个平滑的线性下降斜率。3秒内达到原设定频率,然后以固定频率输出。图中红圈的3次加起来正好是3秒。该功能适用??于系统刚启动时需要一点时间“预热”的场景。Nginx可以使用Nginx自带的两个模块进行Nginx接入层限流:连接数限流模块ngx_http_limit_conn_module漏桶算法实现的请求限流模块ngx_http_limit_req_module1。ngx_http_limit_conn_module这种情况我们经常遇到,服务器流量异常,负载过大等等。对于大容量的恶意攻击访问,会造成带宽浪费,服务器压力,影响业务。通常认为是限制同一??IP的连接数和并发数。ngx_http_limit_conn_module模块来实现这个需求。该模块可以根据定义的key限制每个key值的连接数,就像一个IP源的连接数一样。并非所有连接都被该模块计算在内,只有那些正在处理请求的连接(其标头已被完全读入)。我们可以在nginx_conf的http{}中加入如下配置来实现限制:#限制每个用户的并发连接数,命名为onelimit_conn_zone$binary_remote_addrzone=one:10m;#配置限流后的日志级别,默认错误级别limit_conn_log_levelerror;#配置限流后返回的状态码,默认返回503limit_conn_status503;然后在server{}中加入如下代码:#限制并发用户连接数为1limit_connone1;然后我们用ab测试模拟并发请求:ab-n5-c5http://10.23.22.239/index.html得到如下结果,很明显是并发受限,超过阈值就显示503:在另外刚才配置了单个IP的并发限制,也可以设置域名的并发限制,配置和客户端IP类似。#http{}段配置limit_conn_zone$server_namezone=perserver:10m;#server{}部分配置limit_connperserver1;2.ngx_http_limit_req_module上面我们使用了ngx_http_limit_conn_module模块来限制连接数。那么如何限制请求的数量呢?这个需要通过ngx_http_limit_req_module模块来实现,可以通过定义的key值来限制请求处理的频率。特别是,可以限制来自单个IP地址的请求的处理频率。限制的方法是使用漏斗算法固定每秒处理请求的数量,推迟太多的请求。如果请求的频率超过限制字段的配置值,请求处理将被延迟或丢弃,因此所有请求都按照定义的频率进行处理。在http{}中配置#区域名称为一个,大小为10m,平均处理请求频率不能超过每秒一次。limit_req_zone$binary_remote_addrzone=one:10mrate=1r/s;configureinserver{}#设置每个IP桶的数量为5limit_reqzone=oneburst=5;上面的设置定义了每个IP的请求处理只能限制在每秒1个。并且服务器可以为每个IP缓存5个请求,如果操作了5个请求,该请求将被丢弃。使用ab测试模拟客户端连续访问10次:ab-n10-c10http://10.23.22.239/index.html如下图,链接数设置为5,一共请求10次,第一个请求被立即处理。数字2-6存储在桶中。由于桶满了,没有设置nodelay,所以剩下的4个请求被丢弃。