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