上一篇文章中卸例,我介紹了秒殺系統(tǒng)在架構(gòu)上要考慮的幾個原則好乐,我估計你很快就會問:“知易行難,這些原則應(yīng)該怎么應(yīng)用到系統(tǒng)中呢绰沥?”別急篱蝇,從這篇文章開始,我就會逐一介紹秒殺系統(tǒng)的各個關(guān)鍵環(huán)節(jié)中涉及的關(guān)鍵技術(shù)徽曲。
今天我們就先來討論第一個關(guān)鍵點:數(shù)據(jù)的動靜分離零截。不知道你之前聽過這個解決方案嗎?不管你有沒有聽過秃臣,我都建議你先停下來思考動靜分離的價值涧衙。如果你的系統(tǒng)還沒有開始應(yīng)用動靜分離的方案,那你也可以想想為什么沒有奥此,是之前沒有想到弧哎,還是說業(yè)務(wù)體量根本用不著?
不過我可以確信地說稚虎,如果你在一個業(yè)務(wù)飛速發(fā)展的公司里撤嫩,并且你在深度參與公司內(nèi)類秒殺類系統(tǒng)的架構(gòu)或者開發(fā)工作,那么你遲早會想到動靜分離的方案祥绞。為什么非洲?很簡單,秒殺的場景中蜕径,對于系統(tǒng)的要求其實就三個字:快两踏、準(zhǔn)、穩(wěn)兜喻。
那怎么才能“快”起來呢梦染?我覺得抽象起來講,就只有兩點朴皆,一點是提高單次請求的效率帕识,一點是減少沒必要的請求。今天我們聊到的“動靜分離”其實就是瞄著這個大方向去的遂铡。
不知道你是否還記得肮疗,最早的秒殺系統(tǒng)其實是要刷新整體頁面的,但后來秒殺的時候扒接,你只要點擊“刷新?lián)寣殹卑粹o就夠了伪货,這種變化的本質(zhì)就是動靜分離们衙,分離之后,客戶端大幅度減少了請求的數(shù)據(jù)量碱呼。這不自然就“快”了嗎蒙挑?
何為動靜數(shù)據(jù)
那到底什么才是動靜分離呢?所謂“動靜分離”愚臀,其實就是把用戶請求的數(shù)據(jù)(如 HTML 頁面)劃分為“動態(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ù)桃焕。比如說:
很多媒體類的網(wǎng)站,某一篇文章的內(nèi)容不管是你訪問還是我訪問捧毛,它都是一樣的。所以它就是一個典型的靜態(tài)數(shù)據(jù)让网,但是它是個動態(tài)頁面呀忧。
我們?nèi)绻F(xiàn)在訪問淘寶的首頁,每個人看到的頁面可能都是不一樣的溃睹,淘寶首頁中包含了很多根據(jù)訪問者特征推薦的信息而账,而這些個性化的數(shù)據(jù)就可以理解為動態(tài)數(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ù)锯茄。
還有一點要注意,就是頁面中“不包含”茶没,指的是“頁面的 HTML 源碼中不含有”肌幽,這一點務(wù)必要清楚。
理解了靜態(tài)數(shù)據(jù)和動態(tài)數(shù)據(jù)抓半,我估計你很容易就能想明白“動靜分離”這個方案的來龍去脈了喂急。分離了動靜數(shù)據(jù),我們就可以對分離出來的靜態(tài)數(shù)據(jù)做緩存笛求,有了緩存之后廊移,靜態(tài)數(shù)據(jù)的“訪問效率”自然就提高了讥蔽。
那么,怎樣對靜態(tài)數(shù)據(jù)做緩存呢画机?我在這里總結(jié)了幾個重點冶伞。
第一,你應(yīng)該把靜態(tài)數(shù)據(jù)緩存到離用戶最近的地方步氏。靜態(tài)數(shù)據(jù)就是那些相對不會變化的數(shù)據(jù)响禽,因此我們可以把它們緩存起來。緩存到哪里呢荚醒?常見的就三種芋类,用戶瀏覽器里、CDN 上或者在服務(wù)端的 Cache 中界阁。你應(yīng)該根據(jù)情況侯繁,把它們盡量緩存到離用戶最近的地方。
第二泡躯,靜態(tài)化改造就是要直接緩存 HTTP 連接贮竟。相較于普通的數(shù)據(jù)緩存而言,你肯定還聽過系統(tǒng)的靜態(tài)化改造较剃。靜態(tài)化改造是直接緩存 HTTP 連接而不是僅僅緩存數(shù)據(jù)咕别,如下圖所示,Web 代理服務(wù)器根據(jù)請求 URL写穴,直接取出對應(yīng)的 HTTP 響應(yīng)頭和響應(yīng)體然后直接返回惰拱,這個響應(yīng)過程簡單得連 HTTP 協(xié)議都不用重新組裝,甚至連 HTTP 請求頭也不需要解析啊送。
第三偿短,讓誰來緩存靜態(tài)數(shù)據(jù)也很重要。不同語言寫的 Cache 軟件處理緩存數(shù)據(jù)的效率也各不相同馋没。以 Java 為例昔逗,因為 Java 系統(tǒng)本身也有其弱點(比如不擅長處理大量連接請求,每個連接消耗的內(nèi)存較多披泪,Servlet 容器解析 HTTP 協(xié)議較慢)纤子,所以你可以不在 Java 層做緩存,而是直接在 Web 服務(wù)器層上做款票,這樣你就可以屏蔽 Java 語言層面的一些弱點控硼;而相比起來,Web 服務(wù)器(如 Nginx艾少、Apache卡乾、Varnish)也更擅長處理大并發(fā)的靜態(tài)文件請求。
如何做動靜分離的改造
理解了動靜態(tài)數(shù)據(jù)的“why”和“what”缚够,接下來我們就要看“how”了幔妨。我們?nèi)绾伟褎討B(tài)頁面改造成適合緩存的靜態(tài)頁面呢鹦赎?其實也很簡單,就是去除前面所說的那幾個影響因素误堡,把它們單獨分離出來古话,做動靜分離。
下面锁施,我以典型的商品詳情系統(tǒng)為例來詳細介紹陪踩。這里,你可以先打開京東或者淘寶的商品詳情頁悉抵,看看這個頁面里都有哪些動靜數(shù)據(jù)肩狂。我們從以下 5 個方面來分離出動態(tài)內(nèi)容。
URL 唯一化姥饰。商品詳情系統(tǒng)天然地就可以做到 URL 唯一化傻谁,比如每個商品都由 ID 來標(biāo)識,那么 http://www.aaaa.com/item.htm?id=xxxx 就可以作為唯一的 URL 標(biāo)識列粪。為啥要 URL 唯一呢审磁?前面說了我們是要緩存整個 HTTP 連接,那么以什么作為 Key 呢篱竭?就以 URL 作為緩存的 Key力图,例如以 id=xxx 這個格式進行區(qū)分。
分離瀏覽者相關(guān)的因素掺逼。瀏覽者相關(guān)的因素包括是否已登錄,以及登錄身份等瓤介,這些相關(guān)因素我們可以單獨拆分出來吕喘,通過動態(tài)請求來獲取。
分離時間因素刑桑。服務(wù)端輸出的時間也通過動態(tài)請求獲取氯质。
異步化地域因素。詳情頁面上與地域相關(guān)的因素做成異步方式獲取祠斧,當(dāng)然你也可以通過動態(tài)請求方式獲取闻察,只是這里通過異步獲取更合適。
去掉 Cookie琢锋。服務(wù)端輸出的頁面包含的 Cookie 可以通過代碼軟件來刪除辕漂,如 Web 服務(wù)器 Varnish 可以通過 unset req.http.cookie 命令去掉 Cookie。注意吴超,這里說的去掉 Cookie 并不是用戶端收到的頁面就不含 Cookie 了钉嘹,而是說,在緩存的靜態(tài)數(shù)據(jù)中不含有 Cookie鲸阻。
分離出動態(tài)內(nèi)容之后跋涣,如何組織這些內(nèi)容頁就變得非常關(guān)鍵了缨睡。這里我要提醒你一點,因為這其中很多動態(tài)內(nèi)容都會被頁面中的其他模塊用到陈辱,如判斷該用戶是否已登錄奖年、用戶 ID 是否匹配等,所以這個時候我們應(yīng)該將這些信息 JSON 化(用 JSON 格式組織這些數(shù)據(jù))沛贪,以方便前端獲取陋守。
前面我們介紹里用緩存的方式來處理靜態(tài)數(shù)據(jù)。而動態(tài)內(nèi)容的處理通常有兩種方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案鹏浅。
ESI 方案(或者 SSI):即在 Web 代理服務(wù)器上做動態(tài)內(nèi)容請求嗅义,并將請求插入到靜態(tài)頁面中,當(dāng)用戶拿到頁面時已經(jīng)是一個完整的頁面了隐砸。這種方式對服務(wù)端性能有些影響之碗,但是用戶體驗較好。
CSI 方案季希。即單獨發(fā)起一個異步 JavaScript 請求褪那,以向服務(wù)端獲取動態(tài)內(nèi)容。這種方式服務(wù)端性能更佳式塌,但是用戶端頁面可能會延時博敬,體驗稍差。
動靜分離的幾種架構(gòu)方案
前面我們通過改造把靜態(tài)數(shù)據(jù)和動態(tài)數(shù)據(jù)做了分離峰尝,那么如何在系統(tǒng)架構(gòu)上進一步對這些動態(tài)和靜態(tài)數(shù)據(jù)重新組合偏窝,再完整地輸出給用戶呢?
這就涉及對用戶請求路徑進行合理的架構(gòu)了武学。根據(jù)架構(gòu)上的復(fù)雜度祭往,有 3 種方案可選:
1.實體機單機部署;2.統(tǒng)一 Cache 層火窒;3.上 CDN硼补。
方案 1:實體機單機部署
這種方案是將虛擬機改為實體機,以增大 Cache 的容量熏矿,并且采用了一致性 Hash 分組的方式來提升命中率已骇。這里將 Cache 分成若干組,是希望能達到命中率和訪問熱點的平衡票编。Hash 分組越少褪储,緩存的命中率肯定就會越高,但短板是也會使單個商品集中在一個分組中栏妖,容易導(dǎo)致 Cache 被擊穿乱豆,所以我們應(yīng)該適當(dāng)增加多個相同的分組,來平衡訪問熱點和命中率的問題吊趾。
這里我給出了實體機單機部署方案的結(jié)構(gòu)圖宛裕,如下:
實體機單機部署有以下幾個優(yōu)點:
1.沒有網(wǎng)絡(luò)瓶頸瑟啃,而且能使用大內(nèi)存;2.既能提升命中率揩尸,又能減少 Gzip 壓縮; 3.減少 Cache 失效壓力蛹屿,因為采用定時失效方式,例如只緩存 3 秒鐘岩榆,過期即自動失效错负。
這個方案中,雖然把通常只需要虛擬機或者容器運行的 Java 應(yīng)用換成實體機勇边,優(yōu)勢很明顯犹撒,它會增加單機的內(nèi)存容量,但是一定程度上也造成了 CPU 的浪費粒褒,因為單個的 Java 進程很難用完整個實體機的 CPU识颊。
另外就是,一個實體機上部署了 Java 應(yīng)用又作為 Cache 來使用奕坟,這造成了運維上的高復(fù)雜度祥款,所以這是一個折中的方案。如果你的公司里月杉,沒有更多的系統(tǒng)有類似需求刃跛,那么這樣做也比較合適,如果你們有多個業(yè)務(wù)系統(tǒng)都有靜態(tài)化改造的需求苛萎,那還是建議把 Cache 層單獨抽出來公用比較合理桨昙,如下面的方案 2 所示。
方案 2:統(tǒng)一 Cache 層
所謂統(tǒng)一 Cache 層腌歉,就是將單機的 Cache 統(tǒng)一分離出來绊率,形成一個單獨的 Cache 集群。統(tǒng)一 Cache 層是個更理想的可推廣方案究履,該方案的結(jié)構(gòu)圖如下:
將 Cache 層單獨拿出來統(tǒng)一管理可以減少運維成本,同時也方便接入其他靜態(tài)化系統(tǒng)脸狸。此外最仑,它還有一些優(yōu)點。
單獨一個 Cache 層炊甲,可以減少多個應(yīng)用接入時使用 Cache 的成本泥彤。這樣接入的應(yīng)用只要維護自己的 Java 系統(tǒng)就好,不需要單獨維護 Cache卿啡,而只關(guān)心如何使用即可吟吝。
統(tǒng)一 Cache 的方案更易于維護,如后面加強監(jiān)控颈娜、配置的自動化剑逃,只需要一套解決方案就行浙宜,統(tǒng)一起來維護升級也比較方便。
可以共享內(nèi)存蛹磺,最大化利用內(nèi)存粟瞬,不同系統(tǒng)之間的內(nèi)存可以動態(tài)切換,從而能夠有效應(yīng)對各種攻擊萤捆。
這種方案雖然維護上更方便了裙品,但是也帶來了其他一些問題,比如緩存更加集中俗或,導(dǎo)致:
Cache 層內(nèi)部交換網(wǎng)絡(luò)成為瓶頸市怎;
緩存服務(wù)器的網(wǎng)卡也會是瓶頸;
機器少風(fēng)險較大辛慰,掛掉一臺就會影響很大一部分緩存數(shù)據(jù)区匠。
要解決上面這些問題,可以再對 Cache 做 Hash 分組昆雀,即一組 Cache 緩存的內(nèi)容相同辱志,這樣能夠避免熱點數(shù)據(jù)過度集中導(dǎo)致新的瓶頸產(chǎn)生。
方案 3:上 CDN
在將整個系統(tǒng)做動靜分離后狞膘,我們自然會想到更進一步的方案揩懒,就是將 Cache 進一步前移到 CDN 上,因為 CDN 離用戶最近挽封,效果會更好已球。
但是要想這么做,有以下幾個問題需要解決辅愿。
失效問題智亮。前面我們也有提到過緩存時效的問題,不知道你有沒有理解点待,我再來解釋一下阔蛉。談到靜態(tài)數(shù)據(jù)時,我說過一個關(guān)鍵詞叫“相對不變”癞埠,它的言外之意是“可能會變化”状原。比如一篇文章,現(xiàn)在不變苗踪,但如果你發(fā)現(xiàn)個錯別字颠区,是不是就會變化了?如果你的緩存時效很長通铲,那用戶端在很長一段時間內(nèi)看到的都是錯的毕莱。所以,這個方案中也是,我們需要保證 CDN 可以在秒級時間內(nèi)朋截,讓分布在全國各地的 Cache 同時失效蛹稍,這對 CDN 的失效系統(tǒng)要求很高。
命中率問題质和。Cache 最重要的一個衡量指標(biāo)就是“高命中率”稳摄,不然 Cache 的存在就失去了意義。同樣饲宿,如果將數(shù)據(jù)全部放到全國的 CDN 上厦酬,必然導(dǎo)致 Cache 分散,而 Cache 分散又會導(dǎo)致訪問請求命中同一個 Cache 的可能性降低瘫想,那么命中率就成為一個問題仗阅。
發(fā)布更新問題。如果一個業(yè)務(wù)系統(tǒng)每周都有日常業(yè)務(wù)需要發(fā)布国夜,那么發(fā)布系統(tǒng)必須足夠簡潔高效减噪,而且你還要考慮有問題時快速回滾和排查問題的簡便性。
從前面的分析來看车吹,將商品詳情系統(tǒng)放到全國的所有 CDN 節(jié)點上是不太現(xiàn)實的筹裕,因為存在失效問題、命中率問題以及系統(tǒng)的發(fā)布更新問題窄驹。那么是否可以選擇若干個節(jié)點來嘗試實施呢朝卒?答案是“可以”,但是這樣的節(jié)點需要滿足幾個條件:
靠近訪問量比較集中的地區(qū)乐埠;
離主站相對較遠抗斤;
節(jié)點到主站間的網(wǎng)絡(luò)比較好,而且穩(wěn)定丈咐;
節(jié)點容量比較大瑞眼,不會占用其他 CDN 太多的資源。
最后棵逊,還有一點也很重要伤疙,那就是:節(jié)點不要太多。
基于上面幾個因素辆影,選擇 CDN 的二級 Cache 比較合適掩浙,因為二級 Cache 數(shù)量偏少,容量也更大秸歧,讓用戶的請求先回源的 CDN 的二級 Cache 中,如果沒命中再回源站獲取數(shù)據(jù)衅澈,部署方式如下圖所示:
使用 CDN 的二級 Cache 作為緩存键菱,可以達到和當(dāng)前服務(wù)端靜態(tài)化 Cache 類似的命中率,因為節(jié)點數(shù)不多,Cache 不是很分散经备,訪問量也比較集中拭抬,這樣也就解決了命中率問題,同時能夠給用戶最好的訪問體驗侵蒙,是當(dāng)前比較理想的一種 CDN 化方案造虎。
除此之外,CDN 化部署方案還有以下幾個特點:
把整個頁面緩存在用戶瀏覽器中纷闺;
如果強制刷新整個頁面算凿,也會請求 CDN;
實際有效請求犁功,只是用戶對“刷新?lián)寣殹卑粹o的點擊氓轰。
這樣就把 90% 的靜態(tài)數(shù)據(jù)緩存在了用戶端或者 CDN 上,當(dāng)真正秒殺時浸卦,用戶只需要點擊特殊的“刷新?lián)寣殹卑粹o署鸡,而不需要刷新整個頁面。這樣一來限嫌,系統(tǒng)只是向服務(wù)端請求很少的有效數(shù)據(jù)靴庆,而不需要重復(fù)請求大量的靜態(tài)數(shù)據(jù)。
秒殺的動態(tài)數(shù)據(jù)和普通詳情頁面的動態(tài)數(shù)據(jù)相比更少怒医,性能也提升了 3 倍以上炉抒。所以“搶寶”這種設(shè)計思路,讓我們不用刷新頁面就能夠很好地請求到服務(wù)端最新的動態(tài)數(shù)據(jù)裆熙。
總結(jié)一下
今天端礼,我主要介紹了實現(xiàn)動靜分離的幾種思路,并由易到難給出了幾種架構(gòu)方案入录,以及它們各自的優(yōu)缺點蛤奥。可以看到僚稿,不同的架構(gòu)方案會引入不同的問題凡桥,比如我們把緩存數(shù)據(jù)從 CDN 上移到用戶的瀏覽器里,針對秒殺這個場景是沒問題的蚀同,但針對一般的商品可否也這樣做呢缅刽?
你可能會問,存儲在瀏覽器或 CDN 上蠢络,有多大區(qū)別衰猛?我的回答是:區(qū)別很大!因為在 CDN 上刹孔,我們可以做主動失效啡省,而在用戶的瀏覽器里就更不可控,如果用戶不主動刷新的話,你很難主動地把消息推送給用戶的瀏覽器卦睹。
另外畦戒,在什么地方把靜態(tài)數(shù)據(jù)和動態(tài)數(shù)據(jù)合并并渲染出一個完整的頁面也很關(guān)鍵,假如在用戶的瀏覽器里合并结序,那么服務(wù)端可以減少渲染整個頁面的 CPU 消耗障斋。如果在服務(wù)端合并的話,就要考慮緩存的數(shù)據(jù)是否進行 Gzip 壓縮了:如果緩存 Gzip 壓縮后的靜態(tài)數(shù)據(jù)可以減少緩存的數(shù)據(jù)量徐鹤,但是進行頁面合并渲染時就要先解壓垃环,然后再壓縮完整的頁面數(shù)據(jù)輸出給用戶;如果緩存未壓縮的靜態(tài)數(shù)據(jù)凳干,這樣不用解壓靜態(tài)數(shù)據(jù)晴裹,但是會增加緩存容量。雖然這些都是細節(jié)問題救赐,但你在設(shè)計架構(gòu)方案時都需要考慮清楚涧团。