如何设计秒杀服务的限流策略?

1,366 阅读14分钟

对于秒杀业务,大家应该比较熟悉了。比如,“某商品原价 1299 元, 双十一整点秒杀价仅 500 元,限量 100 件,先到先得” 等等。通过这段文案我们能够发现,参与秒杀活动商品的价格都比平时低很多,因此会吸引大量的用户来抢购。从而不难发现,对于秒杀系统的挑战就是:在流量瞬时突增的情况下,如何做依旧能够保证系统的稳定性。

​ 1、基于合法性限流。

​ 2、基于负载限流。

​ 3、基于服务限流。

​ 4、基于监控限流。

那举个例子来说,比如说我现在横轴是时间轴,纵轴是用户的并发访问量,在横轴的 S 点就是秒杀的开始时刻。

image-20210216010644047

那一般而言,在秒杀开始之前,用户的访问量是一条比较平滑的曲线,但随着秒杀活动的开始,用户的访问量会急剧的增大,并且随着秒杀的结束,访问量又会急剧的下降。

我们假设在初始时用户的访问量在 1 万左右浮动,秒杀服务器能够承受的极限是 5 万,但在秒杀活动期间,用户的实际访问量可能达到了 100万。

那么很明显,100 万的访问量已经远远超过了系统能够正常承载的 5 万并发量,因此这时候就可能会导致秒杀系统的不稳定,甚至宕机的情况。

所以我们现在不得不提到刚才所说的了,秒杀系统面临的最大挑战就是如何要保证在流量突增的情况下,仍然保证系统的稳定性。

那么在实际开发中,其实有很多方案都可以保证系统的稳定性。 而我们本场 Chat 要分享的重点就是说如何要通过限流策略抵御秒杀期间的流量峰值,从而实现稳定性,那一起来看一下具体怎么操作。

当海量请求到来时,我们可以对请求进行层层设卡、层层拦截,最终将海量请求削减成服务器能够处理的请求。

多次限流

那举个例子来说,比如秒杀开始时,可能有 100万的请求同时扑向服务器。如果从多层限流的角度来说,我们就可以在第 1 层把流量先削减成 30 万,然后在第 2 层减到 10 万,再在第 3 层减到 5 万,然后直接将这 5 万直接处理就可以了。

image-20210216011102660

不难发现,在限流时,我们既要层层限流,也要尽早限流,因为上游拦截的请求越多,下游的流量就越少。

那接下来我们就一起看一看到底如何进行层层限流。

基于合法性限流

先看一下低层限流又是合法性限流,为了更好的解决这个问题,我们先需要看一下到底什么是合法性限流?

合法性限流指的是仅仅限制那些合法的用户请求能够抵达到秒杀服务器,而将一些非法的请求全部进行拦截掉。那因此这里就需要注意了,在请求合法性限流以前,就得先知道哪些请求是合法的,哪些是非法的。

举一些非法的例子。比如在秒杀活动期间,那实际参与秒杀活动的用户可能是人,也可能是机器人,并且还可能存在同一用户反复购买同一件商品的行为,也是我们说的刷单行为。

image-20210216011521119

那么显然机器人和用户刷单都是一种不合理的行为,这种行为会影响到其他正常用户的购物体验,因此就属于不合法的请求。

合法性限流解决方案

而关于如何限制这些不合法的请求,那么就得具体问题具体分析和讨论了。

通过验证码的方式

比如说,如果非法请求的发起者是机器人,那么最容易想到的方法就是使用验证码。并且验证码还有一个作用,它可以拉长用户的访问时间。

image-20210216011838779

举个例子,假设某一秒钟有 100 万个用户同时下单,但如果使用了验证码,那么用户从输入验证码到整个下单的整个过程就可能需要三秒钟,也就是说下单量仍然是 100 万不变,但下单的总体时间可能从 1 秒钟拉长到了 3 秒,那么原来需要 1 秒的时间,现在就需要 3 秒的世界,原来 100 万的请求,现在每秒钟就只需要处理 33 万,因此也可以降低流量的峰值。

通过 IP 的方式

再来看一下IP限制,如果通过网络技术监测到了某个 IP 下的下单频率在毫秒级别,或者反复购买同一件商品,那么就能断定下单的是机器人或者是不合法的用户,这样我们就可以将这个 IP 加到黑名单之中,从而减少不合法的流量。

image-20210216012315911

那还有一种做法是隐藏秒杀的入口地址,它指的是在秒杀开始之前,服务器并不会向外界暴露秒杀服务的地址,当秒杀服务开始之后才开放地址。

image-20210216012531174

那到这里,第 1 层合法性线路就讲解完了,接下来我们再看一下第二层限流,也就是负载限流。

基于负载限流

先看一下负载限流的理论基础是什么?一个是集群,一个是网络 7 层模型。

image-20210216012920319

我们在搭建集群时经常会用到一些工具,比如说 Nginx 和 LVS,那这些都可以用于负载限流。

基于软件实现限流

假设经过了第 1 层合法性限流以后,还剩 33 万的请求,如果通过集群搭建了三台服务器,那么每台服务器也就只需要承载 11 万的请求量了,那这样也能降低请求的并发量。

image-20210216013115819

但是根据网络 7 层模型,Nginx 处于第 7 层。那除此以外,在网络 7 层模型之中的其他层也可以进行负载。

image-20210216013229595

比方说我们在第 2 层的数据链路层,也可以通过 MAC 地址进行负载。比如我们可以生成一个虚拟 MAC ,然后将这个 MAC 地址映射到其他三个真实的服务器上。同样的,也可以在网络第 3 层通过 IP 进行负载,在第 4 层通过端口号进行负载。

那看到这里,有同学可能会问了,能否进行级联负载呢?

我们假设当请求到来时,能否先在第 2 层进行负载,然后再在第 3 层、之后再在第 4 层、第 7 层分别都进行一次负载?

image-20210216013512422

如果这样做,在功能上肯定是可以实现的。但这种级联的做法也会同时增加请求的路径,因为我们知道每增加 1 次负载,就会增加 1 个转发路径,而每增加 1 个转发路径,就可能带来网络延迟问题,因此太多的接连负载也是不推荐的。

那么对于既然负载,常见的做法有哪一些呢?我认为单独的是由 Nginx 或者使用 Nginx 和 LVS 来实现二级负载就已经对于大部分系统足够了。

刚才提到的 LVS 是处于第 4 层,它是通过网络端口进行的复杂,而 Nginx 是第 7 层应用级别的负载。

image-20210216020208458

那还有就是我们这里说的负载,都是通过软件进行的负载,也就是软负载。

基于硬件实现限流

那除此以外,我们还可以购买一些硬件工具进行负载,也是硬负载。常见的应用负载工具有 F5 或 Array 等。

image-20210216020319119

好,大家可能已经发现了啊,前两层限流都是想办法将请求拦截在抵达服务器之前,但是如果请求已经抵达到了服务器,又该如何进行限流呢?

基于服务限流

那这个其实就是我们马上要讲的第 3 层限流优势,服务限流。

首先我们可以通过 Web 服务器本身进行限流,比方说 Tomcat 是一款比较熟悉的 Web 服务器,如果连接 Tomcat 的数量太多,就可能造成 Tomcat 的不稳定,那该怎么办呢?

我们可以把 Tomcat 的最大链接数,设置为一个合理的值,比方说我们可以设置单 Tomcat 的最大链接数只为 300,那如果超过 300 的链接请求就会被 Tomcat 的无条件拒绝,那这样就可以保证谈不开的稳定性了。

image-20210216020734835

那再比如,我们也可以在服务器的内部,通过编写一些算法来进行限流,那常见的算法有哪一些呢?

基于算法实现限流

比如说令牌桶算法、漏洞算法都是的。那对于这些算法,如果你的编写有些困难,我们也可以直接调一些类库里边儿已经存在的 API。

public class RateLimiter {
    //令牌桶限流:每秒只生成100个令牌,只有抢到令牌的线程才能抢购。
    static RateLimiter tokenRateLimiter = RateLimiter.create( 100.0) ;
    public static void miaoShaController () {
        //每次抢购操作,都会持续尝试l秒
        if (tokenRateLimiter.tryAcquire(1,TimeUnit.SECONDS)) {
            //开启抢购线程
        }
    }
}

那么这就是使用 Google Guava 类裤的令牌桶算法,create() 的方法可以限制每秒钟最多有 100 个线程可以同时参与抢购,tryAcquire 方法可以用于设置每秒的抢购动作会持续 1 秒钟,实现起来非常简单。

基于消息队列实现限流

那除了刚才讲的服务器配置参数以及限流算法以外,我们在服务器之中还可以使用队列来进行限流,这里说的队列主要是消息队列。

这里我们拿一个例子来说,假设每秒钟有 10 万的请求量,并且系统里边有三个子系统 A、B 和 C,那三个子系统每秒钟能够处理的极限分别是 2 万请求、3 万请求和 4 万请求。

image-20210216021423083

在不使用消息队列的情况下,如果这 10 万请求分别平均分给这三个子系统,那么每个子系统就需要处理 3.3 万的请求。那很显然,在每秒钟之内,系统 A 只能处理 2 万请求,如果接收到了 3.3 万请求,就可能导致系统 A 延迟甚至崩溃的情况。

而如果使用消息队列就可以很好地解决这种问题。那消息队列本质是一种缓冲区,当 10 万请求到来时,消息队列可以将这 10 万请求临时存储,然后三个子系统再分别根据自己的性能,分别去消息队列中针对性的去拉取特定数量的请求。

image-20210216021557036

比方说系统 A 的极限是 2 万,那么他每次最多就只需要从队列之中取 2 万数据就够了,那这样就可以避免超额请求对系统 A 造成的压力的情况了。

除了前面介绍的服务器限流以及队列限流以外,我们还可以使用第 3 个服务限流,也就是缓存限流。

缓存限流

限流的本质是为了不断地削减请求的数量,而缓存的作用是为了减少用户请求服务端的数量,因此缓存也可以作为限流的一种实现方案。

但为了有效地使用缓存进行限流,我们需要先将系统设计成前后端分离或者动静分离的结构,然后分别的对静态以及动态缓存进行限流。

静态缓存实现限流

先看一下对静态请求如何进行缓存。那当客户端第 1 次请求服务端的时候,服务端会将网页的基本结构代码想给客户端。

比如我们第 1 次访问某个网站时,网站服务器就会将搭建此网站的 HTML、JavaScript 脚本等代码响应给客户端,那么客户端就可以将这些 HTML 和 JavaScript 代码缓存到客户端浏览器之中。那么这样一来,当用户以后再次访问这个网站时,就可以直接从本地浏览器的缓存中获取 HTML 和 JavaScript 代码了。

对于 HTML 这种体积比较小的代码,我们可以直接将其缓存在浏览器之中,但是如果体积较大的图片,我们最好将它们缓存的 Nginx,或者通过 Nginx 转发在 OSS 等于服务器之中,而如果是视频等一些体积特别大的静态资源,也可以将它缓存在 CDN 中,利用 CDN 区域部署就近访问的特点来提高用户的访问速度。

image-20210216022419755

并且我们知道各个缓存并不是独立的,也可以相互补充,比如说 OSS 也可以作为 CDN 的回源站点。

动态缓存实现限流

接下来再看一下动态缓存,那对于动态缓存,一般先建议缓存在本地的服务器之中,如果本地服务器的缓存失效,我们再缓存到由 Redis 组成的远程集群之中,进行二次的查询。也就是说我们可以搭建本地缓存以及二远程缓存组成的二级结构,进行动态请求的缓存。

image-20210216022641542

需要注意的是,缓存的级别也并不是越多越好。有同学可能也会想到,他说缓存既然这么好用,那么干脆多来几级缓存。我们可以在CPU、内存、硬盘、网络等节点上分别设置缓存,并且每个节点里边还可以再次细分出多级缓存。

那如果这样做,就必须要考虑多级缓存带来的一致性问题了,缓存的级别越多,一致性的问题就越严重,而解决这种一致性问题又会增加系统的开发成本以及系统的额外开销。

还要知道的是,我们缓存的级别越多,请求在系统内部的跳转路径也会越长,这也就类似于多级负载带来的问题。所以说我们学技术一定要懂得权衡,不要盲目的进行技术的堆砌。

那么对于大部分项目而言,我们使用静态缓存加上二级动态缓存已经完全足够了。

那总得来说,我们静态缓存可以将大量的静态资源缓存在服务器以外的地方,而动态缓存可以很大程度上减少请求抵达数据库的次数。

image-20210216023010713

基于监控限流

那最后我们再来看一下监控限流。我们知道 CPU、内存、并发量等都是衡量系统稳定性的重要指标,如果他们的使用频率过高,也可能造成系统的不稳定。

因此我们也可以建议创建一些线程,专门用于监控这些指标。比方说我们可以建立一个线程,专门用于监控 CPU 的利用率,如果 CPU 利用率达到了极限,就可以临时性地采取服务降级或拒绝策略。

那这里说的服务降级实际上与精兵简政的思想类似,它指的是当系统资源不足时,我们就可以把查看三个月以前的历史订单、历史评论等一些非核心的服务临时关闭,从而为系统节约出一部分的资源来。

那在采用服务降级或拒绝策略一段时间之后,CPu 等资源利用率就会恢复到正常状态,那之后我们就可以重新接收并处理新的请求了。

总结

好的,我们今天分享的主题是如何设计秒杀服务的限流策略。这里我介绍了合法性限流、负载限流、服务限流。其中合法性限流可以拦截大量的非法请求,而负载限流可以通过集群技术抵抗大规模的流量冲击服务,下流则是通过对服务器的参数配置、限流算法、MQ 缓存以及监控等手段进行限流。

image-20210216023344877