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