對於秒殺業務,你們應該比較熟悉了。好比,「某商品原價 1299 元, 雙十一整點秒殺價僅 500 元,限量 100 件,先到先得」 等等。經過這段文案咱們可以發現,參與秒殺活動商品的價格都比平時低不少,所以會吸引大量的用戶來搶購。從而不難發現,對於秒殺系統的挑戰就是:在流量瞬時突增的狀況下,如何作依舊可以保證系統的穩定性。java
一、基於合法性限流。算法
二、基於負載限流。數據庫
三、基於服務限流。後端
四、基於監控限流。瀏覽器
那舉個例子來講,好比說我如今橫軸是時間軸,縱軸是用戶的併發訪問量,在橫軸的 S 點就是秒殺的開始時刻。緩存
那通常而言,在秒殺開始以前,用戶的訪問量是一條比較平滑的曲線,但隨着秒殺活動的開始,用戶的訪問量會急劇的增大,而且隨着秒殺的結束,訪問量又會急劇的降低。服務器
咱們假設在初始時用戶的訪問量在 1 萬左右浮動,秒殺服務器可以承受的極限是 5 萬,但在秒殺活動期間,用戶的實際訪問量可能達到了 100萬。markdown
那麼很明顯,100 萬的訪問量已經遠遠超過了系統可以正常承載的 5 萬併發量,所以這時候就可能會致使秒殺系統的不穩定,甚至宕機的狀況。網絡
因此咱們如今不得不提到剛纔所說的了,秒殺系統面臨的最大挑戰就是如何要保證在流量突增的狀況下,仍然保證系統的穩定性。併發
那麼在實際開發中,其實有不少方案均可以保證系統的穩定性。 而咱們本場 Chat 要分享的重點就是說如何要經過限流策略抵禦秒殺期間的流量峯值,從而實現穩定性,那一塊兒來看一下具體怎麼操做。
當海量請求到來時,咱們能夠對請求進行層層設卡、層層攔截,最終將海量請求削減成服務器可以處理的請求。
屢次限流
那舉個例子來講,好比秒殺開始時,可能有 100萬的請求同時撲向服務器。若是從多層限流的角度來講,咱們就能夠在第 1 層把流量先削減成 30 萬,而後在第 2 層減到 10 萬,再在第 3 層減到 5 萬,而後直接將這 5 萬直接處理就能夠了。
不難發現,在限流時,咱們既要層層限流,也要儘早限流,由於上游攔截的請求越多,下游的流量就越少。
那接下來咱們就一塊兒看一看到底如何進行層層限流。
先看一下低層限流又是合法性限流,爲了更好的解決這個問題,咱們先須要看一下到底什麼是合法性限流?
合法性限流指的是僅僅限制那些合法的用戶請求可以抵達到秒殺服務器,而將一些非法的請求所有進行攔截掉。那所以這裏就須要注意了,在請求合法性限流之前,就得先知道哪些請求是合法的,哪些是非法的。
舉一些非法的例子。好比在秒殺活動期間,那實際參與秒殺活動的用戶多是人,也多是機器人,而且還可能存在同一用戶反覆購買同一件商品的行爲,也是咱們說的刷單行爲。
那麼顯然機器人和用戶刷單都是一種不合理的行爲,這種行爲會影響到其餘正經常使用戶的購物體驗,所以就屬於不合法的請求。
而關於如何限制這些不合法的請求,那麼就得具體問題具體分析和討論了。
好比說,若是非法請求的發起者是機器人,那麼最容易想到的方法就是使用驗證碼。而且驗證碼還有一個做用,它能夠拉長用戶的訪問時間。
舉個例子,假設某一秒鐘有 100 萬個用戶同時下單,但若是使用了驗證碼,那麼用戶從輸入驗證碼到整個下單的整個過程就可能須要三秒鐘,也就是說下單量仍然是 100 萬不變,但下單的整體時間可能從 1 秒鐘拉長到了 3 秒,那麼原來須要 1 秒的時間,如今就須要 3 秒的世界,原來 100 萬的請求,如今每秒鐘就只須要處理 33 萬,所以也能夠下降流量的峯值。
再來看一下IP限制,若是經過網絡技術監測到了某個 IP 下的下單頻率在毫秒級別,或者反覆購買同一件商品,那麼就能判定下單的是機器人或者是不合法的用戶,這樣咱們就能夠將這個 IP 加到黑名單之中,從而減小不合法的流量。
那還有一種作法是隱藏秒殺的入口地址,它指的是在秒殺開始以前,服務器並不會向外界暴露秒殺服務的地址,當秒殺服務開始以後纔開放地址。
那到這裏,第 1 層合法性線路就講解完了,接下來咱們再看一下第二層限流,也就是負載限流。
先看一下負載限流的理論基礎是什麼?一個是集羣,一個是網絡 7 層模型。
咱們在搭建集羣時常常會用到一些工具,好比說 Nginx 和 LVS,那這些均可以用於負載限流。
假設通過了第 1 層合法性限流之後,還剩 33 萬的請求,若是經過集羣搭建了三臺服務器,那麼每臺服務器也就只須要承載 11 萬的請求量了,那這樣也能下降請求的併發量。
可是根據網絡 7 層模型,Nginx 處於第 7 層。那除此之外,在網絡 7 層模型之中的其餘層也能夠進行負載。
比方說咱們在第 2 層的數據鏈路層,也能夠經過 MAC 地址進行負載。好比咱們能夠生成一個虛擬 MAC ,而後將這個 MAC 地址映射到其餘三個真實的服務器上。一樣的,也能夠在網絡第 3 層經過 IP 進行負載,在第 4 層經過端口號進行負載。
那看到這裏,有同窗可能會問了,可否進行級聯負載呢?
咱們假設當請求到來時,可否先在第 2 層進行負載,而後再在第 3 層、以後再在第 4 層、第 7 層分別都進行一次負載?
若是這樣作,在功能上確定是能夠實現的。但這種級聯的作法也會同時增長請求的路徑,由於咱們知道每增長 1 次負載,就會增長 1 個轉發路徑,而每增長 1 個轉發路徑,就可能帶來網絡延遲問題,所以太多的接連負載也是不推薦的。
那麼對於既然負載,常見的作法有哪一些呢?我認爲單獨的是由 Nginx 或者使用 Nginx 和 LVS 來實現二級負載就已經對於大部分系統足夠了。
剛纔提到的 LVS 是處於第 4 層,它是經過網絡端口進行的複雜,而 Nginx 是第 7 層應用級別的負載。
那還有就是咱們這裏說的負載,都是經過軟件進行的負載,也就是軟負載。
那除此之外,咱們還能夠購買一些硬件工具進行負載,也是硬負載。常見的應用負載工具備 F5 或 Array 等。
好,你們可能已經發現了啊,前兩層限流都是想辦法將請求攔截在抵達服務器以前,可是若是請求已經抵達到了服務器,又該如何進行限流呢?
那這個其實就是咱們立刻要講的第 3 層限流優點,服務限流。
首先咱們能夠經過 Web 服務器自己進行限流,比方說 Tomcat 是一款比較熟悉的 Web 服務器,若是鏈接 Tomcat 的數量太多,就可能形成 Tomcat 的不穩定,那該怎麼辦呢?
咱們能夠把 Tomcat 的最大連接數,設置爲一個合理的值,比方說咱們能夠設置單 Tomcat 的最大連接數只爲 300,那若是超過 300 的連接請求就會被 Tomcat 的無條件拒絕,那這樣就能夠保證談不開的穩定性了。
那再好比,咱們也能夠在服務器的內部,經過編寫一些算法來進行限流,那常見的算法有哪一些呢?
好比說令牌桶算法、漏洞算法都是的。那對於這些算法,若是你的編寫有些困難,咱們也能夠直接調一些類庫裏邊兒已經存在的 API。
public class RateLimiter {
//令牌桶限流:每秒只生成100個令牌,只有搶到令牌的線程才能搶購。
static RateLimiter tokenRateLimiter = RateLimiter.create( 100.0) ;
public static void miaoShaController () {
//每次搶購操做,都會持續嘗試l秒
if (tokenRateLimiter.tryAcquire(1,TimeUnit.SECONDS)) {
//開啓搶購線程
}
}
}
複製代碼
那麼這就是使用 Google Guava 類褲的令牌桶算法,create() 的方法能夠限制每秒鐘最多有 100 個線程能夠同時參與搶購,tryAcquire 方法能夠用於設置每秒的搶購動做會持續 1 秒鐘,實現起來很是簡單。
那除了剛纔講的服務器配置參數以及限流算法之外,咱們在服務器之中還可使用隊列來進行限流,這裏說的隊列主要是消息隊列。
這裏咱們拿一個例子來講,假設每秒鐘有 10 萬的請求量,而且系統裏邊有三個子系統 A、B 和 C,那三個子系統每秒鐘可以處理的極限分別是 2 萬請求、3 萬請求和 4 萬請求。
在不使用消息隊列的狀況下,若是這 10 萬請求分別平均分給這三個子系統,那麼每一個子系統就須要處理 3.3 萬的請求。那很顯然,在每秒鐘以內,系統 A 只能處理 2 萬請求,若是接收到了 3.3 萬請求,就可能致使系統 A 延遲甚至崩潰的狀況。
而若是使用消息隊列就能夠很好地解決這種問題。那消息隊列本質是一種緩衝區,當 10 萬請求到來時,消息隊列能夠將這 10 萬請求臨時存儲,而後三個子系統再分別根據本身的性能,分別去消息隊列中針對性的去拉取特定數量的請求。
比方說系統 A 的極限是 2 萬,那麼他每次最多就只須要從隊列之中取 2 萬數據就夠了,那這樣就能夠避免超額請求對系統 A 形成的壓力的狀況了。
除了前面介紹的服務器限流以及隊列限流之外,咱們還可使用第 3 個服務限流,也就是緩存限流。
限流的本質是爲了避免斷地削減請求的數量,而緩存的做用是爲了減小用戶請求服務端的數量,所以緩存也能夠做爲限流的一種實現方案。
但爲了有效地使用緩存進行限流,咱們須要先將系統設計成先後端分離或者動靜分離的結構,而後分別的對靜態以及動態緩存進行限流。
先看一下對靜態請求如何進行緩存。那當客戶端第 1 次請求服務端的時候,服務端會將網頁的基本結構代碼想給客戶端。
好比咱們第 1 次訪問某個網站時,網站服務器就會將搭建此網站的 HTML、JavaScript 腳本等代碼響應給客戶端,那麼客戶端就能夠將這些 HTML 和 JavaScript 代碼緩存到客戶端瀏覽器之中。那麼這樣一來,當用戶之後再次訪問這個網站時,就能夠直接從本地瀏覽器的緩存中獲取 HTML 和 JavaScript 代碼了。
對於 HTML 這種體積比較小的代碼,咱們能夠直接將其緩存在瀏覽器之中,可是若是體積較大的圖片,咱們最好將它們緩存的 Nginx,或者經過 Nginx 轉發在 OSS 等於服務器之中,而若是是視頻等一些體積特別大的靜態資源,也能夠將它緩存在 CDN 中,利用 CDN 區域部署就近訪問的特色來提升用戶的訪問速度。
而且咱們知道各個緩存並非獨立的,也能夠相互補充,好比說 OSS 也能夠做爲 CDN 的回源站點。
接下來再看一下動態緩存,那對於動態緩存,通常先建議緩存在本地的服務器之中,若是本地服務器的緩存失效,咱們再緩存到由 Redis 組成的遠程集羣之中,進行二次的查詢。也就是說咱們能夠搭建本地緩存以及二遠程緩存組成的二級結構,進行動態請求的緩存。
須要注意的是,緩存的級別也並非越多越好。有同窗可能也會想到,他說緩存既然這麼好用,那麼幹脆多來幾級緩存。咱們能夠在CPU、內存、硬盤、網絡等節點上分別設置緩存,而且每一個節點裏邊還能夠再次細分出多級緩存。
那若是這樣作,就必需要考慮多級緩存帶來的一致性問題了,緩存的級別越多,一致性的問題就越嚴重,而解決這種一致性問題又會增長系統的開發成本以及系統的額外開銷。
還要知道的是,咱們緩存的級別越多,請求在系統內部的跳轉路徑也會越長,這也就相似於多級負載帶來的問題。因此說咱們學技術必定要懂得權衡,不要盲目的進行技術的堆砌。
那麼對於大部分項目而言,咱們使用靜態緩存加上二級動態緩存已經徹底足夠了。
那總得來講,咱們靜態緩存能夠將大量的靜態資源緩存在服務器之外的地方,而動態緩存能夠很大程度上減小請求抵達數據庫的次數。
那最後咱們再來看一下監控限流。咱們知道 CPU、內存、併發量等都是衡量系統穩定性的重要指標,若是他們的使用頻率太高,也可能形成系統的不穩定。
所以咱們也能夠建議建立一些線程,專門用於監控這些指標。比方說咱們能夠創建一個線程,專門用於監控 CPU 的利用率,若是 CPU 利用率達到了極限,就能夠臨時性地採起服務降級或拒絕策略。
那這裏說的服務降級實際上與精兵簡政的思想相似,它指的是當系統資源不足時,咱們就能夠把查看三個月之前的歷史訂單、歷史評論等一些非核心的服務臨時關閉,從而爲系統節約出一部分的資源來。
那在採用服務降級或拒絕策略一段時間以後,CPu 等資源利用率就會恢復到正常狀態,那以後咱們就能夠從新接收並處理新的請求了。
好的,咱們今天分享的主題是如何設計秒殺服務的限流策略。這裏我介紹了合法性限流、負載限流、服務限流。其中合法性限流能夠攔截大量的非法請求,而負載限流能夠經過集羣技術抵抗大規模的流量衝擊服務,下流則是經過對服務器的參數配置、限流算法、MQ 緩存以及監控等手段進行限流。