上文说到,性能是高并发的目标之一。
追求性能没有错,但并非永无止境
。想要提升性能,势必投入成本,不过它们并不是一直成正比,而是随着成本不断增加,性能提升幅度逐渐衰减,甚至可能不再提升。所以,有时间我们要懂得适可而止。
思考一下,追求性能是为了解决什么问题,至少有一点,是为
了让应用系统能够应对突发请求
。换言之,如果能解决这个问题,是不是也算实现了高并发的目标。
而有时候,我们在解决问题时,不要总是习惯做加法,还可以尝试做减法,架构设计同样如此。那么,如何通过做减法的方式,来解决应对突发请求的问题呢,让我们来讲讲限制。
限制,从狭义上可以理解为是一种约束或控制能力。在软件领域中,它可以针对功能性或非功能性,而在高并发的场景中,它更偏向于非功能性。
限制应用系统的处理能力,并不代表要降低应用系统的处理能力,而是
通过某些控制手段,让突发请求能够被平滑地处理
,同时起到应用系统的保护能力,避免瘫痪,还能将应用系统的资源进行合理分配,避免浪费。
那么,到底有哪些控制手段,既能实现以上这些能力,又能减少对客户体验上的影响,下面就来介绍几种常用的控制手段。
第一招:限流
限流,是在一个时间窗口内,对请求进行速率控制。若请求达到提前设定的阈值时,则对请求进行排队或拒绝。常用的限流算法有两种:漏桶算法和令牌桶算法。
漏桶算法,所有请求先进入漏桶,然后按照一个恒定的速率对漏桶里的请求进行处理,是一种控制处理速率的限流方式,用于平滑突发请求速率。
它的优点是,
能够确保资源不会瞬间耗尽
,避免请求处理发生阻塞现象,另外,还能够保护被应用系统所调用的外部服务,也免受突发请求的冲击。
它的缺点是,
对于突发请求仍然会以一个恒定的速率来进行处理
,其灵活性会较弱一点,容易发生突发请求超过漏桶的容量,导致后续请求直接被丢弃。
令牌桶算法,应用系统会以一个恒定的速率往桶里放入令牌,请求处理前,会从桶里获取令牌,当桶里没有令牌可取时,则拒绝服务,是一种平均流入速率的限流方式。
它的优点是,
在限制平均流入速率的同时,还能在面对突发请求的情况下,确保资源被充分利用
,不会被闲置或浪费。
它的缺点是,
舍弃了处理速率的强控制能力
,那么如果某些功能依赖外部服务,可能将会让外部服务无法承受压力,导致无法正常返回,而且还浪费了这次获取的令牌。
综上,两种算法并没有绝对的好坏,而是需要根据实际的情况,选择合适的方式,从而在发挥限流作用的同时不会引发其他问题。但在一些秒杀活动中,软件党的高频请求,会很容易触发限流,导致大量正常请求被误杀的问题。
虽然在请求被限流后,会返回友好话术,减轻对客户体验的影响,但也有可能他们的请求,会一直无法得到有效处理,这时候耐心再好的客户也会离开及抱怨。
所以,我们除了使用限流这招外,还得搭配其他的招数组合一起使用,从而让应用系统能够对资源进行合理分配,避免资源浪费,减少正常请求被误杀的情况。
第二招:降频
降频,是在一个时间窗口内,对同一特征的请求进行速率控制。若请求达到提前设定的阈值时,则会对请求进行拒绝。
虽然和限流有点类似,但存在着细微的差别。对限流而言,它并不关心请求方,而只对服务端的速率进行控制,而对降频而言,它会
基于某种特征,对请求方的请求速率进行控制
。
而降频的目的,是为了减少应用系统资源被不正常的请求所消耗,而导致正常的请求因限流被拒绝的情况发生。它的实现方式也有多种,而且在前端和后端都可以使用。
识别不正常的请求是降频的第一步,也是最关键的一步。一般会制定某种特征+某段时间+请求数量这种三段式的识别规则。
特征可以是账号、会话、IP地址、设备号等,时间一般会是1秒,也可以设置更长。账号+1秒+5笔,意思就是同一个账号在1秒内可以发生5笔请求,但是这里请求数量与限流的设定参考依据不同。
限流大小主要依据性能来决定,而降频中的请求数量,一般会以正常人的交互速率作为参考。所以,并不能因为性能好,就设定账号+1秒+100笔这种识别规则,这不但不科学还会浪费资源。
接下来,有了识别规则还得搭配对应的处置手段,常见的有两种模式:挑战和拒绝。
挑战弹出验证码,输入并验证通过后,可以继续请求仅适用于前端拒绝弹出“请求频繁”提示,且这笔请求将直接被拒绝适用于前端及后端
限流会发生误杀,难道降频就不会吗,其实也会发生,特别是用户的网络环境是一个出口IP地址时。所以,如果是基于IP地址特征的识别规则,请求数量建议适当放大。
在降频策略方面,建议配置多层+渐进式的方式
,识别规则较为严格的采用挑战模式,识别规则较为宽松的采用拒绝模式,减少因降频而引发的误杀情况,参考如下:
优先级
|
识别规则
|
处置手段
|
1
|
账号+1秒+5笔
|
挑战
|
2
|
账号+1秒+10笔
|
拒绝
|
3
|
IP地址+1秒+20笔
|
挑战
|
4
|
IP地址+1秒+40笔
|
拒绝
|
降频确实可以使应用系统的资源,被合理地分配给请求方,但并不能保证万无一失,特别对于那些技术高超的软件党们,他们仍然可以通过其他方式绕开这种控制手段。
不过,你可以将此视为一种攻防战,通过增强防守的方式,来提高攻击者成本,而攻击者一定会权衡成本和收益,当成本大于收益时,可能就不会有攻击,毕竟没有人会这么无聊透顶。
虽然有了限流和降频这两招,但仍可能无法应对高并发的场景,况且在初期,限流和降频的策略,也无法设计得非常完美。所以,有些时候还得使出最后一招。
第三招:降级
降级,是当应用系统处理超载时,对其服务进行裁剪的一种机制。常见的是应用系统处理阻塞时,会关闭非核心服务,并将资源给到核心服务,从而确保核心服务正常。
经常有人将它与熔断混为一谈,但并非一回事。
降级主要是针对应用系统本身
,若处理能力不足则可触发,而熔断主要是针对应用系统所调用的外部服务,若外部服务不稳定时则可触发。
当然,两者也有一定的关系,因为当发生熔断时,也可以触发降级机制,比如当同步调用外部服务出现性能问题时,可以降级为异步调用,避免造成线程阻塞而瘫痪。
不过在降级前,必须得先梳理应用系统中的核心服务
,可以采用经典的二八原则,将服务划分为20%核心服务+80%非核心服务。而这种分法的意图,是希望让你找到真正重要的核心服务,不然,你会觉得都很重要。
在梳理过程中,建议
通过多个维度来进行综合评判,如下是我经常采用的一种梳理方法,你可以将此作为一种参考,并结合自己的服务分类标准进行调整。
首先,可以设计一张类似如下的矩阵图,请尽量地简约它,将应用系统中的各类服务,按照矩阵所设定的不同属性进行分门别类。
|
操作类
|
查询类
|
业务类
|
订单下单
订单支付
订单退货
...
|
商品查询
订单查询
退货进度
...
|
基础类
|
用户登录
用户登出
密码修改
头像修改
...
|
用户查询
历史浏览
我的收藏
我的分享
...
|
然后,将业务类+操作类的挑选出来作为核心服务,你会不会认为这就结束了。不好意思,游戏才刚刚开始。不过你可以试想一下,假设仅保留这些核心服务,会出现什么问题。
用户登录不了无法订单支付,订单查询不了无法订单退货。所以,我们还需引入服务关键路径的概念,可以理解为在使用某个服务前,还必须要使用的其他服务。
分别对挑选出来的核心服务,进行服务关键路径的梳理。
待服务关键路径梳理完成后,再对路径上的所有服务进行合并及去重,将会得到一组新的核心服务:用户登录/商品查询/订单下单/订单支付/订单查询/订单退货。
来计算下核心服务的占比,所有服务14个/核心服务6个,占42.86%,远远超过了20%。所以,建议继续从这些核心服务中,识别更核心的部分,但仍然以服务关键路径为整体。
相比订单下单/订单支付/订单退货这三条服务关键路径,我想订单支付可能会更有价值。最后,我们可以仅将订单支付这条服务关键路径上的服务作为核心服务。
重新再来计算下核心服务的占比,所有服务14个/核心服务4个,占28.57%,虽然还是超过了20%,但这并不是重点,重点是我们已经找到了最核心的服务。
其余的核心服务,可以降级为准核心服务,重组后得到如下这份服务重要程度清单。
核心服务
|
准核心服务
|
非核心服务
|
用户登录
商品查询
订单下单
订单支付
|
订单查询
订单退货
|
退货进度
用户登出
密码修改
头像修改
用户查询
历史浏览
我的收藏
我的分享
|
当拥有这份清单后,若应用系统处理阻塞时,就可以按照非核心服务>准核心服务>核心服务这个顺序依次进行降级。不过,降级不一定要拒绝请求,也可以是限流请求,这样可以减少对服务能力的裁剪力度。
以上只是一种相对较粗的降级策略,如果你想要制定更精细化的降级策略,还需要对每个服务进行优先级的设定,高低依据可以结合自身需要来制定,例如:历史服
务使用情况。
当有了限流、降频、降级这三招,基本就能够在资源有限的情况下,让突发请求能够被平滑地处理,将应用系统的资源能够被合理地分配,以及当应用系统处理堵塞时,确保核心服务正常。