全站資源開放下載,感謝廣大網友的支持
鏈接失效請移步職業司平臺
非盈利平臺

非盈利平臺

只為分享一些優質內容

Java幫幫-微信公眾號

Java幫幫-微信公眾號

將分享做到極致

微信小程序

微信小程序

更方便的閱讀

職業司微信公眾號

職業司微信公眾號

實時動態通知

安卓APP

安卓APP

我們從此不分開

程序員生活志-公眾號

程序員生活志-公眾號

程序員生活學習圈,互聯網八卦黑料

支付寶贊助-Java幫幫社區
微信贊助-Java幫幫社區

從構建分布式秒殺系統聊聊限流特技

5
發表時間:2018-11-08 12:01來源:Java幫幫-微信公眾號


前言

俗話說的好,冰凍三尺非一日之寒,滴水穿石非一日之功,羅馬也不是一天就建成的。兩周前秒殺案例初步成型,分享到了中國最大的同性交友網站-碼云。同時也收到了不少小伙伴的建議和投訴。我從不認為分布式、集群、秒殺這些就應該是大廠的專利,在互聯網的今天無論什么時候都要時刻武裝自己,只有這樣,也許你的春天就在明天。

在開發秒殺系統案例的過程中,前面主要分享了隊列、緩存、鎖和分布式鎖以及靜態化等等。緩存的目的是為了提升系統訪問速度和增強系統的處理能力;分布式鎖解決了集群下數據的安全一致性問題;靜態化無疑是減輕了緩存以及DB層的壓力。

限流

然而再牛逼的機器,再優化的設計,對于特殊場景我們也是要特殊處理的。就拿秒殺來說,可能會有百萬級別的用戶進行搶購,而商品數量遠遠小于用戶數量。如果這些請求都進入隊列或者查詢緩存,對于最終結果沒有任何意義,徒增后臺華麗的數據。對此,為了減少資源浪費,減輕后端壓力,我們還需要對秒殺進行限流,只需保障部分用戶服務正常即可。

就秒殺接口來說,當訪問頻率或者并發請求超過其承受范圍的時候,這時候我們就要考慮限流來保證接口的可用性,以防止非預期的請求對系統壓力過大而引起的系統癱瘓。通常的策略就是拒絕多余的訪問,或者讓多余的訪問排隊等待服務。

限流算法

任何限流都不是漫無目的的,也不是一個開關就可以解決的問題,常用的限流算法有:令牌桶,漏桶。

令牌桶

令牌桶算法是網絡流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型情況下,令牌桶算法用來控制發送到網絡上的數據的數目,并允許突發數據的發送(百科)。


在秒殺活動中,用戶的請求速率是不固定的,這里我們假定為10r/s,令牌按照5個每秒的速率放入令牌桶,桶中最多存放20個令牌。仔細想想,是不是總有那么一部分請求被丟棄。

漏桶

漏桶算法的主要目的是控制數據注入到網絡的速率,平滑網絡上的突發流量。漏桶算法提供了一種機制,通過它,突發流量可以被整形以便為網絡提供一個穩定的流量(百科)。


令牌桶是無論你流入速率多大,我都按照既定的速率去處理,如果桶滿則拒絕服務。

應用限流

Tomcat

在Tomcat容器中,我們可以通過自定義線程池,配置最大連接數,請求處理隊列等參數來達到限流的目的。


Tomcat默認使用自帶的連接池,這里我們也可以自定義實現,打開/conf/server.xml文件,在Connector之前配置一個線程池:

<Executorname="tomcatThreadPool"        namePrefix="tomcatThreadPool-"        maxThreads="1000"        maxIdleTime="300000"        minSpareThreads="200"/>

  • name:共享線程池的名字。這是Connector為了共享線程池要引用的名字,該名字必須唯一。默認值:None;

  • namePrefix:在JVM上,每個運行線程都可以有一個name 字符串。這一屬性為線程池中每個線程的name字符串設置了一個前綴,Tomcat將把線程號追加到這一前綴的后面。默認值:tomcat-exec-;

  • maxThreads:該線程池可以容納的最大線程數。默認值:200;

  • maxIdleTime:在Tomcat關閉一個空閑線程之前,允許空閑線程持續的時間(以毫秒為單位)。只有當前活躍的線程數大于minSpareThread的值,才會關閉空閑線程。默認值:60000(一分鐘)。

  • 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)@Documentedpublic@interface ServiceLimit {        String description()default "";}

自定義切面

/** * 限流 AOP */@Component@Scope@AspectpublicclassLimitAspect{    //每秒只發出100個令牌,此處是單進程服務的限流,內部采用令牌捅算法實現privatestatic   RateLimiter rateLimiter = RateLimiter.create(100.0);    //Service層切點  限流@Pointcut("@annotation(com.itstyle.seckill.common.aop.ServiceLimit)")      publicvoidServiceAspect(){    }    @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@Transactionalpublic Result startSeckil(    long seckillId, long userId){        //省略部分業務代碼,詳見秒殺源碼}

分布式限流

Nginx

如何使用Nginx實現基本的限流,比如單個IP限制每秒訪問50次。通過Nginx限流模塊,我們可以設置一旦并發連接數超過我們的設置,將返回503錯誤給客戶端。

配置nginx.conf

#統一在http域中進行配置#限制請求limit_req_zone $binary_remote_addr $uri zone=api_read:20m rate=50r/s;#按ip配置一個連接 zonelimit_conn_zone $binary_remote_addr zone=perip_conn:10m;#按server配置一個連接 zonelimit_conn_zone $server_name zone=perserver_conn:100m;server {            listen80;            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,客戶端得到一個服務器忙的響應。如果系統處理請求的速度比較慢,桶里的請求也不能一直待在里面,如果超過一定時間,也是會被直接退回,返回服務器忙的響應。

OpenResty

背影有沒有很熟悉,對這就是那個直呼理解萬歲老羅,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-n1000-c100http://127.0.0.1/

測試結果:

Server Software:        openresty/1.13.6.1#服務器軟件Server Hostname:        127.0.0.1#IPServer 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:           0Total 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   maxConnect:        000.804Processing:     547489.1500501Waiting:        247489.2500501Total:          947588.4500501Percentage of the requests served within a certain time (ms)  50%    50066%    50075%    50080%    50090%    50195%    50198%    50199%    501100%    501 (longest request)


Java幫幫學習群生態

Java幫幫學習群生態

總有一款能幫到你

Java學習群

Java學習群

與大牛一起交流

大數據學習群

大數據學習群

在數據中成長

九點編程學習群

九點編程學習群

深夜九點學編程

python學習群

python學習群

人工智能,爬蟲

測試學習群

測試學習群

感受測試的魅力

Java幫幫生態承諾

Java幫幫生態承諾

一直堅守,不負重望

初心
勤儉
誠信
正義
分享
友鏈交換:加幫主QQ2524138991 留言即可 24小時內答復  
業司
教育資訊
會員登錄
獲取驗證碼
登錄
登錄
我的資料
留言
回到頂部