你好,游客 登录
背景:
阅读新闻

从构建分布式秒杀系统聊聊限流特技

[日期:2018-06-19] 来源:51CTO博客  作者: [字体: ]

  前言

  俗话说的好,冰冻三尺非一日之寒,滴水穿石非一日之功,罗马也——是一天就建成的。两周前秒杀案例初步成型,分享到了中国较大的同×××友网站-码云。同时也收到了——少小伙伴的建议和投诉。我从不认为分布式、集群、秒杀这些就应该是大厂的专利,在互联网的今天无论什么时候都要时刻武装自己,只有这样,也许你的春天就在明天。

  在开发秒杀系统案例的过程中,前面主要分享了队列、缓存、锁和分布式锁以及静态化等等。缓存的目的是为了提升系统访问速度和增强系统的处理能力;分布式锁解决了集群下数据的安全一致性问题;静态化无疑是减轻了缓存以及DB层的压力。

  限流

  然而再牛逼的机器,再优化的设计,对于特殊场景我们也是要特殊处理的。就拿秒杀来说,可能会有 别的用户进行——,而商品数量远远小于用户数量。如果这些请求都进入队列或者查询缓存,对于较终结果没有任何意义,徒增后台华丽的数据。对此,为了减少资源浪费,减轻后端压力,我们还需要对秒杀进行限流,只需保障部分用户服务正常即可。

  就秒杀接口来说,当访问频率或者并发请求超过其承受范围的时候,这时候我们就要考虑限流来——接口的可用性,以防止非预期的请求对系统压力过大而引起的系统瘫痪。通常的策略就是拒绝多余的访问,或者让多余的访问排队等待服务。

  限流算法

  任何限流都不是漫无目的的,也不是一个开关就可以解决的问题,常用的限流算法有:令牌桶,漏桶。

  令牌桶

  令牌桶算法是网络流量×××(Traffic Shaping)和速率限制(Rate Limiting)中较常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送(百科)。

  在秒杀活动中,用户的请求速率是不固定的,这里我们假定为10r/s,令牌按照5个每秒的速率放入令牌桶,桶中较多存放20个令牌。仔细想想,是不是总有那么一部分请求被丢弃。

  漏桶

  漏桶算法的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被×××以便为网络提供一个稳定的流量(百科)。

  令牌桶是无论你流入速率多大,我都按照既定的速率去处理,如果桶满则拒绝服务。

  应用限流

  Tomcat

  在Tomcat容器中,我们可以通过自定义线程池,配置较大连接数,请求处理队列等参数来达到限流的目的(图片源自网络)。

  Tomcat默认使用自带的连接池,这里我们也可以自定义实现,打开/conf/server.xml文件,在Connector之前配置一个线程池:

  <Executorname="tomcatThreadPool"

  namePrefix="tomcatThreadPool-"

  maxThreads="1000"

  maxIdleTime="300000"

  minSpareThreads="200"/>

  name:共享线程池的名字。这是Connector为了共享线程池要引用的名字,该名字必须——。默认值:None;

  namePrefix:在JVM上,每个运行线程都可以有一个name 字符串。这一属性为线程池中每个线程的name字符串设置了一个前缀,Tomcat将把线程号追加到这一前缀的后面。默认值:tomcat-exec-;

  maxThreads:该线程池可以容纳的较大线程数。默认值:200;

  maxIdleTime:在Tomcat关闭一个空闲线程之前,允许空闲线程持续的时间(以毫秒为单位)。只有当前活跃的线程数大于minSpareThread的值,才会关闭空闲线程。默认值:60000(一分钟)。

  minSpareThreads:Tomcat应该始终打开的较小不活跃线程数。默认值:25。

  配置Connector

  <Connectorexecutor="tomcatThreadPool"

  port="8080"protocol="HTTP/1.1"

  connectionTimeout="20000"

  redirectPort="8443"

  minProcessors="5"

  maxProcessors="75"

  acceptCount="1000"/>

  executor:表示使用该参数值对应的线程池;

  minProcessors:服务器启动时创建的处理请求的线程数;

  maxProcessors:较大可以创建的处理请求的线程数;

  acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。

  API限流

  秒杀活动中,接口的请求量会是平时的数百倍甚至数千倍,从而有可能导致接口不可用,并引发连锁反应导致整个系统崩溃,甚至有可能会影响到其它服务。

  那么如何应对这种突然事件呢?这里我们采用开源工具包guava提供的限流工具类RateLimiter进行API限流,该类基于"令牌桶算法",开箱即用。

  /**

  *自定义注解限流

  */

  @Target({ElementType.PARAMETER,ElementType.METHOD})

  @Retention(RetentionPolicy.RUNTIME)

  @Documented

  public@interfaceServiceLimit{

  Stringdescription()default"";

  }

  自定义切面

  /**

  *限流AOP

  */

  @Component

  @Scope

  @Aspect

  publicclassLimitAspect{

  //每秒只发出100个令牌,此处是单进程服务的限流,内部采用令牌捅算法实现

  privatestaticRateLimiterrateLimiter=RateLimiter.create(100.0);

  //Service层切点限流

  @Pointcut("@annotation(com.itstyle.seckill.common.aop.ServiceLimit)")

  publicvoidServiceAspect(){

  }

  @Around("ServiceAspect()")

  publicObjectaround(ProceedingJoinPointjoinPoint){

  Booleanflag=rateLimiter.tryAcquire();

  Objectobj=null;

  try{

  if(flag){

  obj=joinPoint.proceed();

  }

  }catch(Throwablee){

  e.printStackTrace();

  }

  returnobj;

  }

  }

  业务实现:

  @Override

  @ServiceLimit

  @Transactional

  publicResultstartSeckil(longseckillId,longuserId){

  //省略部分业务代码,详见秒杀源码

  }

  分布式限流

  Nginx

  #统一在http域中进行配置

  #限制请求

  limit_req_zone$binary_remote_addr$urizone=api_read:20mrate=50r/s;

  #按ip配置一个连接zone

  limit_conn_zone$binary_remote_addrzone=perip_conn:10m;

  #按server配置一个连接zone

  limit_conn_zone$server_namezone=perserver_conn:100m;

  server{

  listen80;

  server_nameseckill.52itstyle.com;

  indexindex.jsp;

  location/{

  #请求限流排队通过burst默认是0

  limit_reqzone=api_readburst=5;

  #连接数限制,每个IP并发请求为2

  limit_connperip_conn2;

  #服务所限制的连接数(即限制了该server并发连接数量)

  limit_connperserver_conn1000;

  #连接限速

  limit_rate100k;

  proxy_passhttp://seckill;

  }

  }

  upstreamseckill{

  fair;

  server172.16.1.120:8080weight=1max_fails=2fail_timeout=30s;

  server172.16.1.130:8080weight=1max_fails=2fail_timeout=30s;

  }

  配置说明

  imit_conn_zone

  是针对每个IP定义一个存储session状态的容器。这个示例中定义了一个100m的容器,按照32bytes/session,可以处理3200000个session。

  limit_rate300k;

  对每个连接限速300k. 注意,这里是对连接限速,而不是对IP限速。如果一个IP允许两个并发连接,那么这个IP就是限速limit_rate×2。

  burst=5;

  这相当于桶的大小,如果某个请求超过了系统处理速度,会被放入桶中,等待被处理。如果桶满了,那么抱歉,请求直接返回503,客户端得到一个服务器忙的响应。如果系统处理请求的速度比较慢,桶里的请求也不能一直待在里面,如果超过一定时间,也是会被直接退回,返回服务器忙的响应。

  OpenResty

  背影有没有很熟悉,对这就是那个直呼理解万岁老罗,2015年老罗在锤子科技T2发布会上将门票收入捐赠给了 OpenResty,也相信老罗是个有情怀的胖子。

  这里我们使用 OpenResty 开源的限流方案,测试案例使用OpenResty1.13.6.1较新版本,自带lua-resty-limit-traffic模块以及案例 ,实现起来更为方便。

  限制接口总并发数/请求数

  秒杀活动中,由于突发流量暴增,有可能会影响整个系统的稳定性从而造成崩溃,这时候我们就要限制秒杀接口的总并发数/请求数。

  这里我们采用 lua-resty-limit-traffic中的resty.limit.count模块实现,由于文章篇幅具体代码参见源码openresty/lua/limit_count.lua。

  限制接口时间窗请求数

  秒杀场景下,有时候并都是人肉鼠标,比如12306的抢票软件,软件刷票可比人肉鼠标快多了。此时我们就要对客户端单位时间内的请求数进行限制,以至于刷票不是那么猖獗。当然了道高一尺魔高一丈,抢票软件总是会有办法绕开你的防线,从另一方面讲也促进了技术的进步。

  这里我们采用 lua-resty-limit-traffic中的resty.limit.conn模块实现,具体代码参见源码openresty/lua/limit_conn.lua。

  平滑限制接口请求数

  之前的限流方式允许突发流量,也就是说瞬时流量都会被允许。突然流量如果不加以限制会影响整个系统的稳定性,因此在秒杀场景中需要对请求×××为平均速率处理,即20r/s。

  这里我们采用 lua-resty-limit-traffic 中的resty.limit.req 模块实现漏桶限流和令牌桶限流。

  其实漏桶和令牌桶根本的区别就是,如何处理超过请求速率的请求。漏桶会把请求放入队列中去等待均速处理,队列满则拒绝服务;令牌桶在桶容量允许的情况下直接处理这些突发请求。

  漏桶

  桶容量大于——,并且是延迟模式。如果桶没满,则进入请求队列以固定速率等待处理,否则请求被拒绝。

  令牌桶

  桶容量大于——,并且是非延迟模式。如果桶中存在令牌,则允许突发流量,否则请求被拒绝。

  压测

  为了测试以上配置效果,我们采用AB压测,Linux下执行以下命令即可:

  #安装

  yum-yinstallhttpd-tools

  #查看ab版本

  ab-v

  #查看帮助

  ab--help

  测试命令:

  ab-n1000-c100http://127.0.0.1/

  测试结果:

  ServerSoftware:openresty/1.13.6.1#服务器软件

  ServerHostname:127.0.0.1#IP

  ServerPort:80#请求端口号

  DocumentPath:/#文件路径

  DocumentLength:12bytes#页面字节数

  ConcurrencyLevel:100#请求的并发数

  Timetakenfortests:4.999seconds#总访问时间

  Completerequests:1000#总请求树

  Failedrequests:0#请求失败数量

  Writeerrors:0

  Totaltransferred:140000bytes#请求总数据大小

  HTMLtransferred:12000bytes#html页面实际总字节数

  Requestspersecond:200.06[#/sec](mean)#每秒多少请求,这个是非常重要的参数数值,服务器的吞吐量

  Timeperrequest:499.857[ms](mean)#用户平均请求等待时间

  Timeperrequest:4.999[ms](mean,acrossallconcurrentrequests)#服务器平均处理时间,也就是服务器吞吐量的倒数

  Transferrate:27.35[Kbytes/sec]received#每秒获取的数据长度

  ConnectionTimes(ms)

  minmean[+/-sd]medianmax

  Connect:000.804

  Processing:547489.1500501

  Waiting:247489.2500501

  Total:947588.4500501

  Percentageoftherequestsservedwithinacertaintime(ms)

  50%500

  66%500

  75%500

  80%500

  90%501

  95%501

  98%501

  99%501

  ——501(longestrequest)

  总结

  以上限流方案,只是针对此次秒杀案例做一个简单的小结,大家也不要刻意区分那种方案的好坏,只要适合业务场景就是较好的。

  参考

  https://github.com/openresty/lua-resty-limit-traffic

  https://blog.52itstyle.com/archives/1764/

 

  https://blog.52itstyle.com/archives/775/

收藏 推荐 打印 | 录入:Cstor | 阅读:
本文评论   查看全部评论 (0)
表情: 表情 姓名: 字数
点评:
       
评论声明
  • 尊重网上道德,遵守中华人民共和国的各项有关法律法规
  • 承担一切因您的行为而直接或间接导致的民事或刑事法律责任
  • 本站管理人员有权保留或删除其管辖留言中的任意内容
  • 本站有权在网站内转载或引用您的评论
  • 参与本评论即表明您已经阅读并接受上述条款
热门评论