对于秒杀业务大家应该比较熟悉了,比方说某件商品原价是1299元,那么双11整点秒杀价仅500元,限量100件,先到先得。我们不难发现,参与秒杀商品的价格一般比平时比较低廉,因此会吸引到大量用户同时去抢购。所以说,对于秒杀系统的最大挑战,就是如何在流量瞬时突增的情况下保证系统的稳定性。
举个例子来说,比如说现在横轴是时间轴,纵轴是用户的并发访问量,在横轴的S点就是秒杀的开始时刻,一般而言,在秒杀开始之前,用户的访问量是一条比较平滑的曲线,但随着秒杀活动的开始,用户的访问量会急剧的增大,并且随着秒杀的结束,访问量又会急剧的下降。我们假设在初始时用户的访问量在1万左右浮动,秒杀服务器能够承受的极限是5万,但在秒杀活动期间,用户的实际访问量可能达到100万。那么很明显100万的访问量已经远远超过了系统能够正常承载的5万并发量,因此这时候就可能会导致秒杀系统的不稳定甚至宕机的情况。所以,秒杀系统面临的最大挑战,就是如何保证在流量突增的情况下仍然保证系统的稳定性。在实际开发中,有很多方案都可以保证系统的稳定性。
今天要分享的重点是,如何通过限流策略抵御秒杀期间的流量峰值,从而实现稳定性。当海量请求到来时,我们可以对请求进行层层设卡、层层拦截,最终将海量请求消减成服务器能够处理的请求数。举个例子来说,比如秒杀开始时可能有100万的请求同时扑向服务器,如果从多层限流的角度来说,我们就可以在第1层把流量先消减成30万,然后在第2层减到10万,在第3层减到5万,然后直接将这5万直接处理就可以了,在限流时我们既要层层限流,也要尽早限流,因为上游拦截的请求越多,下游的流量就越少。
合法性限流
先看一下第一层限流,也就是合法性限流,首先,什么是合法性限流?它说的是仅仅限制那些合法的用户请求能够抵达到秒杀服务器,而将一些非法的请求全部进行拦截掉。因此这里就需要注意了,在请求合法性限流以前,就得先知道哪些请求是合法的,哪些是非法的。我举一些非法的例子,比如在秒杀活动期间,实际参与秒杀活动的用户可能是人,也可能是机器人,并且还可能存在同一用户反复购买同一件商品的行为,也就是我们说的刷单行为。那么显然机器人和用户刷单都是一种不合理的行为,这种行为会影响到其他正常用户的购物体验,因此就属于不合法的请求。而关于如何限制这些不合法的请求,那么就得具体问题具体分析和讨论了。
比如说,如果非法请求的发起者是机器人,那么最容易想到的方法就是使用验证码,并且验证码还有一个作用,它可以拉长用户的访问时间。举个例子,假设某一秒钟有100万个用户同时下单,但如果使用了验证码,那么用户从输入验证码到整个下单的整个过程就可能需要三秒钟,也就是说下单量仍然是100万不变,但下单的总体时间可能从一秒钟拉长到了三秒,那么原来需要一秒的时间,现在就需要三秒,原来100万的请求,现在每秒钟就只需要处理33万,因此也可以降低流量的峰值。
再来看一下IP限制,如果通过网络技术监测到了某个IP的下单频率,在毫秒级别或者反复购买同一件商品,那么就能断定下单的是机器人或者是不合法的用户,这样我们就可以将IP加到黑名单之中,从而减少不合法的流量。还有一种做法是隐藏秒杀的入口地址,他指的是在秒杀开始之前,服务器并不会向外界暴露秒杀服务的地址,当秒杀服务开始之后才开放地址。接下来我们再看一下第二层限流,也就是负载性限流。
负载限流
先看一下负载限流的理论基础是什么,一个是集群,一个是网络7层模型,我们在搭建集群时经常会用到一些工具,比如说Nginx和LVS,这些都可以用于负载限流。假设经过了第1层合法性限流以后,还是有33万的请求,如果通过集群搭建了三台服务器,那么每台服务器也就只需要承载11万个请求量了,这样也能降低请求的并发量。
但是根据网络7层模型,Nginx处于第7层,除此以外,在网络7层模型之中的其他层也可以进行负载,比方说我们在第2层的数据链路层,也可以通过MAC地址进行负载,比如我们可以生成一个虚拟MAC,然后将MAC地址映射到其他三个真实的服务器上,同样的也可以在网络第3层通过IP进行负载。在第4层通过端口号进行负载。
那么能否进行级联负载呢,我们假设当请求到来时,能否先在第2层进行负载,然后再在第3层,之后再在第4层、第7层分别都进行一次负载。如果这样做在功能上肯定是可以实现的,但这种级联的做法也会同时增加请求的路径。每增加一次负载就会增加一个转发路径,而每增加一个转发路径就可能带来网络延迟问题,因此太多的级联负载也是不推荐的。那么对于级联负载常见的做法有哪一些?我认为单独的使用Nginx,或者Nginx和LVS来实行二级负载,就已经对于大部分系统足够了。
刚才提到的LVS是处于第4层,它是通过网络端口进行的负载,而Nginx是第7层应用级别的负载,还有我们这里说的负载都是通过软件进行的负载,也就是软负载。除此以外我们还可以购买一些硬件工具进行负载,也就是硬负载,常见的硬负载工具,有F5、Array等。大家可能已经发现了,前两层限流都是想办法将请求拦截在抵达服务器之前,但是如果请求已经抵达到了服务器,又该如何进行限流?其实就是我们马上要讲到第3层限流。,也就是服务限流。
服务限流
首先我们可以通过Web 服务器本身进行限流,比方说Tomcat是一款比较熟悉的Web服务器,如果连接Tomcat的数量太多,就可能造成Tomcat的不稳定,该怎么办呢?我们可以把Tomcat最大连接数设置为一个合理的值,比方说我们可以设置Tomcat最大连接数值为300,如果超过300的连接请求就会被Tomcat无条件拒绝,这样就可以保证Tomcat稳定性了。再比如我们也可以在服务器的内部,通过编写一些算法来进行限流。常见的算法比如说令牌桶算法、漏桶算法。对于这些算法,如果你的编写有些困难,我们也可以直接调用一些类库里边已经存在的API。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
public class TestRateLimiter {
// 令牌桶限流:每秒只生成100个令牌,只有抢到令牌的线程才能抢购
static RateLimiter tokenRateLimiter = RateLimiter.create(100.0);
public static void miaoshaController(){
// 每次抢购操作,都会持续尝试1秒
if(tokenRateLimiter.tryAcquire(1,TimeUnit.SECONDS)){
// 开启抢购线程
}
}
}
这就是使用Google Guava类库的令牌桶算法,create()方法可以限制每秒钟最多有100个线程可以同时参与抢购,而tryAcquire()方法可以用于设置每秒的抢购动作会持续一秒钟,实现起来非常简单。
除了刚才讲的服务器配置参数以及限流算法以外,我们在服务器之中还可以使用队列来进行限流。这个说的队列主要是消息队列。这里我们拿一个例子来说,假设每秒钟有10万的请求量,并且系统里边有A、B、C三个子系统,每秒钟能够处理的极限分别是2万请求、3万请求和4万请求。在不使用消息队列的情况下,如果这10万请求分别平均分给这三个子系统,那么每个子系统就需要处理3.3万的请求。很显然在每秒钟之内,系统A只能处以2万请求,如果接收到了3.3万请求,就可能导致系统A延迟甚至崩溃的情况,而如果使用消息队列就可以很好的解决这种问题。消息队列本质是一种缓冲区,当10万请求到来时消息队列可以将这10万请求临时存储,然后三个子系统再分别根据自己的性能,分别去消息队列中针对性的去拉取特定数量的请求。比方说系统A的极限是2万,那么他每次最多就只需要从队列之中取2万数据就够了,这样就可以避免超额请求对系统造成的压力的情况了。
除了前面介绍的服务器限流以及队列限流以外,我们还可以使用第3个服务限流,也就是缓存限流,限流的本质是为了不断的削减请求的数量,而缓存的作用是为了减少用户请求服务端的数量,因此缓存也可以作为限流的一个实现方案,但为了有效的使用缓存进行限流,我们需要先将系统设计成前后端分离或者动静分离的结构,然后分别的对静态以及动态缓存进行限流。
先看一下对静态请求如何进行缓存,当客户端第1次请求服务端的时候,服务端会将网页的基本结构代码显示给客户端,比如我们第1次访问某个网站时,我让服务器就会将搭建此网站的html、JavaScript脚本等代码响应给客户端,那么客户端就可以将这些html、JavaScript代码缓存到客户端浏览器之中。那么这样一来,当用户以后再次访问这个网站时,就可以直接从本地浏览器的缓存中获取html、JavaScript代码了。对于html这种体系比较小的代码,我们可以直接将其缓存的浏览器之中,但是如果体积较大的图片,我们最好将它们缓存的Nginx或者通过Nginx转发在OSS等云服务器之中。而如果是视频等一些体积特别大的静态资源,也可以叫它缓存在CDN中,利用CDN区域部署、就近访问的特点,来提高用户的访问速度。并且我们知道各个缓存并不是独立的,也可以相互补充,比如说OSS也可以作为CDN的回源站点。
接下来再看一下动态缓存,对于动态缓存一般先建议缓存在本地的服务器之中,如果本地服务器的缓存失效,我们再缓存到由Redis组成的远程集群之中进行二次的查询,也就是说我们可以搭建本地缓存以及远程缓存组成的二级结构进行动态请求的缓存,需要注意的是缓存的级别也并不是越多越好。我们可以在CPU、内存、硬盘、网络等节点上分别设置缓存,并且每个节点里边还可以再次细分出多级缓存来。如果这样做就必须要考虑多级缓存带来的一致性问题了,缓冲的级别越多,一致性的问题就越严重,而解决这种一致性问题又会增加系统的开发成本以及系统的额外开销。还要知道的是我们缓存的级别越多,请求在系统内部的跳转路径也会越长,而这也就类似于多级负载带来的问题。
那么对于大部分项目而言,我们使用静态缓存加上二级动态缓存已经完全足够了。总的来说,我们静态缓存可以将大量的静态资源缓存在服务器以外的地方,而动态缓存可以很大程度上减少请求抵达数据库的次数。
最后我们再来看一下监控限流,我们知道CPU、内存、并发量等都是衡量系统稳定性的重要指标,如果他们的使用频率过高,也可能造成系统的不稳定。因此我们也可以创建一些线程专门用于监控这些指标,比方说我们可以建立一个线程,专门用于监控CPU的利用率,如果CPU利用率达到了极限,就可以临时性的采取服务降级或拒绝策略。这里说的服务降级实际上与精兵简政的思想类似,它指的是当系统资源不足时,我们就可以把查看三个月以前的历史订单、历史评论等一些非核心的服务临时关闭,从而为系统节约出一部分的资源来。在采用服务降级或拒绝策略一段时间之后,CPU等资源利用率就会恢复到正常状态,我们就可以重新接收并处理新的请求了。
这里介绍了合法性限流、负载性流、服务限流,其中合法性限流可以拦截大量的非法请求,而负载限流可以通过集群技术抵抗大规模的流量冲击。服务限流则是通过对服务器的参数配置、限流算法、MQ缓存以及监控等手段进行限流。