停更了很久的《面試補習》
,隨著最近的校招來臨,也要提上日程了,在梳理八股文
的同時,也能加深自己的理解,希望對各位童鞋有所幫助~
概述
在最近一期的文章 給幾位小朋友面試輔導後,我發現了一些問題! 中,有提到面試中,真的童鞋們的專案經驗提出了比較多的問題,也不知道有沒有人看 orz
主要列了一下專案中的這些問題:
- 去理解為什麼你要做秒殺系統?
- 秒殺系統適合什麼場景,不適合什麼場景
- 思考你的系統還有哪些欠缺的地方
- 掌握你係統的每一個點,包括功能,效能,資料流和部署架構
- 技術選型,為什麼你要用 redis ,為什麼要用MQ?
- 技術風險,引用了這些中介軟體,對你的系統帶來的收益和風險
- 怎麼去容災,怎麼監控
今天寫的這片關於限流
文章,也是屬於秒殺系統中的一個關鍵技術點. 會從: 技術原理
,技術選型
,使用場景
等多方面來介紹,讓你在面試中,肆意發揮
。
什麼是限流
講一個大家都懂的例子: 三峽大壩排水
- 三峽水庫的存水:可以理解是我們秒殺活動的使用者
- 放閘 : 活動開始
- 排水 :秒殺成功的使用者
如果沒有 閘口
在, 受到的影響是啥? 下游的村莊經受洪水災難
,而對應你的系統也是一樣的崩潰
!
可能大家有疑問,如果我沒有做這個蓄水的動作
(三峽沒有那麼多水),我是不是就不需要做限流
了呢? 其實不然,我們都知道 三峽解決了多少歷史上造成的洪災問題,這裡找了個科普連結。
那對應到我們的秒殺系統
上, 我們怎麼知道我們的系統會在哪個時間點來一波使用者暴增
呢?如果這時候你沒做好準備,是不是就造成了這批使用者的流失?而且系統癱瘓,對存量使用者也有影響。雙輸
我要這鐵棒有何用~
所以,限流
就是我們系統的定海神針
, 讓我們的系統風平浪靜。
最後再以一批資料來說明一下限流
的實際場景:
1個商品
1秒內
100個名額
5000個使用者
1000個進入下單頁面
4000個超時頁面
100個下單
900個庫存不足
結果:
100個成功下單
4900個搶單失敗
限流量: 1000
思考題
求:我這個服務最大併發量多少?
怎麼限流
簡單畫了個呼叫鏈路
H5/客戶端
-> Nginx
-> Tomcat
-> 秒殺系統
-> DB
簡單梳理為
- 閘道器限流
- Nginx 限流
- Tomcat 限流
- 服務端限流
- 單機限流
- 分散式限流
閘道器限流
Nginx 限流
Nginx自帶了兩個限流模組:
連線數
限流模組 ngx_http_limit_conn_module漏桶演算法實現的請求
限流模組 ngx_http_limit_req_module
1、ngx_http_limit_conn_module
主要用於限制指令碼攻擊,如果我們的秒殺活動開始,一個黑客
(假裝有,畢竟我們的系統要做大做強!)寫了指令碼來攻擊,會造成我們頻寬被浪費,大量無效請求產生,對於這類請求, 我們可以通過對 ip 的連線數進行限制。
我們可以在nginx_conf的http{}中加上如下配置實現限制:
#限制每個使用者的併發連線數,取名one
limit_conn_zone $binary_remote_addr zone=one:10m;
#配置異常日誌,和狀態碼
limit_conn_log_level error;
limit_conn_status 503;
# 在server{} 限制使用者併發連線數為1
limit_conn one 1;
2、ngx_http_limit_req_module
上面說的 是 ip 的連線數, 那麼如果我們要控制請求數呢? 限制的方法是通過使用漏斗演算法,每秒固定處理請求數,推遲過多請求。如果請求的頻率超過了限制域配置的值,請求處理會被延遲或被丟棄,所以所有的請求都是以定義的頻率被處理的。
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
#設定每個IP桶的數量為5
limit_req zone=one burst=5;
3、怎麼理解 連線數,請求數限流
- 連線數限流(ngx_http_limit_conn_module)
每個IP,我們只會接待一個,只有當這個IP 處理結束了, 我才會接待下一位。(單位時間內,只有一個連線在處理)
有味道的解讀:廁所(IP)限制只有一個坑了,只有當我上完了,才能下一個人上。
- 請求數限流(ngx_http_limit_req_module)
通過漏桶演算法
,按照單位時間放行請求,也不管你伺服器能不能處理完,我就放,哎,就是放!
有味道的解讀:廁所有五個坑,我一分鐘放5個人進去,下一分鐘再放5個人進去。 裡面可能有5個人,也可能有10個人,我也不清楚。
4、怎麼選擇?
可能面試官在聽到你對 nginx 的限流那麼瞭解後,會問你在什麼情況下使用哪種限流策略
- IP限流:可以在活動開始前進行配置,也可以用於預防指令碼攻擊(IP代理的情況另說)
- 請求數限流: 日程可以配置,保護我們的伺服器在突發流量造成的崩潰
漏桶演算法
漏桶演算法的主要概念如下:
- 一個固定容量的漏桶,按照常量固定速率流出水滴;
- 如果桶是空的,則不需流出水滴;
- 可以以任意速率流入水滴到漏桶;
- 如果流入水滴超出了桶的容量,則流入的水滴溢位了(被丟棄),而漏桶容量是不變的。
Tomcat 限流
這個其實不太好用,但是也瞭解一下吧~
可能現在的童鞋,對 Tomcat
也不太瞭解了,畢竟 SpringBoot
裡面封裝了 Tomcat
,讓開發者越來越懶惰了,但是人類進化,根本原因就是懶,所以也未嘗不是一件好事。
在 Tomcat 的配置檔案中, 有一個 maxThreads
<Connector port="8080" connectionTimeout="30000" protocol="HTTP/1.1"
maxThreads="1000" redirectPort="8000" />
這個好像沒啥好介紹的了,如果你碰到你壓測的時候,併發上不去,可以檢查一下這個配置。
之前面試的時候,面試官有問過我 Tomcat
的問題:
Tomcat 預設最大連線數是多少?
你們伺服器的執行緒數設定了多少?
執行緒佔用記憶體是多少?
總結
結合我們的 秒殺系統
,那麼在介紹我們系統的時候,我們可以說,在限流這塊,從閘道器角度,我們可以使用了 Nginx
的 ngx_http_limit_conn_module
模組,針對 IP 在單位時間內只允許一個請求,避免使用者多次請求,減輕服務的壓力。在進入到訂單介面後,在單位時間內,會產生多次請求, 可以使用 ngx_http_limit_req_module
模組,針對請求數做限流,避免由於 IP
限制,導致訂單丟失。
除此之外,在服務上線前,我們針對伺服器進行了最大併發的壓測(如200併發
),因此在 Tomcat
允許的最大請求中,設定為(300
,稍微上調,有其他請求)。
伺服器限流
單機限流
如果我們的系統部署,是隻有一臺機器,那我們可以直接使用 單機限流的方案(畢竟你一臺機器還要用分散式限流,是不是有點過了~)
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
例項程式碼
public static void main(String[] args) throws InterruptedException {
// 每秒產生 1 個令牌
RateLimiter rt = RateLimiter.create(1, 1, TimeUnit.SECONDS);
System.out.println("try acquire token: " + rt.tryAcquire(1) + " time:" + System.currentTimeMillis());
System.out.println("try acquire token: " + rt.tryAcquire(1) + " time:" + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("try acquire token: " + rt.tryAcquire(1) + " time:" + System.currentTimeMillis());
System.out.println("try acquire token: " + rt.tryAcquire(1) + " time:" + System.currentTimeMillis());
System.out.println("-------------分隔符-----------------");
}
RateLimiter.tryAcquire()
和 RateLimiter.acquire()
兩個方法都通過限流器獲取令牌,
1、tryAcquire
支援傳入等待時間,通過 canAcquire 判斷最早一個生成令牌時間,判斷是否進行等待下一個令牌的獲取。
public boolean tryAcquire(int permits, long timeout, TimeUnit unit);
private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
示例程式碼:
public static void main(String[] args) throws InterruptedException {
// 每秒產生 1 個令牌
RateLimiter rt = RateLimiter.create(1, 3, TimeUnit.SECONDS);
System.out.println("try acquire token: " + rt.tryAcquire(1,TimeUnit.SECONDS) + " time:" + System.currentTimeMillis());
System.out.println("try acquire token: " + rt.tryAcquire(5,TimeUnit.SECONDS) + " time:" + System.currentTimeMillis());
Thread.sleep(10000);
System.out.println("-------------分隔符-----------------");
System.out.println("try acquire token: " + rt.tryAcquire(1,TimeUnit.SECONDS) + " time:" + System.currentTimeMillis());
System.out.println("try acquire token: " + rt.tryAcquire(1,TimeUnit.SECONDS) + " time:" + System.currentTimeMillis());
}
輸出結果:
2、acquire
acquire 為阻塞等待獲取令牌,通過檢視原始碼可以看出同步加鎖操作:
示例程式碼:
RateLimiter rt = RateLimiter.create(1);
// 每秒產生 1 個令牌
for (int i = 0; i < 11; i++) {
new Thread(() -> {
// 獲取 1 個令牌
rt.acquire();
System.out.println("try acquire token success,time:" +System.currentTimeMillis() + " ThreaName:"+Thread.currentThread().getName());
}).start();
}
輸出結果:
令牌演算法
上面說到了幾個概念, 在nignx
我們提到的是 漏斗演算法
,在 RateLimiter
這裡我們提到的是令牌演算法
我們可以通過上面這個圖來進行解釋,有一個容量有限的桶,令牌以固定的速率新增到這個桶裡面。由於桶的容量是有限的,所以不可能無限制的往裡面新增令牌,如果令牌到達桶的時候,桶是滿的,那麼這個令牌就被拋棄了。每次請求,n個數量的令牌從桶裡面被移除,如果桶裡面的令牌數少於n,那麼該請求就會被拒絕或阻塞。
這裡有幾個關鍵的屬性
/** The currently stored permits. */
double storedPermits; //目前令牌數量
/** The maximum number of stored permits. */
double maxPermits; //最大令牌數量
private long nextFreeTicketMicros = 0L; //下一個令牌獲取時間
在獲取令牌前,會有一個判斷規則,判斷當前獲取令牌時間,是否滿足上一次令牌時間獲取 - 生產令牌時間,
比如
:我這次獲取令牌時間為 100 秒,令牌生成時間為 10秒 一個,那麼當我 105秒過來拿的時候, 不管令牌桶有沒有令牌,我都沒辦法獲取到令牌。
private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
這裡是重點!!!
那麼令牌桶當中的令牌數量(存量)到底有什麼用呢? 針對不同的請求,我們可以設定需要不同數量的令牌,優先順序高的,只需要1個令牌即可;優先順序低的,則需要多個令牌。 那麼當獲取令牌時間到了之後, 進行下一層判斷,令牌數是否足夠, 優先順序高的請求(需要令牌數量比較少的),可以馬上放行!!!!!
在 RateLimit
中重新整理令牌的演算法:
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}
叢集限流
隨著我們秒殺系統
做大做強,一臺機器肯定不能滿足我們的訴求了,那麼我們的部署架構就會衍生成為下面這個架構圖(簡版
)
在將叢集限流前,提個思考問題:
叢集部署我們就不能用單機部署的方案了嗎?
答案肯定是可以的, 我們可以將單機限流
的方案擴充到叢集每一臺機器,那麼每天機器都是複用了相同的一套限流程式碼(RateLimit
實現)。
那麼這個方案存在什麼問題呢?
- 流量分配不均
- 誤限,錯限
- 更新不及時
主要講一下 誤限
, 我們服務端接收到的請求,都是有 nginx
進行分發,如果某個時間段,由於請求的分配不均(60,30,10比例分配,限流50qps)
,會觸發第一臺機器的限流,而對於叢集而言,我的整體限流閥值
為 150 qps
,現在 100qps
就限流了, 那肯定不行哇!
Redis 實現
參考文件: https://juejin.cn/post/6844904161604009997
我們可以藉助 Redis
的有序集合 ZSet
來實現時間視窗演算法限流,實現的過程是先使用 ZSet
的 key
儲存限流的 ID
,score
用來儲存請求的時間,每次有請求訪問來了之後,先清空之前時間視窗的訪問量,統計現在時間視窗的個數和最大允許訪問量對比,如果大於等於最大訪問量則返回 false
執行限流操作,負責允許執行業務邏輯,並且在 ZSet
中新增一條有效的訪問記錄。
此實現方式存在的缺點有兩個:
- 使用 ZSet 儲存有每次的訪問記錄,如果資料量比較大時會佔用大量的空間,比如 60s 允許 100W 訪問時;
- 此程式碼的執行非原子操作,先判斷後增加,中間空隙可穿插其他業務邏輯的執行,最終導致結果不準確。
限流中介軟體
Sentinel
是阿里中介軟體團隊研發的面向分散式服務架構的輕量級高可用流量控制元件,主要以流量為切入點,從流量控制
、熔斷降級
、系統負載保護
等多個維度來幫助使用者保護服務的穩定性。
限流中介軟體的原理是在太有東西了,我這裡簡單裂了一下他們之間的一些區別,後續會單獨寫一篇文章來分享 Sentinel
的實現原理! 目前可以比較容易理解的就是,底層是基於滑動視窗
的方式實現
滑動視窗演算法
在 Sentinel
和 Hystrix
的底層實現,都是採用了滑動視窗
,這裡接簡單來描述一下什麼是滑動視窗,在1S
內, 我允許通過 5個請求, 分別處於 0~200ms
,200~400ms
以此類推,當時間點來到1.2s
的時候,我們的時間區間變成了 200ms ~ 1200ms。 那麼第一個請求,就不在統計的區間範圍內了, 我們目前總的 請求數為 4
, 因此能夠再接受一個新的請求進來處理!
總結
想閒扯一下,在我畫的那張圖中,我列出了 Hystrix
(豪豬),Sentinel
(哨兵)和螞蟻內源的Guardian
(守衛)。他們都有一個共性: 保護
。豪豬有堅硬的刺保護柔軟的身體,哨兵和守衛則保護著身後的家人。
當面試官問你為什麼要使用限流的時候, 你應該第一反應就是保護系統
,保護系統不受傷害!這才是你為什麼要用到限流的各種策略的根本原因。
在討論到高可用的時候,我們會想到,削峰
,限流
和熔斷
。 他們的目標都是為了保護我們的系統,提升系統的可用率,我們常說的系統可用率 幾個9
,這些資料都是由各種高可用的策略來保護的。
後續的計劃:
熔斷
,結合Sentinel
的原理來介紹一下,秒殺系統使用熔斷的場景削峰
,結合RocketMQ
講一下,削峰的優缺點,引入MQ
帶來的成本和風險
點關注,不迷路
好了各位,以上就是這篇文章的全部內容了,我後面會每週都更新幾篇高質量的大廠面試和常用技術棧相關的文章。感謝大夥能看到這裡,如果這個文章寫得還不錯, 求三連!!!
創作不易,感謝各位的支援和認可,我們下篇文章見!
我是 九靈
,有需要交流的童鞋可以 加我wx,Jayce-K
,關注公眾號:Java 補習課
,掌握第一手資料!
如果本篇部落格有任何錯誤,請批評指教,不勝感激 !