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