對頁面進行徹底的動靜分離,使得用戶秒殺時不需要刷新整個頁面,借此把頁面刷新的數(shù)據(jù)降到最少。
用戶看到的數(shù)據(jù)可以分為:靜態(tài)數(shù)據(jù) 和 動態(tài)數(shù)據(jù)。
簡單來說,"動態(tài)數(shù)據(jù)"和"靜態(tài)數(shù)據(jù)"的主要區(qū)別就是看頁面中輸出的數(shù)據(jù)是否和URL、瀏覽者、時間、地域相關(guān),以及是否含有Cookie等私密數(shù)據(jù)。
比如說:
這里再強調(diào)一下,我們所說的靜態(tài)數(shù)據(jù),不能僅僅理解為傳統(tǒng)意義上完全存在磁盤上的HTML頁面,它也可能是經(jīng)過Java系統(tǒng)產(chǎn)生的頁面,但是它輸出的頁面本身不包含上面所說的那些因素。
也就是所謂"動態(tài)"還是"靜態(tài)",并不是說數(shù)據(jù)本身是否動靜,而是數(shù)據(jù)中是否含有和訪問者相關(guān)的個性化數(shù)據(jù)。
靜態(tài)化改造就是要直接緩存 HTTP 連接。
相較于普通的數(shù)據(jù)緩存而言,你肯定還聽過系統(tǒng)的靜態(tài)化改造。靜態(tài)化改造是直接緩存 HTTP 連接而不是僅僅緩存數(shù)據(jù),如下圖所示,Web 代理服務器根據(jù)請求 URL,直接取出對應的 HTTP 響應頭和響應體然后直接返回,這個響應過程簡單得連 HTTP 協(xié)議都不用重新組裝,甚至連 HTTP 請求頭也不需要解析。
圖片
高并發(fā)時候,商詳頁面是最先受到?jīng)_擊的,通過商詳靜態(tài)化,可以幫助服務器擋掉99.9%流量。
分類舉例:商品圖片、商品詳細描述等,所有用戶看到的內(nèi)容都是一樣的,這一類數(shù)據(jù)就可以上靜態(tài)化。
會員折扣、優(yōu)惠券等信息具備個體差異性,就需要放在動態(tài)接接口中,根據(jù)入?yún)⑿畔崟r查詢。
我們從以下 5 個方面來分離出動態(tài)內(nèi)容:
分離出動態(tài)內(nèi)容之后,如何組織這些內(nèi)容頁就變得非常關(guān)鍵了。
動態(tài)內(nèi)容的處理通常有兩種方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
網(wǎng)站應用,靜態(tài)資源占流量的多數(shù)。系統(tǒng)做了動靜分離之后,就可以把靜態(tài)資源通過CDN加速。
這樣,靜態(tài)資源的請求大部分通過就近部署的CDN服務器提供服務,用戶的延遲也會有明顯的提升。網(wǎng)站服務器專注于服務動態(tài)流量,帶寬壓力會小很多。
動靜分離,部署時靜態(tài)資源要給一個單獨域名,這個域名是個CNAME,CNAME映射到CDN服務廠商提供的DNS服務器,CDN DNS服務器會根據(jù)請求的IP地址所在區(qū)域和資源內(nèi)容,返回就近的CDN緩存服務器ip,后續(xù)用戶對這個DNS的請求都會轉(zhuǎn)到這個IP上來。
Tips:CNAME 簡單來講就是給域名起了個別名。
CDN 工作流程大致如下:
圖片
靜態(tài)資源上 CDN 存在以下幾個問題:
失效需要一個失效系統(tǒng)來實現(xiàn),一般有主動失效和被動失效。
主動失效需要監(jiān)控數(shù)據(jù)庫數(shù)據(jù)的變化然后轉(zhuǎn)成消息來發(fā)送失效消息,這個實現(xiàn)比較復雜,阿里有個系統(tǒng)叫metaq,可以網(wǎng)上參考下。
被動失效就是只緩存固定時間,然后到期后自動失效
部署方式如下圖所示:
圖片
你可能會問,存儲在瀏覽器或 CDN 上,有多大區(qū)別?我的回答是:區(qū)別很大!因為在 CDN 上,我們可以做主動失效,而在用戶的瀏覽器里就更不可控,如果用戶不主動刷新的話,你很難主動地把消息推送給用戶的瀏覽器。
比如,1 元賣 iPhone,100 臺,于是來了一百萬人搶購。
我們把技術(shù)挑戰(zhàn)放在一邊,先從用戶或是產(chǎn)品的角度來看一下,秒殺的流程是什么樣的。
從技術(shù)上來說,這個倒計時按鈕上的時間和按鈕可以被點擊的時間是需要后臺服務器來校準的,這意味著:
很明顯,要讓 100 萬用戶能夠在同一時間打開一個頁面,這個時候,我們就需要用到 CDN 了。數(shù)據(jù)中心肯定是扛不住的,所以,我們要引入 CDN。
在 CDN 上,這 100 萬個用戶就會被幾十個甚至上百個 CDN 的邊緣結(jié)點給分擔了,于是就能夠扛得住。然后,我們還需要在這些 CDN 結(jié)點上做點小文章。
一方面,我們需要把小服務部署到 CDN 結(jié)點上去,這樣,當前端頁面來問開沒開始時,這個小服務除了告訴前端開沒開始外,它還可以統(tǒng)計下有多少人在線。每個小服務會把當前在線等待秒殺的人數(shù)每隔一段時間就回傳給我們的數(shù)據(jù)中心,于是我們就知道全網(wǎng)總共在線的人數(shù)有多少。
假設,我們知道有大約 100 萬的人在線等著搶,那么,在我們快要開始的時候,由數(shù)據(jù)中心向各個部署在 CDN 結(jié)點上的小服務上傳遞一個概率值,比如說是 0.02%。
于是,當秒殺開始的時候,這 100 萬用戶都在點下單按鈕,首先他們請求到的是 CDN 上的這些服務,這些小服務按照 0.02% 的量把用戶放到后面的數(shù)據(jù)中心,也就是 1 萬個人放過去兩個,剩下的 9998 個都直接返回秒殺已結(jié)束。于是,100 萬用戶被放過了 0.02% 的用戶,也就是 200 個左右,而這 200 個人在數(shù)據(jù)中心搶那 100 個 iPhone,也就是 200 TPS,這個并發(fā)量怎么都應該能扛住了。
熱點數(shù)據(jù)亦分 靜態(tài)熱點 和 動態(tài)熱點。
所謂"靜態(tài)熱點數(shù)據(jù)",就是能夠提前預測的熱點數(shù)據(jù)。
例如,我們可以通過賣家報名的方式提前篩選出來,通過報名系統(tǒng)對這些熱點商品進行打標。另外,我們還可以通過大數(shù)據(jù)分析來提前發(fā)現(xiàn)熱點商品,比如我們分析歷史成交記錄、用戶的購物車記錄,來發(fā)現(xiàn)哪些商品可能更熱門、更好賣,這些都是可以提前分析出來的熱點。
所謂"動態(tài)熱點數(shù)據(jù)",就是不能被提前預測到的,系統(tǒng)在運行過程中臨時產(chǎn)生的熱點。例如,賣家在抖音上做了廣告,然后商品一下就火了,導致它在短時間內(nèi)被大量購買。
靜態(tài)熱點比較好處理,所以秒級內(nèi)自動發(fā)現(xiàn)熱點商品就成為了熱點緩存的關(guān)鍵。
這里我給出一個動態(tài)熱點發(fā)現(xiàn)系統(tǒng)的具體實現(xiàn):
這里我給出了一個圖,其中用戶訪問商品時經(jīng)過的路徑有很多,我們主要是依賴前面的導購頁面(包括首頁、搜索頁面、商品詳情、購物車等)提前識別哪些商品的訪問量高,通過這些系統(tǒng)中的中間件來收集熱點數(shù)據(jù),并記錄到日志中。
圖片
我們通過部署在每臺機器上的Agent把日志匯總到聚合和分析集群中,然后把符合一定規(guī)則的熱點數(shù)據(jù),通過訂閱分發(fā)系統(tǒng)再推送到相應的系統(tǒng)中。你可以是把熱點數(shù)據(jù)填充到Cache中,或者直接推送到應用服務器的內(nèi)存中,還可以對這些數(shù)據(jù)進行攔截,總之下游系統(tǒng)可以訂閱這些數(shù)據(jù),然后根據(jù)自己的需求決定如何處理這些數(shù)據(jù)。
熱點發(fā)現(xiàn)要做到接近實時(3s內(nèi)完成熱點數(shù)據(jù)的發(fā)現(xiàn)),因為只有做到接近實時,動態(tài)發(fā)現(xiàn)才有意義,才能實時地對下游系統(tǒng)提供保護。
對于緩存系統(tǒng)來講,緩存命中率是最重要的指標,甚至都沒有之一。時間拉的越長,不確定性越多,緩存命中率必然越低。比如如果10s內(nèi)才發(fā)送熱點就沒意義了,因為10s內(nèi)用戶可以進行的操作太多了。時間越長,不可控元素越多,熱點緩存命中率越低。
可以參考,京東開源的熱點探測 Hot Key。
可以考慮建立實時熱點發(fā)現(xiàn)系統(tǒng)。
具體步驟如下:
限制更多的是一種保護機制,限制的辦法也有很多,例如對被訪問商品的 ID 做一致性 Hash,然后根據(jù) Hash 做分桶,每個分桶設置一個處理隊列,這樣可以把熱點商品限制在一個請求隊列里,防止因某些熱點商品占用太多的服務器資源,而使其他請求始終得不到服務器的處理資源。
使用Java堆內(nèi)存來存儲緩存對象。使用堆緩存的好處是不需要序列化/反序列化,是最快的緩存。缺點也很明顯,當緩存的數(shù)據(jù)量很大時,GC(垃圾回收)暫停時間會變長,存儲容量受限于堆空間大小。
一般通過軟引用/弱引用來存儲緩存對象,即當堆內(nèi)存不足時,可以強制回收這部分內(nèi)存釋放堆內(nèi)存空間。一般使用堆緩存存儲較熱的數(shù)據(jù)。可以使用Caffeine Cache實現(xiàn)。
現(xiàn)在應用最多的是多級緩存方案,就好比 CPU 也有 L1,L2,L3。
Nginx緩存 → 分布式Redis緩存(可以使用Lua腳本直接在Nginx里讀取Redis)→堆內(nèi)存。
整體流程如下:
添加秒殺答題。有以下兩個目的:
你可能有疑問了,排隊和鎖競爭不都是要等待嗎,有啥區(qū)別?
如果熟悉 MySQL 的話,你會知道 InnoDB 內(nèi)部的死鎖檢測,以及 MySQL Server 和 InnoDB 的切換會比較消耗性能。
對于分布式限流,目前遇到的場景是業(yè)務上的限流,而不是流量入口的限流。流量入口限流應該在接入層完成,而接入層筆者一般使用 Nginx。業(yè)務的限流一般用Redis + Lua腳本。
千萬不要超賣,這是大前提。超賣直接導致的就是資損。
在正常的電商平臺購物場景中,用戶的實際購買過程一般分為兩步:下單和付款。你想買一臺 iPhone 手機,在商品頁面點了“立即購買”按鈕,核對信息之后點擊“提交訂單”,這一步稱為下單操作。下單之后,你只有真正完成付款操作才能算真正購買,也就是俗話說的“落袋為安”。
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
先說第一種,"下單減庫存",可能導致惡意下單。
正常情況下,買家下單后付款的概率會很高,所以不會有太大問題。但是有一種場景例外,就是當賣家參加某個活動時,此時活動的有效時間是商品的黃金售賣時間,如果有競爭對手通過惡意下單的方式將該賣家的商品全部下單(雇幾個人下單將你的商品全都鎖了),讓這款商品的庫存減為零,那么這款商品就不能正常售賣了。要知道,這些惡意下單的人是不會真正付款的,這正是"下單減庫存"方式的不足之處。
既然,從而影響賣家的商品銷售,那么有沒有辦法解決呢?你可能會想,采用"付款減庫存"的方式是不是就可以了?的確可以。但是,"付款減庫存"又會導致另外一個問題:庫存超賣。
假如有 100 件商品,就可能出現(xiàn) 300 人下單成功的情況,因為下單時不會減庫存,所以也就可能出現(xiàn)下單成功數(shù)遠遠超過真正庫存數(shù)的情況,這尤其會發(fā)生在做活動的熱門商品上。這樣一來,就會導致很多買家下單成功但是付不了款,買家的購物體驗自然比較差。
超賣情況可以區(qū)別對待:對普通的商品下單數(shù)量超過庫存數(shù)量的情況,可以通過補貨來解決;但是有些賣家完全不允許庫存為負數(shù)的情況,那只能在買家付款時提示庫存不足。
預扣庫存方案確實可以在一定程度上緩解上面的問題。但沒有徹底解決,比如針對惡意下單這種情況,雖然把有效的付款時間設置為 10 分鐘,但是惡意買家完全可以在 10 分鐘后再次下單,或者采用一次下單很多件的方式把庫存減完。針對這種情況,解決辦法還是要結(jié)合安全和反作弊的措施來制止。
例如,給經(jīng)常下單不付款的買家進行識別打標(可以在被打標的買家下單時不減庫存)、給某些類目設置最大購買件數(shù)(例如,參加活動的商品一人最多只能買 3 件),以及對重復下單不付款的操作進行次數(shù)限制等。
方案的核心思路:將庫存扣減異步化,庫存扣減流程調(diào)整為下單時只記錄扣減明細(DB記錄插入),異步進行真正庫存扣減(更新)。
大量請求對同一數(shù)據(jù)行的的競爭更新,會導致數(shù)據(jù)庫的性能急劇下降,甚至發(fā)生數(shù)據(jù)庫分片的連接被熱點單商品扣減。
前置校驗庫存,從db更換為redis,庫存扣減操作,從更新操作,直接修改為插入操作(性能角度,插入鎖比更新鎖的性能高)
熱點發(fā)現(xiàn)系統(tǒng)(中間件)會通過消息隊列的方式通知應用,應用對庫存進行熱點打標。一但庫存不再是熱點(熱點失效),則會進行庫存熱點重置。
將商品庫存分開放,分而治之。例如,原來的秒殺商品的id為10001,庫存為1000件,在Redis中的存儲為(10001, 1000),我們將原有的庫存分割為5份,則每份的庫存為200件,此時,我們在Redia中存儲的信息為(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。將key分散到redis的不同槽位中,這就能夠提升Redis處理請求的性能和并發(fā)量。
單個熱點商品會影響整個數(shù)據(jù)庫的性能,導致0.01%的商品影響99.99%的商品的售賣,這是我們不愿意看到的情況。一個解決思路是遵循前面介紹的原則進行隔離,把熱點商品放到單獨的熱點庫中。但是這無疑會帶來維護上的麻煩,比如要做熱點數(shù)據(jù)的動態(tài)遷移以及單獨的數(shù)據(jù)庫等。
線程隔離主要是指線程池隔離,在實際使用時,我們會把請求分類,然后交給不同的線程池處理。當一種業(yè)務的請求處理發(fā)生問題時,不會將故障擴散到其他線程池,從而保證其他服務可用。
圖片
隨著對系統(tǒng)可用性的要求,會進行多機房部署,每個機房的服務都有自己的服務分組,本機房的服務應該只調(diào)用本機房服務,不進行跨機房調(diào)用。其中,一個機房服務發(fā)生問題時,可以通過DNS/負載均衡將請求全部切到另一個機房,或者考慮服務能自動重試其他機房的服務,從而提升系統(tǒng)可用性。
圖片
核心業(yè)務以及非核心業(yè)務可以放在不同的線程池。
可以使用Hystrix來實現(xiàn)線程池隔離。
所謂“降級”,就是當系統(tǒng)的容量達到一定程度時,是為了保證核心服務的穩(wěn)定而犧牲非核心服務的做法。
降級方案可以這樣設計:當秒殺流量達到 5w/s 時,把成交記錄的獲取從展示 20 條降級到只展示 5 條。“從 20 改到 5”這個操作由一個開關(guān)來實現(xiàn),也就是設置一個能夠從開關(guān)系統(tǒng)動態(tài)獲取的系統(tǒng)參數(shù)。
降級無疑是在系統(tǒng)性能和用戶體驗之間選擇了前者,降級后肯定會影響一部分用戶的體驗,例如在雙 11 零點時,如果優(yōu)惠券系統(tǒng)扛不住,可能會臨時降級商品詳情的優(yōu)惠信息展示,把有限的系統(tǒng)資源用在保障交易系統(tǒng)正確展示優(yōu)惠信息上,即保障用戶真正下單時的價格是正確的。所以降級的核心目標是犧牲次要的功能和用戶體驗來保證核心業(yè)務流程的穩(wěn)定,是一個不得已而為之的舉措。
如果限流還不能解決問題,最后一招就是直接拒絕服務了。
當系統(tǒng)負載達到一定閾值時,例如 CPU 使用率達到 90% 或者系統(tǒng) load 值達到 2*CPU 核數(shù)時,系統(tǒng)直接拒絕所有請求,這種方式是最暴力但也最有效的系統(tǒng)保護方式。
在最前端的 Nginx 上設置過載保護,當機器負載達到某個值時直接拒絕 HTTP 請求并返回 503 錯誤碼,在 Java 層同樣也可以設計過載保護。
在項目的架構(gòu)中,我們一般會同時部署 LVS 和 Nginx 來做 HTTP 應用服務的負載均衡。也就是說,在入口處部署 LVS,將流量分發(fā)到多個 Nginx 服務器上,再由 Nginx 服務器分發(fā)到應用服務器上。
為什么這么做呢?
主要和 LVS 和 Nginx 的特點有關(guān),LVS 是在網(wǎng)絡棧的四層做請求包的轉(zhuǎn)發(fā),請求包轉(zhuǎn)發(fā)之后,由客戶端和后端服務直接建立連接,后續(xù)的響應包不會再經(jīng)過 LVS 服務器,所以相比 Nginx,性能會更高,也能夠承擔更高的并發(fā)。
可 LVS 缺陷是工作在四層,而請求的URL是七層的概念,不能針對URL做更細致地請求分發(fā),而且LVS也沒有提供探測后端服務是否存活的機制;而Nginx雖然比LVS的性能差很多,但也可以承擔每秒幾萬次的請求,并且它在配置上更加靈活,還可以感知后端服務是否出現(xiàn)問題。
因此,LVS適合在入口處,承擔大流量的請求分發(fā),而Nginx要部在業(yè)務服務器之前做更細維度的請求分發(fā)。
我給你的建議是,如果你的QPS在十萬以內(nèi),那么可以考慮不引入 LVS 而直接使用 Nginx 作為唯一的負載均衡服務器,這樣少維護一個組件,也會減少系統(tǒng)的維護成本。
但對于Nginx來說,我們要如何保證配置的服務節(jié)點是可用的呢?
這就要感謝淘寶開源的 Nginx 模塊 nginx_upstream_check_moduule 了,這個模塊可以讓 Nginx 定期地探測后端服務的一個指定的接口,然后根據(jù)返回的狀態(tài)碼,來判斷服務是否還存活。當探測不存活的次數(shù)達到一定閾值時,就自動將這個后端服務從負載均衡服務器中摘除。
它的配置樣例如下:
upstream server { server 192.168.1.1:8080; server 192.168.1.2:8080; check interval=3000 rise=2 fall=5 timeout=1000 type=http default_down=true check_http_send "GET /health_check HTTP/1.0/r/n/n/n/n"; //檢測URL check_http_expect_alivehttp_2xx; //檢測返回狀態(tài)碼為 200 時認為檢測成功}
不過這兩個負載均衡服務適用于普通的Web服務,對于微服務多架構(gòu)來說,它們是不合適的。因為微服務架構(gòu)中的服務節(jié)點存儲在注冊中心里,使用 LVS 就很難和注冊中心交互,獲取全量的服務節(jié)點列表。
另外,一般微服務架構(gòu)中,使用的是RPC協(xié)議而不是HTTP協(xié)議,所以Nginx也不能滿足要求。
所以,我們會使用另一類的負載均衡服務,客戶端負載均衡服務,也就是把負載均衡的服務內(nèi)嵌在RPC客戶端中。
當我們的應用單實例不能支撐用戶請求時,此時就需要擴容,從一臺服務器擴容到兩臺、幾十臺、幾百臺。
然而,用戶訪問時是通過如 http://www.jd.com 的方式訪問,在請求時,瀏覽器首先會查詢DNS服務器獲取對應的IP,然后通過此 IP 訪問對應的服務。
因此,一種方式是 www.jd.com 域名映射多個IP,但是,存在一個最簡單的問題,假設某臺服務器重啟或者出現(xiàn)故障,DNS 會有一定的緩存時間,故障后切換時間長,而且沒有對后端服務進行心跳檢查和失敗重試的機制。
對于一般應用來說,有Nginx就可以了。但Nginx一般用于七層負載均衡,其吞吐量是有一定限制的。為了提升整體吞吐量,會在 DNS 和 Nginx之間引入接入層,如使用LVS(軟件負載均衡器)、F5(硬負載均衡器)可以做四層負載均衡,即首先 DNS解析到LVS/F5,然后LVS/F5轉(zhuǎn)發(fā)給Nginx,再由Nginx轉(zhuǎn)發(fā)給后端Real Server。
圖片
對于一般業(yè)務開發(fā)人員來說,我們只需要關(guān)心到Nginx層面就夠了,LVS/F5一般由系統(tǒng)/運維工程師來維護。Nginx目前提供了HTTP (ngx_http_upstream_module)七層負載均衡,而1.9.0版本也開始支持TCP(ngx_stream_upstream_module)四層負載均衡。
一致性hash算法最好在 lua腳本里指定。
Nginx商業(yè)版還提供了 least_time,即基于最小平均響應時間進行負載均衡。
Nginx的服務檢查是惰性的,Nginx只有當有訪問時后,才發(fā)起對后端節(jié)點探測。如果本次請求中,節(jié)點正好出現(xiàn)故障,Nginx依然將請求轉(zhuǎn)交給故障的節(jié)點,然后再轉(zhuǎn)交給健康的節(jié)點處理。所以不會影響到這次請求的正常進行。但是會影響效率,因為多了一次轉(zhuǎn)發(fā),而且自帶模塊無法做到預警。
比如對于訂單庫,當對其分庫分表后,如果想按照商家維度或者按照用戶維度進行查詢,那么是非常困難的,因此可以通過異構(gòu)數(shù)據(jù)庫來解決這個問題。可以采用下圖的架構(gòu)。
圖片
異構(gòu)數(shù)據(jù)主要存儲數(shù)據(jù)之間的關(guān)系,然后通過查詢源庫查詢實際數(shù)據(jù)。不過,有時可以通過數(shù)據(jù)冗余存儲來減少源庫查詢量或者提升查詢性能。
針對這類場景問題,最常用的是采用“異構(gòu)索引表”的方式解決,即采用異步機制將原表的每一次創(chuàng)建或更新,都換另一個維度保存一份完整的數(shù)據(jù)表或索引表,拿空間換時間。
也就是應用在插入或更新一條訂單ID為分庫分表鍵的訂單數(shù)據(jù)時,也會再保存一份按照買家ID為分庫分表鍵的訂單索引數(shù)據(jù),其結(jié)果就是同一買家的所有訂單索引表都保存在同一數(shù)據(jù)庫中,這就是給訂單創(chuàng)建了異構(gòu)索引表。
本文鏈接:http://www.www897cc.com/showinfo-26-94282-0.html我們一起聊聊如何設計一個秒殺系統(tǒng)?
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com