從構(gòu)建分布式秒殺系統(tǒng)聊聊限流特技
前言
俗話說(shuō)的好,冰凍三尺非一日之寒,滴水穿石非一日之功,羅馬也不是一天就建成的。兩周前秒殺案例初步成型,分享到了中國(guó)***的同×××友網(wǎng)站-碼云。同時(shí)也收到了不少小伙伴的建議和投訴。我從不認(rèn)為分布式、集群、秒殺這些就應(yīng)該是大廠的專利,在互聯(lián)網(wǎng)的今天無(wú)論什么時(shí)候都要時(shí)刻武裝自己,只有這樣,也許你的春天就在明天。
在開發(fā)秒殺系統(tǒng)案例的過程中,前面主要分享了隊(duì)列、緩存、鎖和分布式鎖以及靜態(tài)化等等。緩存的目的是為了提升系統(tǒng)訪問速度和增強(qiáng)系統(tǒng)的處理能力;分布式鎖解決了集群下數(shù)據(jù)的安全一致性問題;靜態(tài)化無(wú)疑是減輕了緩存以及DB層的壓力。
限流
然而再牛逼的機(jī)器,再優(yōu)化的設(shè)計(jì),對(duì)于特殊場(chǎng)景我們也是要特殊處理的。就拿秒殺來(lái)說(shuō),可能會(huì)有***別的用戶進(jìn)行搶購(gòu),而商品數(shù)量遠(yuǎn)遠(yuǎn)小于用戶數(shù)量。如果這些請(qǐng)求都進(jìn)入隊(duì)列或者查詢緩存,對(duì)于最終結(jié)果沒有任何意義,徒增后臺(tái)華麗的數(shù)據(jù)。對(duì)此,為了減少資源浪費(fèi),減輕后端壓力,我們還需要對(duì)秒殺進(jìn)行限流,只需保障部分用戶服務(wù)正常即可。
就秒殺接口來(lái)說(shuō),當(dāng)訪問頻率或者并發(fā)請(qǐng)求超過其承受范圍的時(shí)候,這時(shí)候我們就要考慮限流來(lái)保證接口的可用性,以防止非預(yù)期的請(qǐng)求對(duì)系統(tǒng)壓力過大而引起的系統(tǒng)癱瘓。通常的策略就是拒絕多余的訪問,或者讓多余的訪問排隊(duì)等待服務(wù)。
限流算法
任何限流都不是漫無(wú)目的的,也不是一個(gè)開關(guān)就可以解決的問題,常用的限流算法有:令牌桶,漏桶。
令牌桶
令牌桶算法是網(wǎng)絡(luò)流量×××(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型情況下,令牌桶算法用來(lái)控制發(fā)送到網(wǎng)絡(luò)上的數(shù)據(jù)的數(shù)目,并允許突發(fā)數(shù)據(jù)的發(fā)送(百科)。
在秒殺活動(dòng)中,用戶的請(qǐng)求速率是不固定的,這里我們假定為10r/s,令牌按照5個(gè)每秒的速率放入令牌桶,桶中最多存放20個(gè)令牌。仔細(xì)想想,是不是總有那么一部分請(qǐng)求被丟棄。
漏桶
漏桶算法的主要目的是控制數(shù)據(jù)注入到網(wǎng)絡(luò)的速率,平滑網(wǎng)絡(luò)上的突發(fā)流量。漏桶算法提供了一種機(jī)制,通過它,突發(fā)流量可以被×××以便為網(wǎng)絡(luò)提供一個(gè)穩(wěn)定的流量(百科)。
令牌桶是無(wú)論你流入速率多大,我都按照既定的速率去處理,如果桶滿則拒絕服務(wù)。
應(yīng)用限流
Tomcat
在Tomcat容器中,我們可以通過自定義線程池,配置***連接數(shù),請(qǐng)求處理隊(duì)列等參數(shù)來(lái)達(dá)到限流的目的(圖片源自網(wǎng)絡(luò))。
Tomcat默認(rèn)使用自帶的連接池,這里我們也可以自定義實(shí)現(xiàn),打開/conf/server.xml文件,在Connector之前配置一個(gè)線程池:
- <Executor name="tomcatThreadPool"
- namePrefix="tomcatThreadPool-"
- maxThreads="1000"
- maxIdleTime="300000"
- minSpareThreads="200"/>
- name:共享線程池的名字。這是Connector為了共享線程池要引用的名字,該名字必須唯一。默認(rèn)值:None;
- namePrefix:在JVM上,每個(gè)運(yùn)行線程都可以有一個(gè)name 字符串。這一屬性為線程池中每個(gè)線程的name字符串設(shè)置了一個(gè)前綴,Tomcat將把線程號(hào)追加到這一前綴的后面。默認(rèn)值:tomcat-exec-;
- maxThreads:該線程池可以容納的***線程數(shù)。默認(rèn)值:200;
- maxIdleTime:在Tomcat關(guān)閉一個(gè)空閑線程之前,允許空閑線程持續(xù)的時(shí)間(以毫秒為單位)。只有當(dāng)前活躍的線程數(shù)大于minSpareThread的值,才會(huì)關(guān)閉空閑線程。默認(rèn)值:60000(一分鐘)。
- minSpareThreads:Tomcat應(yīng)該始終打開的最小不活躍線程數(shù)。默認(rèn)值:25。
配置Connector
- <Connector executor="tomcatThreadPool"
- port="8080" protocol="HTTP/1.1"
- connectionTimeout="20000"
- redirectPort="8443"
- minProcessors="5"
- maxProcessors="75"
- acceptCount="1000"/>
- executor:表示使用該參數(shù)值對(duì)應(yīng)的線程池;
- minProcessors:服務(wù)器啟動(dòng)時(shí)創(chuàng)建的處理請(qǐng)求的線程數(shù);
- maxProcessors:***可以創(chuàng)建的處理請(qǐng)求的線程數(shù);
- acceptCount:指定當(dāng)所有可以使用的處理請(qǐng)求的線程數(shù)都被使用時(shí),可以放到處理隊(duì)列中的請(qǐng)求數(shù),超過這個(gè)數(shù)的請(qǐng)求將不予處理。
API限流
秒殺活動(dòng)中,接口的請(qǐng)求量會(huì)是平時(shí)的數(shù)百倍甚至數(shù)千倍,從而有可能導(dǎo)致接口不可用,并引發(fā)連鎖反應(yīng)導(dǎo)致整個(gè)系統(tǒng)崩潰,甚至有可能會(huì)影響到其它服務(wù)。
那么如何應(yīng)對(duì)這種突然事件呢?這里我們采用開源工具包guava提供的限流工具類RateLimiter進(jìn)行API限流,該類基于"令牌桶算法",開箱即用。
- /**
- * 自定義注解 限流
- */
- @Target({ElementType.PARAMETER, ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface ServiceLimit {
- String description() default "";
- }
自定義切面
- /**
- * 限流 AOP
- */
- @Component
- @Scope
- @Aspect
- public class LimitAspect {
- //每秒只發(fā)出100個(gè)令牌,此處是單進(jìn)程服務(wù)的限流,內(nèi)部采用令牌捅算法實(shí)現(xiàn)
- private static RateLimiter rateLimiter = RateLimiter.create(100.0);
- //Service層切點(diǎn) 限流
- @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;
- }
- }
業(yè)務(wù)實(shí)現(xiàn):
- @Override
- @ServiceLimit
- @Transactional
- public Result startSeckil(long seckillId, long userId) {
- //省略部分業(yè)務(wù)代碼,詳見秒殺源碼
- }
分布式限流
Nginx
- #統(tǒng)一在http域中進(jìn)行配置
- #限制請(qǐng)求
- limit_req_zone $binary_remote_addr $uri zone=api_read:20m rate=50r/s;
- #按ip配置一個(gè)連接 zone
- limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
- #按server配置一個(gè)連接 zone
- limit_conn_zone $server_name zone=perserver_conn:100m;
- server {
- listen 80;
- server_name seckill.52itstyle.com;
- index index.jsp;
- location / {
- #請(qǐng)求限流排隊(duì)通過 burst默認(rèn)是0
- limit_req zone=api_read burst=5;
- #連接數(shù)限制,每個(gè)IP并發(fā)請(qǐng)求為2
- limit_conn perip_conn 2;
- #服務(wù)所限制的連接數(shù)(即限制了該server并發(fā)連接數(shù)量)
- 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;
- }
配置說(shuō)明
- imit_conn_zone
是針對(duì)每個(gè)IP定義一個(gè)存儲(chǔ)session狀態(tài)的容器。這個(gè)示例中定義了一個(gè)100m的容器,按照32bytes/session,可以處理3200000個(gè)session。
- limit_rate 300k;
對(duì)每個(gè)連接限速300k. 注意,這里是對(duì)連接限速,而不是對(duì)IP限速。如果一個(gè)IP允許兩個(gè)并發(fā)連接,那么這個(gè)IP就是限速limit_rate×2。
- burst=5;
這相當(dāng)于桶的大小,如果某個(gè)請(qǐng)求超過了系統(tǒng)處理速度,會(huì)被放入桶中,等待被處理。如果桶滿了,那么抱歉,請(qǐng)求直接返回503,客戶端得到一個(gè)服務(wù)器忙的響應(yīng)。如果系統(tǒng)處理請(qǐng)求的速度比較慢,桶里的請(qǐng)求也不能一直待在里面,如果超過一定時(shí)間,也是會(huì)被直接退回,返回服務(wù)器忙的響應(yīng)。
OpenResty
背影有沒有很熟悉,對(duì)這就是那個(gè)直呼理解萬(wàn)歲老羅,2015年老羅在錘子科技T2發(fā)布會(huì)上將門票收入捐贈(zèng)給了 OpenResty,也相信老羅是個(gè)有情懷的胖子。
這里我們使用 OpenResty 開源的限流方案,測(cè)試案例使用OpenResty1.13.6.1***版本,自帶lua-resty-limit-traffic模塊以及案例 ,實(shí)現(xiàn)起來(lái)更為方便。
限制接口總并發(fā)數(shù)/請(qǐng)求數(shù)
秒殺活動(dòng)中,由于突發(fā)流量暴增,有可能會(huì)影響整個(gè)系統(tǒng)的穩(wěn)定性從而造成崩潰,這時(shí)候我們就要限制秒殺接口的總并發(fā)數(shù)/請(qǐng)求數(shù)。
這里我們采用 lua-resty-limit-traffic中的resty.limit.count模塊實(shí)現(xiàn),由于文章篇幅具體代碼參見源碼openresty/lua/limit_count.lua。
限制接口時(shí)間窗請(qǐng)求數(shù)
秒殺場(chǎng)景下,有時(shí)候并都是人肉鼠標(biāo),比如12306的搶票軟件,軟件刷票可比人肉鼠標(biāo)快多了。此時(shí)我們就要對(duì)客戶端單位時(shí)間內(nèi)的請(qǐng)求數(shù)進(jìn)行限制,以至于刷票不是那么猖獗。當(dāng)然了道高一尺魔高一丈,搶票軟件總是會(huì)有辦法繞開你的防線,從另一方面講也促進(jìn)了技術(shù)的進(jìn)步。
這里我們采用 lua-resty-limit-traffic中的resty.limit.conn模塊實(shí)現(xiàn),具體代碼參見源碼openresty/lua/limit_conn.lua。
平滑限制接口請(qǐng)求數(shù)
之前的限流方式允許突發(fā)流量,也就是說(shuō)瞬時(shí)流量都會(huì)被允許。突然流量如果不加以限制會(huì)影響整個(gè)系統(tǒng)的穩(wěn)定性,因此在秒殺場(chǎng)景中需要對(duì)請(qǐng)求×××為平均速率處理,即20r/s。
這里我們采用 lua-resty-limit-traffic 中的resty.limit.req 模塊實(shí)現(xiàn)漏桶限流和令牌桶限流。
其實(shí)漏桶和令牌桶根本的區(qū)別就是,如何處理超過請(qǐng)求速率的請(qǐng)求。漏桶會(huì)把請(qǐng)求放入隊(duì)列中去等待均速處理,隊(duì)列滿則拒絕服務(wù);令牌桶在桶容量允許的情況下直接處理這些突發(fā)請(qǐng)求。
漏桶
桶容量大于零,并且是延遲模式。如果桶沒滿,則進(jìn)入請(qǐng)求隊(duì)列以固定速率等待處理,否則請(qǐng)求被拒絕。
令牌桶
桶容量大于零,并且是非延遲模式。如果桶中存在令牌,則允許突發(fā)流量,否則請(qǐng)求被拒絕。
壓測(cè)
為了測(cè)試以上配置效果,我們采用AB壓測(cè),Linux下執(zhí)行以下命令即可:
- # 安裝
- yum -y install httpd-tools
- # 查看ab版本
- ab -v
- # 查看幫助
- ab --help
測(cè)試命令:
- ab -n 1000 -c 100 http://127.0.0.1/
測(cè)試結(jié)果:
- Server Software: openresty/1.13.6.1 #服務(wù)器軟件
- Server Hostname: 127.0.0.1 #IP
- Server Port: 80 #請(qǐng)求端口號(hào)
- Document Path: / #文件路徑
- Document Length: 12 bytes #頁(yè)面字節(jié)數(shù)
- Concurrency Level: 100 #請(qǐng)求的并發(fā)數(shù)
- Time taken for tests: 4.999 seconds #總訪問時(shí)間
- Complete requests: 1000 #總請(qǐng)求樹
- Failed requests: 0 #請(qǐng)求失敗數(shù)量
- Write errors: 0
- Total transferred: 140000 bytes #請(qǐng)求總數(shù)據(jù)大小
- HTML transferred: 12000 bytes #html頁(yè)面實(shí)際總字節(jié)數(shù)
- Requests per second: 200.06 [#/sec] (mean) #每秒多少請(qǐng)求,這個(gè)是非常重要的參數(shù)數(shù)值,服務(wù)器的吞吐量
- Time per request: 499.857 [ms] (mean) #用戶平均請(qǐng)求等待時(shí)間
- Time per request: 4.999 [ms] (mean, across all concurrent requests) # 服務(wù)器平均處理時(shí)間,也就是服務(wù)器吞吐量的倒數(shù)
- Transfer rate: 27.35 [Kbytes/sec] received #每秒獲取的數(shù)據(jù)長(zhǎng)度
- 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)
總結(jié)
以上限流方案,只是針對(duì)此次秒殺案例做一個(gè)簡(jiǎn)單的小結(jié),大家也不要刻意區(qū)分那種方案的好壞,只要適合業(yè)務(wù)場(chǎng)景就是***的。
參考
https://github.com/openresty/lua-resty-limit-traffic
https://blog.52itstyle.com/archives/1764/
https://blog.52itstyle.com/archives/775/
分享是快樂的,也見證了個(gè)人成長(zhǎng)歷程,文章大多都是工作經(jīng)驗(yàn)總結(jié)以及平時(shí)學(xué)習(xí)積累,基于自身認(rèn)知不足之處在所難免,也請(qǐng)大家指正,共同進(jìn)步。
本文版權(quán)歸作者所有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出, 如有問題, 可郵件(345849402@qq.com)咨詢。

































