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

从构建分布式秒杀系统聊聊限流特技,第1张

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

前言

俗话说,冰冻三尺,非一日之寒,非一日之寒。两周前,秒杀案有了雏形,分享给了国内最大的同性恋交友网站——码云。同时也收到了很多小伙伴的建议和投诉。我从来不认为分销、集群、秒杀应该是大厂的专利。在如今的互联网,你要时刻武装自己。只有这样,也许你的春天就在明天。

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

限流

但是,再牛逼的机器,再优化的设计,我们也需要特殊场景的特殊处理。以秒杀为例,可能有几百万用户抢购,商品数量远远小于用户数量。如果这些请求都进入队列或查询缓存,对最终结果没有任何意义,只会在后台添加华丽的数据。在这方面,为了减少资源浪费,减轻后端压力,我们还需要对spike的电流进行限制,只是为了保证部分用户的正常服务。

就spike接口而言,当访问频率或并发请求超过其承载范围时,那么就要考虑限制电流,以保证接口的可用性,防止系统因为意外请求对系统的压力过大而瘫痪。通常的策略是拒绝冗余访问或对服务的冗余访问进行排队。

限流算法

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

令牌桶

令牌桶算法是网络流量整形和速率限制中最常用的算法。通常,令牌桶算法用于控制发送到网络的数据数量,并允许传输突发数据(百科全书)。

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

漏桶

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

令牌桶就是不管你的流入率有多大,我都会按照既定的速率来处理。如果桶已满,服务将被拒绝。

应用限流 Tomcat

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

默认情况下,Tomcat使用自己的连接池。在这里,我们还可以定制实现,打开/conf/server.xml文件,并在连接器之前配置一个线程池:

<Executorname="tomcatThreadPool" namePrefix="tomcatThreadPool-" maxThreads="1000" maxIdleTime="300000" minSpareThreads="200"/>
  • Name:共享线程池的名称。这是连接器为了共享线程池而引用的名称。该名称必须唯一。默认值:无;;
  • NamePrefix:在JVM上,每个正在运行的线程都可以有一个名称字符串。该属性为线程池中每个线程的名称字符串设置一个前缀,Tomcat会将线程号附加到这个前缀上。默认值:Tomcat-exec-;
  • MaxThreads:该线程池可以容纳的最大线程数。默认值:200;
  • MaxIdleTime:Tomcat关闭空的空闲线程之前,允许该线程持续的时间(以毫秒为单位)。只有当当前活动线程的数量大于minSpareThread的值时,才会关闭空的空闲线程。默认值:60000(一分钟)。
  • SpareThreads:最小数量的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

    如何使用Nginx实现基本的限流,比如单个IP限制每秒50次访问。通过Nginx限流模块,我们可以设置一旦并发连接数超过我们的设置,将不可用的503服务返回给客户端。

    配置nginx.conf #统一在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; } 配置说明

    限制_连接_区域

    为每个IP定义一个存储会话状态的容器。在这个例子中,定义了一个100m的容器,按照32字节/会话,可以处理3,200,000个会话。

    limit_rate300k

    每个连接的速度限制是300k。请注意,这是连接的速度限制,不是IP的速度限制。如果一个IP允许两个并发连接,那么这个IP就是限速limit_rate×2。

    突发=5;

    这相当于水桶的大小。如果一个请求超过了系统的处理速度,它将被放入桶中,等待处理。如果桶满了,那么不好意思,请求直接返回503,客户端得到服务器忙的响应。如果系统处理请求的速度很慢,那么桶中的请求就不能一直留在桶中。超过一定时间就直接返回,返回服务器忙的响应。

    OpenResty

    后面熟悉吗?没错,这就是老罗万岁,他在2015年锤子科技T2发布会上把门票收入给了OpenResty。我也相信老罗是个有情怀的胖子。

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

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

    在尖峰活动期间,流量的突然增加可能会影响整个系统的稳定性,导致崩溃。这时候我们要限制spike接口的总并发数/请求数。

    这里我们使用lua-resty-limit-traffic中的resty.limit.count模块来实现。文章长度请参考源代码openresty/lua/limit_count.lua。

    限制接口时间窗请求数

    在秒杀场景中,有时候不全是人鼠。比如12306的抢票软件,可以比人类的鼠标快很多。在这一点上,我们要限制客户端单位时间的请求次数,让刷票不那么猖狂。当然,道高一尺魔高一丈,抢票软件总会有办法绕过你的防线,另一方面也会推动技术的进步。

    这里我们用lua-resty-limit-traffic中的openresty/lua/limit_conn.lua模块来实现。具体代码请参考源码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 100%501(longestrequest)

    源代码:从0到1构建分布式秒杀系统。

    总结

    上限流量方案只是这个秒杀案例的简单总结,你不要刻意去区分那个方案的好与坏,只要适合业务场景,就是最好的。

    参考

    https://github.com/openresty/Lua-resty-limit-traffic
    https://blog.52itstyle.com/archives/1764/
    https://blog.52itstyle.com/archives/775/

    分享是快乐的,也见证了个人的成长过程。大部分文章都是工作经验的总结和平时学习的积累。基于你自己的认知不足,这是必然的。请大家指正,共同进步。

    本文版权归作者所有,欢迎转载。但是,这种声明必须在未经作者同意的情况下保存,并且要在文章页面的显著位置给出。如有疑问,可发邮件至([email protected])进行咨询。

    欢迎分享,转载请注明来源:内存溢出

    原文地址: http://outofmemory.cn/zz/777456.html

    (0)
    打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
    上一篇 2022-05-03
    下一篇 2022-05-03

    发表评论

    登录后才能评论

    评论列表(0条)

    保存