俗话说的好,冰冻三尺非一日之寒,滴水穿石非一日之功,罗马也不是一天就建成的。两周前秒杀案例初步成型,分享到了中国最大的同性交友网站-码云。同时也收到了很多小伙伴的建议和投诉。我从不认为分布式、集群、秒杀这些就应该是大厂的专利,在互联网的今天不管何时都要时刻武装本身,只有这样,也许你的春天就在明天。html
在开发秒杀系统案例的过程当中,前面主要分享了队列、缓存、锁和分布式锁以及静态化等等。缓存的目的是为了提高系统访问速度和加强系统的处理能力;分布式锁解决了集群下数据的安全一致性问题;静态化无疑是减轻了缓存以及DB层的压力。java
然而再牛逼的机器,再优化的设计,对于特殊场景咱们也是要特殊处理的。就拿秒杀来讲,可能会有百万级别的用户进行抢购,而商品数量远远小于用户数量。若是这些请求都进入队列或者查询缓存,对于最终结果没有任何意义,徒增后台华丽的数据。对此,为了减小资源浪费,减轻后端压力,咱们还须要对秒杀进行限流,只需保障部分用户服务正常便可。nginx
就秒杀接口来讲,当访问频率或者并发请求超过其承受范围的时候,这时候咱们就要考虑限流来保证接口的可用性,以防止非预期的请求对系统压力过大而引发的系统瘫痪。一般的策略就是拒绝多余的访问,或者让多余的访问排队等待服务。git
任何限流都不是漫无目的的,也不是一个开关就能够解决的问题,经常使用的限流算法有:令牌桶,漏桶。github
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型状况下,令牌桶算法用来控制发送到网络上的数据的数目,并容许突发数据的发送(百科)。算法
在秒杀活动中,用户的请求速率是不固定的,这里咱们假定为10r/s,令牌按照5个每秒的速率放入令牌桶,桶中最多存放20个令牌。仔细想一想,是否是总有那么一部分请求被丢弃。spring
漏桶算法的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,经过它,突发流量能够被整形以便为网络提供一个稳定的流量(百科)。后端
令牌桶是不管你流入速率多大,我都按照既定的速率去处理,若是桶满则拒绝服务。api
在Tomcat容器中,咱们能够经过自定义线程池,配置最大链接数,请求处理队列等参数来达到限流的目的。缓存
Tomcat默认使用自带的链接池,这里咱们也能够自定义实现,打开/conf/server.xml文件,在Connector以前配置一个线程池:
<Executor name="tomcatThreadPool" namePrefix="tomcatThreadPool-" maxThreads="1000" maxIdleTime="300000" minSpareThreads="200"/>
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" minProcessors="5" maxProcessors="75" acceptCount="1000"/>
秒杀活动中,接口的请求量会是平时的数百倍甚至数千倍,从而有可能致使接口不可用,并引起连锁反应致使整个系统崩溃,甚至有可能会影响到其它服务。
那么如何应对这种忽然事件呢?这里咱们采用开源工具包guava提供的限流工具类RateLimiter进行API限流,该类基于"令牌桶算法",开箱即用。
自定义定义注解
/** * 自定义注解 限流 */ @Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ServiceLimit { String description() default ""; }
自定义切面
/** * 限流 AOP */ @Component @Scope @Aspect public class LimitAspect { //每秒只发出100个令牌,此处是单进程服务的限流,内部采用令牌捅算法实现 private static RateLimiter rateLimiter = RateLimiter.create(100.0); //Service层切点 限流 @Pointcut("@annotation(com.itstyle.seckill.common.aop.ServiceLimit)") public void ServiceAspect() { } @Around("ServiceAspect()") public Object around(ProceedingJoinPoint joinPoint) { Boolean flag = rateLimiter.tryAcquire(); Object obj = null; try { if(flag){ obj = joinPoint.proceed(); } } catch (Throwable e) { e.printStackTrace(); } return obj; } }
业务实现:
@Override @ServiceLimit @Transactional public Result startSeckil(long seckillId, long userId) { //省略部分业务代码,详见秒杀源码 }
如何使用Nginx实现基本的限流,好比单个IP限制每秒访问50次。经过Nginx限流模块,咱们能够设置一旦并发链接数超过咱们的设置,将返回503错误给客户端。
#统一在http域中进行配置 #限制请求 limit_req_zone $binary_remote_addr $uri zone=api_read:20m rate=50r/s; #按ip配置一个链接 zone limit_conn_zone $binary_remote_addr zone=perip_conn:10m; #按server配置一个链接 zone limit_conn_zone $server_name zone=perserver_conn:100m; server { listen 80; server_name seckill.52itstyle.com; index index.jsp; location / { #请求限流排队经过 burst默认是0 limit_req zone=api_read burst=5; #链接数限制,每一个IP并发请求为2 limit_conn perip_conn 2; #服务所限制的链接数(即限制了该server并发链接数量) limit_conn perserver_conn 1000; #链接限速 limit_rate 100k; proxy_pass http://seckill; } } upstream seckill { fair; server 172.16.1.120:8080 weight=1 max_fails=2 fail_timeout=30s; server 172.16.1.130:8080 weight=1 max_fails=2 fail_timeout=30s; }
imit_conn_zone
是针对每一个IP定义一个存储session状态的容器。这个示例中定义了一个100m的容器,按照32bytes/session,能够处理3200000个session。
limit_rate 300k;
对每一个链接限速300k. 注意,这里是对链接限速,而不是对IP限速。若是一个IP容许两个并发链接,那么这个IP就是限速limit_rate×2。
burst=5;
这至关于桶的大小,若是某个请求超过了系统处理速度,会被放入桶中,等待被处理。若是桶满了,那么抱歉,请求直接返回503,客户端获得一个服务器忙的响应。若是系统处理请求的速度比较慢,桶里的请求也不能一直待在里面,若是超过必定时间,也是会被直接退回,返回服务器忙的响应。
背影有没有很熟悉,对这就是那个直呼理解万岁老罗,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 -y install httpd-tools # 查看ab版本 ab -v # 查看帮助 ab --help
测试命令:
ab -n 1000 -c 100 http://127.0.0.1/
测试结果:
Server Software: openresty/1.13.6.1 #服务器软件 Server Hostname: 127.0.0.1 #IP Server Port: 80 #请求端口号 Document Path: / #文件路径 Document Length: 12 bytes #页面字节数 Concurrency Level: 100 #请求的并发数 Time taken for tests: 4.999 seconds #总访问时间 Complete requests: 1000 #总请求树 Failed requests: 0 #请求失败数量 Write errors: 0 Total transferred: 140000 bytes #请求总数据大小 HTML transferred: 12000 bytes #html页面实际总字节数 Requests per second: 200.06 [#/sec] (mean) #每秒多少请求,这个是很是重要的参数数值,服务器的吞吐量 Time per request: 499.857 [ms] (mean) #用户平均请求等待时间 Time per request: 4.999 [ms] (mean, across all concurrent requests) # 服务器平均处理时间,也就是服务器吞吐量的倒数 Transfer rate: 27.35 [Kbytes/sec] received #每秒获取的数据长度 Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.8 0 4 Processing: 5 474 89.1 500 501 Waiting: 2 474 89.2 500 501 Total: 9 475 88.4 500 501 Percentage of the requests served within a certain time (ms) 50% 500 66% 500 75% 500 80% 500 90% 501 95% 501 98% 501 99% 501 100% 501 (longest request)
以上限流方案,只是针对这次秒杀案例作一个简单的小结,你们也不要刻意区分那种方案的好坏,只要适合业务场景就是最好的。
https://github.com/openresty/lua-resty-limit-traffic https://blog.52itstyle.com/archives/1764/ https://blog.52itstyle.com/archives/775/