對于秒殺業(yè)務(wù),大家應(yīng)該比較熟悉了。比如,“某商品原價(jià) 1299 元, 雙十一整點(diǎn)秒殺價(jià)僅 500 元,限量 100 件,先到先得” 等等。通過這段文案我們能夠發(fā)現(xiàn),參與秒殺活動(dòng)商品的價(jià)格都比平時(shí)低很多,因此會(huì)吸引大量的用戶來搶購。從而不難發(fā)現(xiàn),對于秒殺系統(tǒng)的挑戰(zhàn)就是:在流量瞬時(shí)突增的情況下,如何做依舊能夠保證系統(tǒng)的穩(wěn)定性。
1、基于合法性限流。
2、基于負(fù)載限流。
3、基于服務(wù)限流。
4、基于監(jiān)控限流。
那舉個(gè)例子來說,比如說我現(xiàn)在橫軸是時(shí)間軸,縱軸是用戶的并發(fā)訪問量,在橫軸的 S 點(diǎn)就是秒殺的開始時(shí)刻。
那一般而言,在秒殺開始之前,用戶的訪問量是一條比較平滑的曲線,但隨著秒殺活動(dòng)的開始,用戶的訪問量會(huì)急劇的增大,并且隨著秒殺的結(jié)束,訪問量又會(huì)急劇的下降。
我們假設(shè)在初始時(shí)用戶的訪問量在 1 萬左右浮動(dòng),秒殺服務(wù)器能夠承受的極限是 5 萬,但在秒殺活動(dòng)期間,用戶的實(shí)際訪問量可能達(dá)到了 100萬。
那么很明顯,100 萬的訪問量已經(jīng)遠(yuǎn)遠(yuǎn)超過了系統(tǒng)能夠正常承載的 5 萬并發(fā)量,因此這時(shí)候就可能會(huì)導(dǎo)致秒殺系統(tǒng)的不穩(wěn)定,甚至宕機(jī)的情況。
所以我們現(xiàn)在不得不提到剛才所說的了,秒殺系統(tǒng)面臨的最大挑戰(zhàn)就是如何要保證在流量突增的情況下,仍然保證系統(tǒng)的穩(wěn)定性。
那么在實(shí)際開發(fā)中,其實(shí)有很多方案都可以保證系統(tǒng)的穩(wěn)定性。 而我們本場 Chat 要分享的重點(diǎn)就是說如何要通過限流策略抵御秒殺期間的流量峰值,從而實(shí)現(xiàn)穩(wěn)定性,那一起來看一下具體怎么操作。
當(dāng)海量請求到來時(shí),我們可以對請求進(jìn)行層層設(shè)卡、層層攔截,最終將海量請求削減成服務(wù)器能夠處理的請求。
多次限流
那舉個(gè)例子來說,比如秒殺開始時(shí),可能有 100萬的請求同時(shí)撲向服務(wù)器。如果從多層限流的角度來說,我們就可以在第 1 層把流量先削減成 30 萬,然后在第 2 層減到 10 萬,再在第 3 層減到 5 萬,然后直接將這 5 萬直接處理就可以了。
不難發(fā)現(xiàn),在限流時(shí),我們既要層層限流,也要盡早限流,因?yàn)樯嫌螖r截的請求越多,下游的流量就越少。
那接下來我們就一起看一看到底如何進(jìn)行層層限流。
基于合法性限流
先看一下低層限流又是合法性限流,為了更好的解決這個(gè)問題,我們先需要看一下到底什么是合法性限流?
合法性限流指的是僅僅限制那些合法的用戶請求能夠抵達(dá)到秒殺服務(wù)器,而將一些非法的請求全部進(jìn)行攔截掉。那因此這里就需要注意了,在請求合法性限流以前,就得先知道哪些請求是合法的,哪些是非法的。
舉一些非法的例子。比如在秒殺活動(dòng)期間,那實(shí)際參與秒殺活動(dòng)的用戶可能是人,也可能是機(jī)器人,并且還可能存在同一用戶反復(fù)購買同一件商品的行為,也是我們說的刷單行為。
那么顯然機(jī)器人和用戶刷單都是一種不合理的行為,這種行為會(huì)影響到其他正常用戶的購物體驗(yàn),因此就屬于不合法的請求。
合法性限流解決方案
而關(guān)于如何限制這些不合法的請求,那么就得具體問題具體分析和討論了。
通過驗(yàn)證碼的方式
比如說,如果非法請求的發(fā)起者是機(jī)器人,那么最容易想到的方法就是使用驗(yàn)證碼。并且驗(yàn)證碼還有一個(gè)作用,它可以拉長用戶的訪問時(shí)間。
舉個(gè)例子,假設(shè)某一秒鐘有 100 萬個(gè)用戶同時(shí)下單,但如果使用了驗(yàn)證碼,那么用戶從輸入驗(yàn)證碼到整個(gè)下單的整個(gè)過程就可能需要三秒鐘,也就是說下單量仍然是 100 萬不變,但下單的總體時(shí)間可能從 1 秒鐘拉長到了 3 秒,那么原來需要 1 秒的時(shí)間,現(xiàn)在就需要 3 秒的世界,原來 100 萬的請求,現(xiàn)在每秒鐘就只需要處理 33 萬,因此也可以降低流量的峰值。
通過 IP 的方式
再來看一下IP限制,如果通過網(wǎng)絡(luò)技術(shù)監(jiān)測到了某個(gè) IP 下的下單頻率在毫秒級(jí)別,或者反復(fù)購買同一件商品,那么就能斷定下單的是機(jī)器人或者是不合法的用戶,這樣我們就可以將這個(gè) IP 加到黑名單之中,從而減少不合法的流量。
那還有一種做法是隱藏秒殺的入口地址,它指的是在秒殺開始之前,服務(wù)器并不會(huì)向外界暴露秒殺服務(wù)的地址,當(dāng)秒殺服務(wù)開始之后才開放地址。
那到這里,第 1 層合法性線路就講解完了,接下來我們再看一下第二層限流,也就是負(fù)載限流。
基于負(fù)載限流
先看一下負(fù)載限流的理論基礎(chǔ)是什么?一個(gè)是集群,一個(gè)是網(wǎng)絡(luò) 7 層模型。
我們在搭建集群時(shí)經(jīng)常會(huì)用到一些工具,比如說 Nginx 和 LVS,那這些都可以用于負(fù)載限流。
基于軟件實(shí)現(xiàn)限流
假設(shè)經(jīng)過了第 1 層合法性限流以后,還剩 33 萬的請求,如果通過集群搭建了三臺(tái)服務(wù)器,那么每臺(tái)服務(wù)器也就只需要承載 11 萬的請求量了,那這樣也能降低請求的并發(fā)量。
但是根據(jù)網(wǎng)絡(luò) 7 層模型,Nginx 處于第 7 層。那除此以外,在網(wǎng)絡(luò) 7 層模型之中的其他層也可以進(jìn)行負(fù)載。
比方說我們在第 2 層的數(shù)據(jù)鏈路層,也可以通過 MAC 地址進(jìn)行負(fù)載。比如我們可以生成一個(gè)虛擬 MAC ,然后將這個(gè) MAC 地址映射到其他三個(gè)真實(shí)的服務(wù)器上。同樣的,也可以在網(wǎng)絡(luò)第 3 層通過 IP 進(jìn)行負(fù)載,在第 4 層通過端口號(hào)進(jìn)行負(fù)載。
那看到這里,有同學(xué)可能會(huì)問了,能否進(jìn)行級(jí)聯(lián)負(fù)載呢?
我們假設(shè)當(dāng)請求到來時(shí),能否先在第 2 層進(jìn)行負(fù)載,然后再在第 3 層、之后再在第 4 層、第 7 層分別都進(jìn)行一次負(fù)載?
如果這樣做,在功能上肯定是可以實(shí)現(xiàn)的。但這種級(jí)聯(lián)的做法也會(huì)同時(shí)增加請求的路徑,因?yàn)槲覀冎烂吭黾?1 次負(fù)載,就會(huì)增加 1 個(gè)轉(zhuǎn)發(fā)路徑,而每增加 1 個(gè)轉(zhuǎn)發(fā)路徑,就可能帶來網(wǎng)絡(luò)延遲問題,因此太多的接連負(fù)載也是不推薦的。
那么對于既然負(fù)載,常見的做法有哪一些呢?我認(rèn)為單獨(dú)的是由 Nginx 或者使用 Nginx 和 LVS 來實(shí)現(xiàn)二級(jí)負(fù)載就已經(jīng)對于大部分系統(tǒng)足夠了。
剛才提到的 LVS 是處于第 4 層,它是通過網(wǎng)絡(luò)端口進(jìn)行的復(fù)雜,而 Nginx 是第 7 層應(yīng)用級(jí)別的負(fù)載。
那還有就是我們這里說的負(fù)載,都是通過軟件進(jìn)行的負(fù)載,也就是軟負(fù)載。
基于硬件實(shí)現(xiàn)限流
那除此以外,我們還可以購買一些硬件工具進(jìn)行負(fù)載,也是硬負(fù)載。常見的應(yīng)用負(fù)載工具有 F5 或 Array 等。
好,大家可能已經(jīng)發(fā)現(xiàn)了啊,前兩層限流都是想辦法將請求攔截在抵達(dá)服務(wù)器之前,但是如果請求已經(jīng)抵達(dá)到了服務(wù)器,又該如何進(jìn)行限流呢?
基于服務(wù)限流
那這個(gè)其實(shí)就是我們馬上要講的第 3 層限流優(yōu)勢,服務(wù)限流。
首先我們可以通過 Web 服務(wù)器本身進(jìn)行限流,比方說 Tomcat 是一款比較熟悉的 Web 服務(wù)器,如果連接 Tomcat 的數(shù)量太多,就可能造成 Tomcat 的不穩(wěn)定,那該怎么辦呢?
我們可以把 Tomcat 的最大鏈接數(shù),設(shè)置為一個(gè)合理的值,比方說我們可以設(shè)置單 Tomcat 的最大鏈接數(shù)只為 300,那如果超過 300 的鏈接請求就會(huì)被 Tomcat 的無條件拒絕,那這樣就可以保證談不開的穩(wěn)定性了。
那再比如,我們也可以在服務(wù)器的內(nèi)部,通過編寫一些算法來進(jìn)行限流,那常見的算法有哪一些呢?
基于算法實(shí)現(xiàn)限流
比如說令牌桶算法、漏洞算法都是的。那對于這些算法,如果你的編寫有些困難,我們也可以直接調(diào)一些類庫里邊兒已經(jīng)存在的 API。
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n76" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class RateLimiter {
//令牌桶限流:每秒只生成100個(gè)令牌,只有搶到令牌的線程才能搶購。
static RateLimiter tokenRateLimiter = RateLimiter.create( 100.0) ;
public static void miaoShaController () {
//每次搶購操作,都會(huì)持續(xù)嘗試l秒
if (tokenRateLimiter.tryAcquire(1,TimeUnit.SECONDS)) {
//開啟搶購線程
}
}
}</pre>
那么這就是使用 Google Guava 類褲的令牌桶算法,create() 的方法可以限制每秒鐘最多有 100 個(gè)線程可以同時(shí)參與搶購,tryAcquire 方法可以用于設(shè)置每秒的搶購動(dòng)作會(huì)持續(xù) 1 秒鐘,實(shí)現(xiàn)起來非常簡單。
基于消息隊(duì)列實(shí)現(xiàn)限流
那除了剛才講的服務(wù)器配置參數(shù)以及限流算法以外,我們在服務(wù)器之中還可以使用隊(duì)列來進(jìn)行限流,這里說的隊(duì)列主要是消息隊(duì)列。
這里我們拿一個(gè)例子來說,假設(shè)每秒鐘有 10 萬的請求量,并且系統(tǒng)里邊有三個(gè)子系統(tǒng) A、B 和 C,那三個(gè)子系統(tǒng)每秒鐘能夠處理的極限分別是 2 萬請求、3 萬請求和 4 萬請求。
在不使用消息隊(duì)列的情況下,如果這 10 萬請求分別平均分給這三個(gè)子系統(tǒng),那么每個(gè)子系統(tǒng)就需要處理 3.3 萬的請求。那很顯然,在每秒鐘之內(nèi),系統(tǒng) A 只能處理 2 萬請求,如果接收到了 3.3 萬請求,就可能導(dǎo)致系統(tǒng) A 延遲甚至崩潰的情況。
而如果使用消息隊(duì)列就可以很好地解決這種問題。那消息隊(duì)列本質(zhì)是一種緩沖區(qū),當(dāng) 10 萬請求到來時(shí),消息隊(duì)列可以將這 10 萬請求臨時(shí)存儲(chǔ),然后三個(gè)子系統(tǒng)再分別根據(jù)自己的性能,分別去消息隊(duì)列中針對性的去拉取特定數(shù)量的請求。
比方說系統(tǒng) A 的極限是 2 萬,那么他每次最多就只需要從隊(duì)列之中取 2 萬數(shù)據(jù)就夠了,那這樣就可以避免超額請求對系統(tǒng) A 造成的壓力的情況了。
除了前面介紹的服務(wù)器限流以及隊(duì)列限流以外,我們還可以使用第 3 個(gè)服務(wù)限流,也就是緩存限流。
緩存限流
限流的本質(zhì)是為了不斷地削減請求的數(shù)量,而緩存的作用是為了減少用戶請求服務(wù)端的數(shù)量,因此緩存也可以作為限流的一種實(shí)現(xiàn)方案。
但為了有效地使用緩存進(jìn)行限流,我們需要先將系統(tǒng)設(shè)計(jì)成前后端分離或者動(dòng)靜分離的結(jié)構(gòu),然后分別的對靜態(tài)以及動(dòng)態(tài)緩存進(jìn)行限流。
靜態(tài)緩存實(shí)現(xiàn)限流
先看一下對靜態(tài)請求如何進(jìn)行緩存。那當(dāng)客戶端第 1 次請求服務(wù)端的時(shí)候,服務(wù)端會(huì)將網(wǎng)頁的基本結(jié)構(gòu)代碼想給客戶端。
比如我們第 1 次訪問某個(gè)網(wǎng)站時(shí),網(wǎng)站服務(wù)器就會(huì)將搭建此網(wǎng)站的 HTML、JavaScript 腳本等代碼響應(yīng)給客戶端,那么客戶端就可以將這些 HTML 和 JavaScript 代碼緩存到客戶端瀏覽器之中。那么這樣一來,當(dāng)用戶以后再次訪問這個(gè)網(wǎng)站時(shí),就可以直接從本地瀏覽器的緩存中獲取 HTML 和 JavaScript 代碼了。
對于 HTML 這種體積比較小的代碼,我們可以直接將其緩存在瀏覽器之中,但是如果體積較大的圖片,我們最好將它們緩存的 Nginx,或者通過 Nginx 轉(zhuǎn)發(fā)在 OSS 等于服務(wù)器之中,而如果是視頻等一些體積特別大的靜態(tài)資源,也可以將它緩存在 CDN 中,利用 CDN 區(qū)域部署就近訪問的特點(diǎn)來提高用戶的訪問速度。
并且我們知道各個(gè)緩存并不是獨(dú)立的,也可以相互補(bǔ)充,比如說 OSS 也可以作為 CDN 的回源站點(diǎn)。
動(dòng)態(tài)緩存實(shí)現(xiàn)限流
接下來再看一下動(dòng)態(tài)緩存,那對于動(dòng)態(tài)緩存,一般先建議緩存在本地的服務(wù)器之中,如果本地服務(wù)器的緩存失效,我們再緩存到由 Redis 組成的遠(yuǎn)程集群之中,進(jìn)行二次的查詢。也就是說我們可以搭建本地緩存以及二遠(yuǎn)程緩存組成的二級(jí)結(jié)構(gòu),進(jìn)行動(dòng)態(tài)請求的緩存。
需要注意的是,緩存的級(jí)別也并不是越多越好。有同學(xué)可能也會(huì)想到,他說緩存既然這么好用,那么干脆多來幾級(jí)緩存。我們可以在CPU、內(nèi)存、硬盤、網(wǎng)絡(luò)等節(jié)點(diǎn)上分別設(shè)置緩存,并且每個(gè)節(jié)點(diǎn)里邊還可以再次細(xì)分出多級(jí)緩存。
那如果這樣做,就必須要考慮多級(jí)緩存帶來的一致性問題了,緩存的級(jí)別越多,一致性的問題就越嚴(yán)重,而解決這種一致性問題又會(huì)增加系統(tǒng)的開發(fā)成本以及系統(tǒng)的額外開銷。
還要知道的是,我們緩存的級(jí)別越多,請求在系統(tǒng)內(nèi)部的跳轉(zhuǎn)路徑也會(huì)越長,這也就類似于多級(jí)負(fù)載帶來的問題。所以說我們學(xué)技術(shù)一定要懂得權(quán)衡,不要盲目的進(jìn)行技術(shù)的堆砌。
那么對于大部分項(xiàng)目而言,我們使用靜態(tài)緩存加上二級(jí)動(dòng)態(tài)緩存已經(jīng)完全足夠了。
那總得來說,我們靜態(tài)緩存可以將大量的靜態(tài)資源緩存在服務(wù)器以外的地方,而動(dòng)態(tài)緩存可以很大程度上減少請求抵達(dá)數(shù)據(jù)庫的次數(shù)。
基于監(jiān)控限流
那最后我們再來看一下監(jiān)控限流。我們知道 CPU、內(nèi)存、并發(fā)量等都是衡量系統(tǒng)穩(wěn)定性的重要指標(biāo),如果他們的使用頻率過高,也可能造成系統(tǒng)的不穩(wěn)定。
因此我們也可以建議創(chuàng)建一些線程,專門用于監(jiān)控這些指標(biāo)。比方說我們可以建立一個(gè)線程,專門用于監(jiān)控 CPU 的利用率,如果 CPU 利用率達(dá)到了極限,就可以臨時(shí)性地采取服務(wù)降級(jí)或拒絕策略。
那這里說的服務(wù)降級(jí)實(shí)際上與精兵簡政的思想類似,它指的是當(dāng)系統(tǒng)資源不足時(shí),我們就可以把查看三個(gè)月以前的歷史訂單、歷史評論等一些非核心的服務(wù)臨時(shí)關(guān)閉,從而為系統(tǒng)節(jié)約出一部分的資源來。
那在采用服務(wù)降級(jí)或拒絕策略一段時(shí)間之后,CPu 等資源利用率就會(huì)恢復(fù)到正常狀態(tài),那之后我們就可以重新接收并處理新的請求了。
總結(jié)
好的,我們今天分享的主題是如何設(shè)計(jì)秒殺服務(wù)的限流策略。這里我介紹了合法性限流、負(fù)載限流、服務(wù)限流。其中合法性限流可以攔截大量的非法請求,而負(fù)載限流可以通過集群技術(shù)抵抗大規(guī)模的流量沖擊服務(wù),下流則是通過對服務(wù)器的參數(shù)配置、限流算法、MQ 緩存以及監(jiān)控等手段進(jìn)行限流。
來源:https://www.jianshu.com/p/6fe8e7635e04