当前位置: 首页 > 后端技术 > Java

看看阿里程序员是怎么谈限流的

时间:2023-04-01 21:53:59 Java

有读者说他们参加秋招时准备的项目就是秒杀系统。在Redis和MySQL的设计上他准备了很多,但是每次面试的时候都是先问面试官他是怎么限制流量的。他对电流的限制毫无准备,他的回答非常杂乱无章。他在采访开始时感到恐慌。其实在实际的秒杀系统中,限流是很重要的,所以面试官也特别注意这方面。今天看到一篇很系统的限流文章,一起来学习一下。为什么要限行在日常生活中,哪些地方需要限行?比如我旁边有个国家级风景名胜区。平时可能没什么人去,一到5月1日或者春节就会人满为患。这个时候,景区管理人员会实行一系列的政策,限制人流进入,为什么要限流呢?如果景区能容纳1万人,现在进了3万人,人山人海,搞不好会出事故。结果大家的体验都不好。如果发生意外,景区可能不得不关闭,无法对外开放。这样做的后果就是大家都觉得体验很糟糕。限流的思路是在保证可用性的情况下,尽可能增加进入的人数,剩下的人在外面排队等候,保证里面的一万人可以正常玩。回到互联网,也是如此。比如某明星公布恋情后,访问量从平时的50万增加到500万。系统最多可以支持200万次访问,所以必须要实现限流规则,保证是可用状态,这样才不会导致服务器崩溃,所有请求都不可用。限流的思路对系统服务进行限流,一般有以下几种方式:熔断系统在设计之初就考虑了熔断措施。当系统出现问题时,如果短时间内无法修复,系统必须自动做出判断,打开熔断开关,拒绝流量访问,避免大流量请求导致后台过载。系统还应该能够动态监控后端程序的修复状态。当程序恢复稳定后,可以关闭熔断器开关,恢复正常服务。常见的熔断器组件有Hystrix和阿里的Sentinel,两者各有优缺点,可根据业务实际情况选择。服务降级对系统的所有功能服务进行分类。当系统出现问题,需要紧急限流时,可以将不太重要的功能降级,停止服务,为核心功能释放更多资源。.比如在电商平台,如果流量突然暴增,可以暂时降级商品评论、积分等非核心功能,停止这些服务,释放机器、CPU等资源确保用户可以正常下单。这些降级后的功能服务可以等待整个系统恢复正常,然后启动它进行补货/补偿处理。除了功能降级之外,还可以采用不直接操作数据库,而是全部读取缓存和写入缓存的方式作为临时降级方案。延迟处理的模式需要在系统前端建立一个流量缓冲池,将所有请求缓冲到这个池中,而不是立即处理。然后后端真正的业务处理程序从这个池中获取请求并顺序处理。通常,可以使用队列模式来实现它们。这相当于使用了异步的方式来减轻后端的处理压力,但是当流量较大时,后端的处理能力是有限的,缓冲池中的请求可能无法及时处理,就会出现得到一定程度的延迟。下面具体的漏桶算法和令牌桶算法就是这个思路。这种权限处理方式需要对用户进行分类。通过预设的分类,让系统优先处理对安全性要求高的用户群,其他用户群的请求将被延迟或直接不处理。缓存、降级、限流用于提高系统吞吐量,提高访问速度,提供高并发。降级是指当系统部分服务组件不可用、流量激增、资源枯竭等情况时,暂时屏蔽已经掉出问题的服务,继续提供降级服务,尽可能给用户友好的提醒,以及返回底部数据。它不会影响整个业务流程。问题解决后,服务将在问题解决后再次受限。指的是缓存和降级失效的场景。例如,当达到阈值时,限制接口调用频率、访问次数、库存数量等,在服务不可用之前提前降级服务。只服务好部分用户。限流算法限流算法有很多,常见的有三种,分别是计数器算法、漏桶算法和令牌桶算法,下面会一一说明。计数器算法简单粗暴,比如指定线程池大小,指定数据库连接池大小,nginx连接数等,都属于计数器算法。计数器算法是限流算法中最简单易行的算法。举个例子,比如我们规定对于A接口,一分钟的访问次数不能超过100次。那么我们可以这样做:一开始我们可以设置一个counter计数器,每有一个请求过来,计数器会加1,如果计数器的值大于100并且请求与第一次请求的间隔还在1分钟以内,说明请求过多,拒绝访问;如果请求和第一次请求的间隔大于1分钟,并且计数器的值还在限流范围内,那么就重新设置计数器,就这样简单粗暴。漏桶算法漏桶算法的思想很简单。水(请求)先进入漏桶,漏桶以一定的速度放水。当进水速度过大,超过水桶所能容纳的容量时,直接溢出。可以看出,漏桶算法可以对数据传输速率施加限制。这样做的好处是:削峰:当大量流量进入时,会发生溢出,这样可以缓冲限流保护服务:不会直接向服务器请求,缓冲压力的消耗速度为fixed因为计算性能是固定的。令牌桶算法tokenbucket与漏桶类似,不同的是令牌桶中放置了一些令牌。服务请求到达后,只有获取token后才会获取服务。比如我们平时去食堂吃饭的时候,在食堂的窗口前面排队的人,这就好像是漏桶算法。大量的人以一定的速度聚集在食堂的窗外享受服务。食堂在外面,享受不到食堂的服务,这叫人满为患。Overfill可以继续请求,也就是继续排队,那有什么问题呢?如果此时有特殊情况,比如有些志愿比较急,或者高三马上就要高考了。这种情况是紧急情况。如果你也用漏桶算法,就得慢慢排队。这并不能解决我们的需求。对于很多应用场景来说,除了要求数据可以被限制在平均传输速率之外,还需要允许一定程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更合适。如图所示,令牌桶算法的原理是系统会以恒定的速度往桶中放入令牌,如果需要处理请求,需要先从桶中取出一个令牌,当有桶中没有令牌当卡可用时,服务被拒绝。令牌桶的好处是,如果访问量激增或者某个时刻出现紧急情况,可以通过改变桶中令牌的数量来改变连接数,就像排队等候的问题在食堂吃饭。如果现在不直接去窗口排队,而是先到楼外拿饭票再去排队,所以有高三的时候可以增加饭票的数量或者先给高三学生token,比漏桶算法灵活多了。并发限流简单来说就是设置系统的QPS阈值总数。这些很常见。以Tomcat为例,很多参数都是基于这个考虑。比如配置的acceptCount设置响应连接数,maxConnections设置瞬时最大连接数,maxThreads设置最大线程数。在各个框架或组件中,并发限制在以下几个方面:限制总并发数(如数据库连接池、线程池)限制瞬时并发数(nginx的limit\_conn模块,使用来限制瞬时并发连接数)限制时间窗口内的平均速率(如Guava的RateLimiter,nginx的limit\_req模块,限制每秒的平均速率)其他包括限制远程接口调用的速率,限制MQ的消费速率。此外,还可以根据网络连接数、网络流量、CPU或内存负载等限制流量。有了并发限流,就意味着在处理高并发的时候多了一个保护机制,不用担心瞬时流量导致系统挂掉或者雪崩,最终损坏服务而不是没有服务;但是限流需要评估好,不能乱用,否则在一些正常的流量中会出现一些奇怪的问题,导致用户体验不好,造成用户流失。接口限流接口限流分为两部分,一是限制一段时间内的接口调用次数,参考前面限流算法的计数器算法,二是设置滑动时间窗算法.接口总数控制一段时间内被调用的接口总数。可以参考前面的计数器算法,这里不再赘述。接口timewindow固定时间窗算法(也就是上面说的counter算法)的问题是统计区间太大,限流不够准确,和之前统计区间的关系和影响不大在第二个统计区间考虑(第一个区间后半段+第二个区间前半段也是一分钟)。为了解决我们上面提到的关键问题,我们尝试将每个统计区间划分为更小的统计区间和更准确的统计计数。上面的例子,假设QPS可以接受100个查询/秒,第一分钟前40秒访问量很低,后面20秒突然增加,持续一段时间,并没有直到第二分钟第40秒开始下降,按照之前的计数方式,前一秒的QPS是94,下一秒的QPS是92,所以没有超过设定的参数,但是!但是在中间区域,QPS达到了142,明显超过了我们允许的服务请求数,所以固定窗口计数器不太靠谱,需要滑动窗口计数器。计数器算法其实是固定窗口算法,只是没有进一步划分时间窗口,所以只有一个格子;可以看出,当滑动窗口的网格划分得越多,即秒级精确到毫秒或纳秒,那么滑动窗口的滚动越平滑,限流统计就越准确。需要注意的是,占用的空间越大。这部分限流实现是限流的具体实现。简单的说,毕竟没人愿意看长代码。Guava实现导入包com.google.guavaguava28.1-jre核心代码LoadingCachecounter=CacheBuilder.newBuilder().expireAfterWrite(2,TimeUnit.SECONDS).build(newCacheLoader(){@OverridepublicAtomicLongload(Longsecend)throwsException{//TODO自动生成的方法存根returnnewAtomicLong(0);}});counter.get(1l).incrementAndGet();(SmoothBursty:Token生成速度恒定)publicstaticvoidmain(String[]args){//RateLimiter.create(2)每秒生成的token数量RateLimiterlimiter=RateLimiter.create(2);//limiter.acquire()阻塞方式获取tokenSystem.out.println(limiter.acquire());尝试{Thread.sleep(2000);}catch(InterruptedExceptione){//TODO自动生成的catch块e.printStackTrace();}System.out.println(limiter.acquire());;System.out.println(limiter.acquire());System.out.println(limiter.acquire());.out.println(limiter.acquire());;System.out.println(limiter.acquire());;System.out.println(limiter.acquire());}\`RateLimiter.create(2)容量和突发量,令牌桶算法让一段时间内没有被消费的令牌暂时存放在令牌桶中,供突发消费Progressivemode(SmoothWarmingUp:Token生成速度缓慢增加,直到保持稳定值)//平滑限流,从冷启动率(full)到平均消耗率的时间间隔RateLimiterlimiter=RateLimiter.create(2,1000l,TimeUnit.毫秒);System.out.println(limiter.acquire());;尝试{Thread.sleep(2000);}catch(InterruptedExceptione){//TODO自动生成的catch块e.printStackTrace();}System.out.println(limiter.acquire());System.out.println(limiter.acquire());System.out.println(limiter.acquire());System.out.println(limiter.acquire());;System.out.println(limiter.acquire());;System.out.println(limiter.acquire());;超时布尔tryAcquire=limiter.tryAcquire(Duration.ofMillis(11));超时时间内是否能拿到token,异步执行分布式系统限流Nginx+Lua实现可以使用resty.lock保持原子特性,请求之间不会发生锁重入https://github.com/openresty/...uselua\_shared\_dict存储数据locallocks=require"resty.lock"localfunctionacquire()locallock=locks:new("locks")localelapsed,err=lock:lock("limit_key")--互斥保证原子属性本地limit_counter=ngx.shared.limit_counter--counterlocalkey="ip:"..os.time()locallimit=5--limitsizelocalcurrent=limit_counter:get(key)ifcurrent~=nilandcurrent+1>limitthen--如果thecurrentlimitsizeisexceededlock:unlock()return0endifcurrent==nilthenlimit_counter:set(key,1,1)--第一次需要设置过期时间,设置key的值为1,--expirationtime1secondelselimit_counter:incr(key,1)--第二次加1endlock:unlock()return1endngx.print(acquire())