俗話說的好,冰凍三尺非一日之寒,滴水穿石非一日之功,羅馬也不是一天就建成的。兩週前秒殺案例初步成型,分享到了中國最大的同性交友網站-碼雲。同時也收到了很多小夥伴的建議和投訴。我從不認爲分佈式、集羣、秒殺這些就應該是大廠的專利,在互聯網的今天不管何時都要時刻武裝本身,只有這樣,也許你的春天就在明天。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/