高并发架构:限流
首先看一个计算系统最大吞吐量的例子:
已知条件:
- 系统中每个业务操作需要调用 10 个服务来协调完成(顺序调用)
- 该业务操作的总超时时间为 10 s
- 每个服务实例的处理时间平均为 0.5 s
- 集群中每个服务均部署了 20 个实例副本
求解以下问题:
单个用户访问,完成一次业务操作,需要耗费系统多少 处理器时间 ?
答:0.5 × 10 = 5 s CPU Time
集群中每个服务每秒最大能处理多少请求?
答:(1 ÷ 0.5) × 20 = 40 QPS
假设不考虑顺序且请求分发是均衡的,在保证不超时的前提下,系统(整个集群)每秒最多处理多少笔业务?
答:40 × 10 ÷ 5 = 80 TPS
如果集群在一段时间内持续收到 100 TPS 的请求,会出现什么情况?
什么是限流
限流 是系统对服务的保护措施,一般是上游服务配置好限流规则,防止突发流量把下游服务打爆。假设服务每秒能支撑最多 50 个请求,如果遇到流量高峰,一秒内打进来 80 个请求,就会导致服务崩溃。这里的崩溃指的是服务连一个请求都无法处理了,因为服务器的资源已经用完,无法支撑系统运行了。就像早期的 12306 购票网站,如果大量的人都抢票,就会把服务打崩,导致大家都买不上票。
限流要达到的目的就是让服务在处于超过自己处理能力的高并发情况下,还能继续处理自己能力内的那部分请求,拒绝多出来的请求。就像上面的例子,服务器在一秒 80 个请求的前提下,还能正常处理 50 个请求,只把剩余的 30 个请求拒绝掉(或者放到队列中等待后续处理)。
依据什么限流-限流指标
限流需要有明确的流量指标用来进行判断。对于服务而言,一般有三种流量指标来反映流量压力的大小。
每秒事务数(Transactions per Second, TPS)
TPS
是衡量信息系统吞吐量的最终标准。“事务”可以理解为一个逻辑上具备原子性的操作,只有成功和失败两种状态。事务在不同的服务上有不同的定义,比如下单操作。
每秒请求数(Hits per Second, HPS)
HPS
指的是每秒从客户端向服务端发起的请求数。如果只要一个请求就能完成一笔业务,那么 HPS
和 TPS
是等价的。但在一些场景中,一笔业务可能需要发送多次请求才能完成,比如支付业务,可能需要分为生成二维码、扫码支付、校验等多个请求。
每秒查询数(Queries per Second, QPS)
QPS
指的是服务器能够响应的查询次数。如果只有一台服务器来应答请求,那么 QPS
和 HPS
是等价的。但在分布式系统中,一个请求的响应往往要由后台多个服务节点共同协作完成。因此 1 HPS
一般可以对应多个 QPS
。
=======================
上面提到的这三种指标,都是基于计数来做统计的。在整体目标上我们最希望使用 TPS
指标来限流,因为这个指标最接近业务。但是系统业务五花八门,不同的业务操作对系统的压力也往往差异巨大,所以不同服务的 TPS
没有什么可比性。更关键的是,流量控制是针对用户实际操作场景来限流的,而用户的操作时间具有不确定性,可能用户扫码后等待了几分钟后才付款,这时候统计的 TPS
是反映不了系统压力的,因此直接针对 TPS
限流实际上很难操作。
目前,大多数系统都倾向于使用 HPS
作为限流指标。HPS
容易观察统计,也能在一定程度上反映系统当前和接下来一段时间的压力。
但限流指标没有明确的法则可以遵循,需要根据系统的实际业务来定。比如下载、视频、直播等 I/O 密集型服务,会选择请求和响应的报文大小作为限流指标,比如只允许单位时间内通过 100M 流量。又比如网络游戏等基于长连接的服务,可能会把登陆用户作为限流指标,比如热门网游在超过一定用户数就会让你在登陆前排队。
如何测量指标
这三种指标都是基于计数做统计的。常用的统计方法有两种。
计数器
这是最简单的统计方法。计数器统计的是当前服务正在进行中的任务,例如服务接收到一个请求,就在内部计数器上加 1,服务返回一个请求(正常返回或超时返回),就在内部计数器上减 1。当服务计数器达到最大数量后,新的请求就会被限制。
后台定时任务每秒获取一次计数器的值,得到的就是统计指标了。例如第一秒计数器值为10,指标就是 10 TPS(或 QPS 等,和统计场景有关)。下一秒计数器值为 9,指标就是 9 TPS。
但这种统计方式有缺陷,得到的指标并不能准确体现当时系统的流量压力。比如:
- 即使统计结果显示每秒的 TPS 都不超过 80,也不能说明系统没有遇到过大于 80 TPS 的压力。假设两个统计周期内系统都收到了 60 TPS 的请求,但两个 60 TPS 的请求分别发生在前一秒里的后 0.5 秒和下一秒里的前 0.5 秒。这样统计出来的结果都是 60 TPS,但系统确实在 1s 内实实在在发生了超过阈值的 120 TPS 请求。
- 假设某服务处理一个请求的平均时长为 2s,可以支撑 10 TPS 的请求。计数器在某一秒内统计的 TPS 达到了 20,看起来好像超过了阈值,但这个时候系统是在正常运行中的。因为第一秒接收了 10 个请求,第二秒又接收了 10 个请求,此时第一秒的请求还在正常处理,虽然第二秒的统计指标是 20 TPS。如果系统基于 10 TPS 来做限流,反而会误杀一部分正常请求,造成不必要的请求失败。
计数器缺陷的根源是只针对离散的时间点做统计,没有体现出一段时间的流量情况。
滑动时间窗
滑动时间窗是一种更准确的统计方法,它统计一段时间内的流量情况。它其实还是依赖了计数器统计,区别是统计一段时间内的计数器值。比如服务端需要统计 10s 内的流量情况,维护了一个长度为 10 的队列。定时任务每秒访问一次计数器的值,并将其压入队列。当队列满了以后,再次入队就会把队首的记录淘汰。这个队列就是一个固定长度的时间窗,窗内可以观察最近 10s 内的流量情况,以此做为限流或熔断的依据。
Hystrix 就使用了这种统计方式,下面是 Hystrix 官方文档 中的示意图:
Hystrix 统计的指标更详细,可以看到它同时记录了成功、失败、超时、拒绝四种类型的数据,通过这些指标来确定断路器的状态。
具体如何限流-限流模式
现在我们定义了流量的指标,也知道了服务器当前的指标值。接下来具体需要如何去限流呢?
流量计数器模式
计数器我们在指标测量里提到过,这是最基础的限流方式。计数器限制的是访问资源的总并发数。这种限流方式通常用在资源数量固定的场景,例如数据库连接池、线程池、服务并发访问数等。流量计数器的实现方式简单,可以应对一定程度的突发流量。
Java 中可以通过 AtomicInteger
、信号量 Semaphore
或者固定数量的线程池来实现。
计数器的弊端:瞬时流量高也会拒绝请求。
令牌桶模式(Token Bucket)
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常用的算法。先有一个木桶,系统按照固定速率,往桶里加入令牌,如果桶已经满了就停止添加。请求进来时,先去桶里拿走一个令牌,取到令牌才可以继续进行请求处理,没有令牌系统就拒绝服务。
令牌桶和信号量的区别?
首先,信号量的信号和令牌桶的令牌都是固定数目的可用资源,两者在资源控制上的作用是相同的。
其次,信号量的资源是被动释放的,令牌桶的资源是以固定速率生成的。被信号量控制的资源数据一定不会大于信号量自身定义的容量。但令牌桶发放的且正在被使用的令牌,有可能大于桶的容量。
举个例子,令牌桶就像是饭店门口的服务员,负责发号。信号量就是饭店里的位置,是固定资源。
令牌桶控制的是请求速率,信号量控制的是资源访问,两者关注的角度不同。
未被使用的令牌会存放在桶里,如果有突然流量进来,只要 Token 足够,就可以一次性放行。因此令牌桶算法支持突发流量。
Guava RateLimiter
提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
漏桶模式(Leaky Bucket)
水(请求)先进入漏桶中,漏桶以固定速率出水,当桶里的水满(请求达到阈值)后,接下来的请求就被丢弃。可以看到漏桶能强制限定数据的传输速率。
漏桶和令牌桶的区别:
- 令牌桶限制的是 平均流入速率 ,并允许一定程度的突发流量;
- 漏桶限制的是 常量流出速率 ,无论进入的流量多大,流出的流量是固定不变的;
漏桶和消息队列的区别?
可以看到,漏桶起到的作用和消息队列的削峰类似。两者的主要区别是获取数据的方式。 漏桶是把数据推送到下游,消息队列则是下游主动拉取数据。因此漏桶可以控制消息流入的速率,而消息队列不能控制,流入速率只能取决于下游的消费速率。
超额的流量如何处理?
根据不同的业务,有两种处理方式:
- 直接返回失败(如 429: Too Many Requests),或者迫使上游进入降级逻辑,称为 否决式限流 ;
- 进入队列等待或阻塞一段时间后继续处理,称为 阻塞式限流 ;
在不引入其他组件的前提下讨论,用 AtomicInteger
实现的计数器就是否决式限流,用信号量实现的计数器就可以是阻塞式限流,因为请求可以进入阻塞队列等待。