服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - 编程技术 - 高并发是一种架构思维模式

高并发是一种架构思维模式

2022-03-01 22:09技术奇妙物语陈俊 编程技术

本文从浅到深依次讲述了性能是实现高并发的基础条件,控制是实现资源最大化利用的方式,以及如何通过取舍来换取当前应用系统更所需的能力,但这些仅仅只是高并发世界里的一个角落。

高并发是一种架构思维模式

什么是高并发,从字面上理解,就是在某一时刻产生大量的请求,那么多少量称为大量,业界并没有标准的衡量范围。 原因非常简单,不同的业务处理复杂度不一样

而我所理解的 高并发, 它并不只是一个数字,而更是一种架构思维模式 ,它让你在面对不同的复杂情况下,从容地选择不同的技术手段,来提升应用系统的 处理能力。

但是,并不意味应用系统从诞生的那一刻,就需要具备强大的处理能力,这种做法并不提倡。要知道, 脱离实际情况的技术,会显得毫无价值,甚至是一种浪费的表现

言归正传,那高并发到底是一种怎样的架构思维模式,它对架构设计又有什么影响,以及如何通过它来驱动架构演进,让我们接着往下读,慢慢去体会这其中的精髓。

性能是一种基础

在架构设计的过程中,思考固然重要,但目标更为关键。 通过目标的牵引力,可以始终确保推进方向,不会脱离成功的轨道 那高并发的目标是什么,估计你的第一反应就是性能。

没错, 性能是高并发的目标之一,它不可或缺,但并不代表所有 。而我将它视为是高并发的一种基础能力,它的能力高低将会直接影响到其他能力的取舍。例如:服务可用性,数据一致性等。

性能在软件研发过程中无处不在,不管是在非功能性需求中,还是在性能测试报告中,都能见到它的身影。那么如何来衡量它的高低呢,先来看看常用的性能指标。

每秒处理事务数(TPS)

每秒能够处理的事务数,其中T(Transactions)可以定义不同的含义,它可以是完整的一笔业务,也可以是单个的接口请求。

每秒请求数(RPS)

每秒请求数量,也可以叫做QPS,但它与TPS有所不同,前者注重请求能力,后者注重处理能力。不过,若所有请求都在得到响应后再次发起,那么RPS基本等于TPS。

响应时长(RT)

从发出请求到得到响应的耗时,一般可以采用毫秒单位来表示,而在一些对RT比较敏感的业务场景下,可以使用精度更高的微秒来表示。

并发用户数(VU)

同时请求的用户数,很多人将它与并发数画上等号, 但两者稍有不同,前者关注客户端,后者关注服务端,除非 每个 用户仅发送一笔请求,且请求从 客户端到服务端 没有延迟,同时服务端有足够的处 理线程。

以上都是些常用的性能指标,基本可以覆盖80%以上的性能衡量要求。但千万不要以单个指标的高低来衡量性能。比如:订单查询TPS=100万就认为性能很高,但RT=10秒。

这显然毫无意义。因此, 建议同时观察多个指标的方式来衡量性能的高低,大多数情况下主要会关注TPS和RT,而你可以 将TPS视为一种水平能力,注重并行处理能力, 将RT视为一种垂直能力,注重单笔处理能力, 两者缺一不可

接触过性能测试的同学,可能会见过如下这种性能测试结果图,图中包含了刚才提到过的三个性能指标,其中横坐标为VU,纵坐标分别为TPS和RT。

高并发是一种架构思维模式

注:表格中的数据都是理想情况下的,实际上会有上下抖动,而这里只是为了想用它来解释一种现象而已。

图中的两条曲线, 在不断增加VU的情况下,TPS不断上升,但RT保持稳定,但当VU增加到一定量级的时候,TPS开始趋于稳定,而RT不断上升。

如果你仔细观察,还会发现一个奇妙的地方,当RT=25ms时,它们三者存在着某种关系,即:TPS=VU/RT。但当RT>25ms时,这种关系似乎被打破了,这里暂时先卖个关子,稍后再说。

根据表格中的数据,性能测试报告结论: 最大TPS=65000,当 RT=25ms(最短)时,最大可承受VU=1500

感觉有点不对劲,用刚才的公式来验证一下,1500/0.025s=60000,但最大却是TPS=65000。那是因为,当VU=1500时,应用系统的使用资源还有空间。

再来观察一下表格中的数据,VU从1500增加到1750时,TPS继续上升,且到了最大值65000。此时,你是不是会理解为当VU增加到1750时,使用资源被耗尽了。话虽没错,但不严谨。

注:使用资源不一定是指硬件资源,也可能是其他方面,例如:应用系统设置的最大处理线程。

其实在VU增加到1750前,使用资源就已饱和,那如何来测算VU的临界值呢。你可以将最大TPS作为已知条件,即:VU=TPS*RT,65000*0.025s=1625。也就是说,当VU=1625时,使用资源将出现瓶颈。

调整性能测试报告 结论: 最大TPS=65000, 当RT=25ms(最短)时 ,最大可承受VU=1625

有人会问,表格中的RT是不是平均值,首先回答为是。不过, 高并发场景对RT会特别敏感,所以除了要考虑RT的平均值外,建议还要考虑它的分位值 ,例如:P99。

举例:假设1000笔请求,其中900笔RT=23ms,50笔RT=36ms,50笔RT=50ms

平均值

P99值

P95值

P90值

25ms

50ms

36ms

23ms

P99的计算方式,是将1000笔请求的RT从小到大进行排序,然后取排在第99%位的数值,基于以上举例数据来进行计算,P99=50ms,其他分位值的计算方式类似。

再次调整性能测试报告结论: 最大TPS=65000,当 RT(平均)=25ms(最短)时,最大可承受VU=1625,RT(P99)=50ms,RT(P95)=36ms,RT(P90)=23ms

在非功能性需求中,你可能会看到这样的需求,性能指标要求: RT(平均) <=30。结合刚才的性能测试报告结论, RT(平均)=25ms(最短)时,最大可承受VU=1625。那就等于在RT上还有5ms的容忍时间。

既然是这样的话,那我们不妨就继续尝试增加VU,不过RT (平均) 会出现上升,但只要控制不要上升到30ms即可, 这是一种通过牺牲耗时(RT)来换取并发用户数(VU)的行为 。但请不要把它理解为每笔请求耗时都会上升5ms,这将是一个严重的误区。

RT(平均) 的增加,完全可能 由于应用系统 当前 没有足够的使用资源来处理请求所造成的,例如:处理线程。如果没有可用线程可以分配给请求时,就会将这请求先放入队列,等前面的请求处理完成并释放线程后,就可以继续处理队列中的请求了。

那也就是说,没有进入队列的请求并不会增加额外的耗时,而只有进入队列的请求会增加。那么 进入队列的请求会 增加多少耗时呢, 在理想情况下(RT恒定), 可能会 是正常处理一笔请求耗时的倍数,而倍数的大小又取决于并发请求的数量。

假设 最大处理 线程=1625,若每个用户仅发送一笔请求,且请求从 客户端到服务端没有延迟的条件下, 当并发用户数=1625时,能够保证 RT=25ms,但当 并发用户数 >1625时,因为线程只能分配给1625笔请求,那多余的请求就无法保证RT=25ms。

超过1625笔的请求会先放入队列,等前面1625笔请求处理完成后,再从队列中拿出最多1625笔请求进行下一批处理,如果队列中还有剩余请求,那就继续按照这种方式循环处理。

进入队列的请求,每等待一批就需要增加前一批的处理耗时。在理想情况下,每一批都是 RT=25ms,如果这笔请求在队列中等待了两批,那就要额外增加50ms的耗时。

VU

第一批

第二批

第三批

1000

1000笔请求

RT=25ms

   

2000

1625笔请求

RT =25ms

375笔请求

RT =50ms

 

4000

1625笔请求

RT =25ms

1625 笔请求

RT =50ms

750笔请求

RT =75ms

因此,并不能简单通过VU=TPS*RT= 65000*0.03=1950 来计算最大可承受VU。而是需要 引入一种叫做 科特尔法则(Little’s Law) 的排队模型来估算,不过由于这个法则比较复杂,这里暂时不做展开。

通过粗略估算后,VU大约在2032,我们再对这个值用上述表格中再反向验算一下。

VU

第一批

第二批

RT (平均)

2032

1625笔请求

RT =25ms

407笔请求

RT =50ms

(1625*25+407*50)/(1625+407 )≈30ms

最终 调整性能测试报告结论: 最大TPS=65000,当RT(平均)=25(最短)时,最大可承受VU=1625,RT(P99)=50,RT(P95)=36,RT(P90)=23;当RT(平均)=30(容忍)时,(理想情况)最大可承受VU=2032 RT(P99)=RT(P95)=50,RT(P90)=25

这就解释了为什么当RT>25ms时,VU=TPS*RT会不成立的原因。 不过,这些都是在理想情况下推演出来的,实际情况会比这要复杂得多。

所以,还是尽量采用多轮性能测试来得到性能指标,这样也更具备真 实性。毕竟 影响性能的因素实在大多且很难完全掌控,任何细微变化都将影响 性能指标的变化。

到这里,我们已经了解了可以用哪些 指标 来衡量性能的高低。不过,这里更想强调的是, 性能是高并发的基础能力,是实现高并发的基础条件,并且你需要有侧重性地提升不同维度的性能指标,而非仅关注某一项

限制是一种设计

上文说到,性能是高并发的目标之一。 追求性能没有错,但并非永无止境 。想要提升 性能,势必投入成本,不过它们并不是一直成正比,而是随着成本不断增加,性能提升幅度逐渐衰减,甚至可能不再提升。所以,有时间我们要懂得适可而止。

思考一下,追求性能是为了解决什么问题,至少有一点,是为了让应用系统能够应对突发请求。换言之,如果能解决这个问题,是不是也算实现了高并发的目标。

而有时候,我们 在解决问题时,不要总是习惯做加法,还可以尝试做减法 ,架构设计同样如此。那么,如何通过做减法的方式,来解决应对突发请求的问题呢。让我们来讲讲限制。

限制,从狭义上可以理解为是一种约束或控制能力 。在软件领域中,它可以针对功能性或非功能性,而在高并发的场景中,它更偏向于非功能性。

限制应用系统的处理能力,并不代表要降低应用系统的处理能力 ,而是通过某些控制手段, 让突发请求能够被平滑地处理,同时起到 应用系统的保护能力, 避免瘫痪, 还能 将应用系统的资源进行合理分配,避免浪费

那么,到底有哪些控制手段,既能实现以上这些能力, 又能减少对客户体验上的影响,下面就来介绍几种常用的控制手段

第一招:限流

限流,是在一个时间窗口内,对请求进行速率控制 。若请求达到提前设定的阈值 ,则对请求进行排队或拒绝。常用的限流算法有两种:漏桶算法和令牌桶算法。

漏桶算法,所有请求先进入漏桶,然后按照一个恒定的速率对漏桶里的请求进行处理,是一种控制处理速率的限流方式,用于平滑突发请求速率。

它的优点是,能够确保资源不会瞬间耗尽,避免请求处理发生阻塞现象,另外,还能够保护被应用系统所调用的外部服务,也免受突发请求的冲击。

它的缺点是,对于突发请求仍然会以一个恒定的速率来进行处理,其灵活性会较弱一点,容易发生突发请求超过漏桶的容量,导致后续请求直接被丢弃。

令牌桶算法,应用系统会以一个恒定的速率往桶里放入令牌,请求处理前,会从桶里获取令牌,当桶里没有令牌可取时,则拒绝服务,是一种平均流入速率的限流方式。

它的优点是,在限制平均流入速率的同时,还能在面对突发请求的情况下,确保资源被充分利用,不会被闲置或浪费。

它的缺点是,舍弃了处理速率的强控制能力,那么如果某些功能依赖外部服务,可能将会让外部服务无法承受压力,导致无法正常返回,而且还浪费了这次获取的令牌。

综上, 两种算法并没有绝对的好坏,而是需要根据实际的情况,选择合适的方式,从而在发挥限流作用的同时不会引发其他问题 但在一些秒杀活动中,软件党的高频请求,会很容易触发限流,导致大量正常请求被误杀的问题。

虽然在请求被限流后,会返回友好话术,减轻对客户体验的影响,但也有可能他们的请求,会一直无法得到有效处理,这时候耐心再好的客户也会离开及抱怨。

所以,我们 除了使用限流这招外,还得搭配其他的招数组合一起使用,从而让 应用系统能够对资源进行合理分配,避免资源浪费,减少正常请求被误杀的情况。

第二招:降频

降频,是在一个时间窗口内,对同一特征的请求进行速率控制 若请求达到提前设定的阈值时,则会对请求进行拒绝。

虽然和限流 有点类似,但存在着细微的差别。对限流而言,它并不关心请求方,而只对服务端的速率进行控制,而对降频而言,它会基于某种特征,对请求方的请求速率进行控制。

而降频的目的,是为了减少 应用系统资源被不正常的请求所消耗,而导致正常的请求因限流被拒绝的情况发生 。它的实现方式也有多种,而且在前端和后端都可以使用。

识别不正常的请求是降频的第一步,也是最关键的一步。一般会制定某种特征+某段时间+请求数量这种三段式的识别规则。

特征可以是账号、会话、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%非核心服务。 这种分法的意图,是希望让你找到真正重要的核心服务,不然,你会觉得都很重要

在梳理过程中,建议通过多个维度来进行综合评判,如下是我经常采用的一种梳理方法,你可以将此作为一种参考,并结合自己的服务分类标准进行调整。

首先,可以设计一张类似如下的矩阵图,请尽量地简约它,将应用系统中的各类服务,按照矩阵所设定的不同属性进行分门别类。

 

操作类

查询类

业务类

订单下单订单支付

订单退货...

 

商品查询订单查询退货进度...

基础类

用户登录用户登出密码修改头像修改

...

用户查询历史浏览我的收藏我的分享...

 

然后,将业务类+操作 类的挑选出来作为 核心服务 ,你会不会认为这就结束了。不好意思,游戏才刚刚开始。不过你可以试想一下,假设 仅保留这些核心服务,会出现什么问题。

用户登录不了无法订单支付,订单查询不了无法订单退货。所以, 我们还需引入服务关键路径的概念,可以理解为在使用某个服务前,还必须要使用的其他服务

分别对挑选出来的核心服务,进行服务关键路径的 梳理。

路径1 :用户登录——>商品查询 —— > 订单下单

路径2: 用户登录—— > 商品查询 —— > 订单下单—— > 订单支付

路径3:用户登录—— > 查询—— > 订单退货

待服务关键路径梳理完成后,再对路径上的所有服务进行合并及去重,将会得到一组新的核心服务:用户登录/商品查询/订单下单/订单支付/订单查询/订单退货。

来计算下核心服务的占比,所有服务14个/核心服务6个,占42.86%,远远超过了20%。所以,建议继续从这些核心服务中,识别更核心的部分,但仍然以服务关键路径为整体。

相比订单下单/订单支付/订单退货这三条服务关键路径,我想订单支付可能会更有价值。最后,我们可以仅将订单支付这条服务关键路径上的服务作为核心服务。

重新再来计算下核心服务的占比, 所有服务14个/核心服务4个,占28.57%, 虽然还是超过了20%,但这并不是重点,重点是我们已经找到了最核心的服务

其余的核心服务,可以降级为准核心服务,重组后得到如下这份服务重要程度清单。

核心服务

准核心服务

非核心服务

用户登录

商品查询

订单下单

订单支付

 

 

 

订单查询

订单退货

 

退货进度

用户登出

密码修改

头像修改

用户查询

历史浏览

我的收藏

我的分享

当拥有这份清单后,若 应用系统处理阻塞时 ,就 可以按照非核心服务>准 核心服务> 核心服务这个顺序依次进行降级 。不过, 降级 不一定要拒绝请求,也可以是限流请求,这样可以减少对服务能力的裁剪力度

以上只是一种相对较粗的降级策略, 如果你想要制定更精细化的降级策略,还需要 每个服务进行优先级的设定 ,高低依据可以结合自身需要来制定,例如: 历史 服务使用情况。

当有了限流、降频、降级这三招,基本就能够在资源有限的情况下,让突发请求能够被平滑地处理,将应用系统的资源能够被合理地分配,以及当应用系统处理堵塞时, 确保核心服务正常。

取舍是一种权衡

现在,我们已经基本了解了如何衡量性能的指标,以及大致掌握了如何保护应用系统的招数。但这些就是高并发全部了吗,我想说这仅仅只是入门级别。

上述内容,主要是为了让你能够清晰地看到应用系统的性能水位在哪里,以及在资源有限下,当面对突发请求时可以采取哪些招数,能让应用系统安全地存活下来。

存活,即代表着可用性,它也是高并发的一个特性,而且是我认为相对比较重要的特性 。设想一下,如果你的应用系统连可用性都无法保证,那再高的性能又有何意义。

对于大部分应用系统而言,大家都会比较关注应用系统的可用性,99.9%不够那就99.99%,甚至还有想做到99.999%的,毕竟可用性的不足会直接影响到业务运作。

但对于 一个想成为高并发的应用系统而言,仅单方面关注高可用,肯定无法称得上这个头衔 ,它仍然还需要在其他特性上具备极佳的表现。

让我们拉回到最初的目标,性能是高并发的目标之一, 不过,这里我们不再谈论性能指标,而是来研究如何来提升性能。 因为,高性能是高并发的另外一个重要特性。

想要提升性能,势必投入成本。随着成本不断增加,性能提升幅度逐渐衰减。这两句话,是不是觉得有点耳熟,但不管你是否还记得,先让我在这里打个问号再说。

在架构设计的过程中,你是否经常会听到“取舍”的这个词,它是 通过牺牲一种能力来换取另外一种能力的方式 ,这些能力可以是性能、可用性、数据一致性或是其他能力。

等等,你是不是突然有意识到,提升性能并不只有投入成本这种方式,至少是在硬件资源方面,我们还可以通过牺牲一种能力去换取。 那到底选择牺牲哪种能力呢,牺牲可用性,一般不会第一时间考虑,那是不是可以考虑牺牲数据一致性。

但在考虑前得先声明一下, 所谓牺牲数据一致性,并不是完全不要,而是将数据强一致性降级为最终一致性 而对于数据最终一致性的理解,就是在数据更新后,要过一段时间后才能看到,而时间的长短就代表着牺牲了多少。

但并不是说,所有情况都必须牺牲数据一致性来提升性能,有些时候也可以考虑牺牲其他能力。但在取舍前,得先弄清楚当前要什么,但更需要弄清楚当前可以失去什么, 不合理的取舍,不但无法换取收益,反而还会引来更多的问题

情况1:数据缓存

缓存,是高并发架构设计中一种不可或缺的能力 ,一般是指那些经常被访问的热点数据,可以将它放入缓存中,从而提升数据被读取的效率。

但是否所有的数据都适合放入缓存中,如果是静态数据,那么你可以很安心地放入。原因很简单,静态数据不会更新,那么缓存和数据源始终保持一致,而且就算缓存中的数据丢失了,至少还有一份在数据源。

通过将静态数据缓存,可以很轻易地提升静态数据的访问性能,甚至可能是几十倍的效果。但应用系统中还有大量的动态数据,仅提升静态数据可能对总体的提升并不一定显著。

你是不是想说,那就把动态数据也放入缓存中不就行了。在下这个决定前,建议你先想一下,动态数据是会更新的, 这就意味着动态数据在放入缓存后,当数据源中的数据被更新后,再次访问返回的都是更新前的数据,这种效果你是否可以接受。

我想应该没有人会接受吧,而你是不是又想说,设置下缓存会过期不就能解决了。没错,但得等过期后才能解决,那还没过期前呢,这种方式只能缓解,但并不能根治,而且还会引入一个新的问题,请问过期设置多久才合适。

设置缓存5秒钟过期,可能永远无法命中缓存,而且不但没有提升性能,还增加了代码复杂度,有点画蛇添足的感觉。 设置缓存5分钟过期,命中缓存的几率可能会提高,但缓存后在5分钟内的如果数据更新,要从缓存开始往后推5分钟才能看到。

即: 第1分钟缓存,第1-6分钟内的任何数据更新,要第6分钟后才能看到。

所以,如果你无法容忍这种情况,请你不要滥用缓存,虽然性能提高了,但问题可能也出现了。反之,如果你可以容忍这种情况,那就可以这么操作,而至于过期设置多久,可以结合业务场景及使用频率综合来评估,毕竟不同的业务容忍度是不同的。

对于是否要将动态数据进行缓存,本质上,其实就是一种取舍,是一种性能与数据一致性的权衡,而 缓存的过期时长,就像是保持这种平衡的支点,从而让这种牺牲变得更有意义

情况2:单机限流

限流,前面已经有介绍过,它有两种常用的限流算法, 漏桶算法和令牌桶算法。不过 ,这两种算法都仅支持单机限流,不支持全局限流。

单机限流,就是对单节点设定一个限流阈值,如果单节点上的请求到达阈值,则会拒绝请求。例如: 限流阈值=每秒100次请求,如果在1秒内单节点上,有第101次的请求则拒绝。

全局限流,就是对一组节点设定一个限流总阈值,如果这组节点上的汇总请求到达阈值,则会拒绝请求。例如: 10个节点的限流总阈值=1000次请求,如果在1秒内这组节点上,汇总有第1001次请求则拒绝,不过单节点上有超过第100次的请求也会接受。

这么看下来,感觉单机限流控制能力更厉害一点,它能保证单节点的请求不会超过100次。而全局限流在极端情况下,单节点都有可能在1秒内会接受1000次请求。当然,这种情况的可能性比较低,比如在突发请求时,9个节点同时宕机。

既然如此,那全局限流有存在的意义吗,难道这就是 漏桶算法和令牌桶算法都不支持全局限流的原因。全局限流就真的没有存在的意义吗。 存在即合理,既然存在,那就一定有它存在的道理

换个情况,还是将 10个节点为一组,不过这次换成采用单机限流。问题来了,每个节点的限流阈值该如何设定,如果采用平均分配,则 限流阈值=每秒100次请求,让我们来测试一下,在1秒内依次发出1000次请求,会发生什么现象。

结果是在第100次请求后,从第101次到第1000次的请求中,可能有些请求会发生被拒绝的情况,而且请求一会儿成功一会儿拒绝,没有任何规律。原因可能是10个节点请求负载不均所引发的,导致某个节点提前超过了100次请求。

基于以上情况,最终1000次请求没有全部成功,这种情况等同于降低了应用系统的吞吐能力。而在实际情况中,就算采用 轮询的负载算法,请求数不均的可能性仍然还是会存在的 。这么一看,单机限流好像也有缺陷。

估计你已经被我说晕了吧,让我们整体再重新梳理一遍,并对 种不同限流模式的影响进行对比。不过,这次 还加上每秒不同的请求数量。

每秒请求

单机限流10*100笔/秒

全局限流1000笔/秒

100笔

无影响

无影

1000笔

少量请求拒绝

耗时小幅波动

10000笔

请求拒绝>9000

请求拒绝=9000耗时大幅波动

注:每个节点的处理能力为100笔/秒

两种限流对比下来,单机限流更强调单机的控制范围,但可能会造成额外的请求拒绝,但对单节点不会造成性能压力,而全局限流更强调整体的控制范围,虽不会造成额外的请求拒绝,但可能会对单节点造成性能压力,引发性能过载。

除此之外, 全局限流还是一种采用中心化的设计思路 ,因此在网络开销方面,还会产生额外的性能损耗,这种损耗在请求量少的时候估计还可以容忍,但在高并发的情况下可能是场灾难,因为在每次限流判断前,还会产生一次网络开销。

所以,不能为了想要实现更精准的限流,就盲目地采用全局限流,它将在高并发的情况下损耗更多的性能。而单机限流所额外造成的少量请求拒绝,在某些情况下,可以考虑采用某些技术手段进行补偿。

不过,不管是单机限流还是全局限流,似乎都和数据一致性没有关系。但事实上,全局限流这种精准限流的方式,也可以视为另一种一致性的表现,而单机限流就是通过对这种一致性的牺牲,来减少性能损耗,何尝不是提升性能的另一种方式。

以上,只是简单列举了两种不同情况下的取舍,而在高并发架构上,可取舍的地方远不止这些。你得知道, 高并发的每一处设计或每一份设计方案的背后,都曾是通过不断地取舍所获得的 ,而没有取舍的高并发架构决策,将会显得毫无说服力。

取舍不但可以作为高并发架构决策的有力武器,也将是驱动架构演进最合理的一种方式 。但要切记,取舍的方向并不是一成不变的,而是会随着外界环境的变化而变化,它将是一种独特的艺术。

写在最后

高并发的魅力之处,就在于它没有唯一的答案,而答案是需要我们以不同的业务场景作为线索去不断地寻找,这种寻找的过程也是一种不断思考的过程 ,这就是我为什么说高并发是一种架构思维模式。

本文从浅到深依次讲述了性能是实现高并发的基础条件,控制是实现资源最大化利用的方式,以及如何通过取舍来换取当前应用系统更所需的能力,但这些仅仅只是高并发世界里的一个角落。因篇幅有限,今天就暂告一段落。

最后想说, 高并发其实并不可怕,可怕的是你知其然而不知其所以然 。对于追求技术的你, 需要不断地拓宽你的技术深度与广度,才能更好地掌握高并 发,以及运用高并发的思维模式来提升应用系统处理能力。

原文地址:https://mp.weixin.qq.com/s/apUVMfAI52uvc7U-iOVvKg?utm_source=tuicool&utm_medium=referral

延伸 · 阅读

精彩推荐