【編者按】此文是根據(jù)京東資深Java工程師張開(kāi)濤11月21日在msup主辦的into100沙龍第14期《京東商品詳情頁(yè)應(yīng)對(duì)大流量的一些實(shí)踐》演講中的分享內(nèi)容整理而成。
以下為主題內(nèi)容:
大家來(lái)京東打開(kāi)商品頁(yè)一般會(huì)看到如通用版、閃購(gòu)、全球購(gòu)等不同的頁(yè)面風(fēng)格,這里面會(huì)牽扯到各種各樣垂直化的模板頁(yè)面渲染。以前的解決方案是做靜態(tài)化,但是靜態(tài)化一個(gè)很大的問(wèn)題就是頁(yè)面改版時(shí)需要重新全量生成新的靜態(tài)頁(yè)。我們有幾億個(gè)商品,對(duì)于這么多商品,你如果生成頁(yè)面的話(huà)需要跑很多天,而且還無(wú)法應(yīng)對(duì)一些突發(fā)情況。
比如新的《廣告法》,需要對(duì)一些數(shù)據(jù)進(jìn)行清洗,后端清洗時(shí)間和成本來(lái)不及,那么很多時(shí)候就是從前臺(tái)展示系統(tǒng)來(lái)進(jìn)行數(shù)據(jù)過(guò)濾。因此需要非常靈活的前端展示架構(gòu)來(lái)支持這種需求。
首先這是我們前端首屏大體的結(jié)構(gòu)。首屏有標(biāo)題、價(jià)格、價(jià)格、庫(kù)存服務(wù),服務(wù)支持,延保服務(wù)等,對(duì)于中心區(qū)有很多很多種服務(wù)。而這么多的服務(wù)只是首屏里的一部分。對(duì)于這么多服務(wù)如何在這個(gè)頁(yè)面里,或者在一個(gè)頁(yè)面里讓它非常非常好的融合進(jìn)來(lái),這是我們要去解決的問(wèn)題。
而第二屏大家看到的就是廣告等等的。在這兒會(huì)有品牌服務(wù),因?yàn)榫〇|有第三方商家,我們會(huì)提供廣告位,叫商家模板。還有像商品介紹、評(píng)價(jià)、咨詢(xún)等等,這一屏也包含了很多的服務(wù)。
商品詳情頁(yè)涉及的服務(wù)對(duì)于商品詳情頁(yè)涉及了如下主要服務(wù):
對(duì)于詳情頁(yè)我們采用了KV結(jié)構(gòu)存儲(chǔ),但它是長(zhǎng)尾,即數(shù)據(jù)是離散數(shù)據(jù)。這種方式的話(huà),如果你做一般緩存的話(huà),可能效率并不是特別高,只會(huì)緩存一些熱點(diǎn),像一些秒殺的商品放在緩存會(huì)有效果。這里還涉及到很多爬蟲(chóng)和一些軟件會(huì)抓取我們頁(yè)面,如果你緩存有問(wèn)題的話(huà),你的數(shù)據(jù)很快就會(huì)從緩存中刷出去。所以設(shè)計(jì)的時(shí)候要考慮離散數(shù)據(jù)問(wèn)題。
最早期的時(shí)候,我們商品詳情頁(yè)采用.NET技術(shù),但是隨著商品數(shù)量增加,而且隨著商品數(shù)據(jù)庫(kù)結(jié)構(gòu)設(shè)計(jì)復(fù)雜性的變化,后來(lái)我們就生成了靜態(tài)頁(yè),通過(guò)JAVA生成頁(yè)面的片段,像商品介紹等等,都是通過(guò)一個(gè)一個(gè)片段輸送出去的。在這一層我們其實(shí)遇到過(guò)很多問(wèn)題,比如這里會(huì)生成很多的小文件,小文件如果你的磁盤(pán)用EXT3或者其他的話(huà),會(huì)受到INODE的限制。
另外一個(gè)問(wèn)題,我們生成這種頁(yè)面片段的話(huà),經(jīng)常會(huì)涉及到,如果頁(yè)面整體風(fēng)格改變的話(huà)需要進(jìn)行全量的數(shù)據(jù)刷新。比如要支持閃購(gòu)單品也。對(duì)于這種的話(huà),我們就需要把所有閃購(gòu)頁(yè)面重新生成靜態(tài)頁(yè)。如果我們業(yè)務(wù)變化很快,說(shuō)這個(gè)頁(yè)面不是我要的,就需要重新生成靜態(tài)頁(yè),再重新刷一下。這對(duì)幾萬(wàn)數(shù)量的商品沒(méi)問(wèn)題,但是現(xiàn)在我們的商品規(guī)模量很龐大,這樣的話(huà),可能會(huì)把依賴(lài)的系統(tǒng)刷掛,因?yàn)槟阏{(diào)用的依賴(lài)方會(huì)非常多。假設(shè)我們現(xiàn)在依賴(lài)的有二十個(gè),每一個(gè)頁(yè)面要調(diào)動(dòng)二十多個(gè)來(lái)源來(lái)拿到相應(yīng)的數(shù)據(jù)。
后來(lái)我們發(fā)現(xiàn)這個(gè)問(wèn)題,其實(shí)最主要的就是頁(yè)面模板變更的速度不能滿(mǎn)足我們需求;另一個(gè),靜態(tài)頁(yè)我們用的機(jī)械盤(pán),當(dāng)遇到大流量時(shí)會(huì)非常非常慢。后來(lái)我們將它動(dòng)態(tài)化,通過(guò)JAVA Worker把數(shù)據(jù)存到KV存儲(chǔ)里,前端就是Nginx+Lua,這樣模板就是數(shù)據(jù)全動(dòng)態(tài)化。對(duì)于這套架構(gòu)我們現(xiàn)在已經(jīng)在線跑了一年多,整體的性能非常穩(wěn)定,平均響應(yīng)時(shí)間在50毫秒之內(nèi),基本可以保持在30~40ms左右。對(duì)于這套設(shè)計(jì),現(xiàn)在變更需求可以非常迅速的去響應(yīng)。
我們有一個(gè)商品詳情頁(yè)異構(gòu)系統(tǒng),依賴(lài)的服務(wù)非常多。我們用它把相關(guān)的數(shù)據(jù)源抓過(guò)來(lái),同步Worker會(huì)把數(shù)據(jù)按照維度進(jìn)行聚合。有商品維度,還有其他維度,比如商品介紹、分類(lèi)、商家、品牌,對(duì)于這些維度我們都會(huì)分開(kāi)進(jìn)行存儲(chǔ)。比如展示商品詳情頁(yè)時(shí),讀取商品信息、商品相關(guān)信息:分類(lèi),商家,品牌等等信息然后渲染頁(yè)面即可;而商品介紹讀出來(lái)吐出去就可以了。
這個(gè)其實(shí)本質(zhì)也是靜態(tài)化思想,是把數(shù)據(jù)做的靜態(tài)化,而沒(méi)有把頁(yè)面靜態(tài)化,這樣的好處是頁(yè)面模塊可以隨時(shí)變更。另外你只要保證數(shù)字是原子化,原子化就是你沒(méi)有對(duì)它進(jìn)行再加工,這樣就可以對(duì)它再利用再處理。
商品詳情頁(yè)統(tǒng)一服務(wù)系統(tǒng)的建立商品詳情頁(yè)上異步加載的服務(wù)非常多,因此我們做了一套統(tǒng)一服務(wù)系統(tǒng)。為什么做這個(gè)系統(tǒng)?我們的目標(biāo)就是所有在頁(yè)面中接入的請(qǐng)求或者接入的服務(wù),都必須經(jīng)過(guò)我們這個(gè)系統(tǒng)。
我們做實(shí)踐的時(shí)候會(huì)做服務(wù)的隔離。為什么做隔離呢?非常簡(jiǎn)單,假設(shè)你的一個(gè)系統(tǒng)里進(jìn)行http調(diào)用,而忘了設(shè)超時(shí)時(shí)間,此時(shí)流量很大時(shí),http服務(wù)出問(wèn)題了,這很可能會(huì)導(dǎo)致應(yīng)用掛掉。所以我們?cè)O(shè)計(jì)的時(shí)候會(huì)把我們的業(yè)務(wù)進(jìn)行分級(jí),在一個(gè)應(yīng)用里對(duì)業(yè)務(wù)分級(jí):0級(jí)業(yè)務(wù),1級(jí)業(yè)務(wù);如庫(kù)存,這里面庫(kù)存就是必須的,沒(méi)有這個(gè)業(yè)務(wù),頁(yè)面不會(huì)進(jìn)行下一步流程,我們?cè)O(shè)置為0級(jí)服務(wù);而如延保服務(wù)沒(méi)有也不影響,我們?cè)O(shè)置為1級(jí)。在這里我們用了servlet3異步化,通過(guò)異步化我們把請(qǐng)求接收到,然后存到隔離的池子里,然后這些池子的請(qǐng)求是相互隔離的,假如一個(gè)池子出問(wèn)題了不會(huì)對(duì)另一個(gè)產(chǎn)生影響的。之前在做的時(shí)候其實(shí)是遇到過(guò),比如在開(kāi)發(fā)試用報(bào)告,沒(méi)有加超時(shí)時(shí)間,把我們的應(yīng)用打掛了。
部署和分組隔離。比如我們有一個(gè)業(yè)務(wù),這個(gè)業(yè)務(wù)可能非常非常多人依賴(lài)我,我就可以進(jìn)行分組。A部門(mén)調(diào)這個(gè)分組,B部門(mén)調(diào)那個(gè)分組。為什么這么做呢?因?yàn)槟悴荒鼙WC所有人按照你的流程來(lái)做。像壓測(cè)沒(méi)有告訴你,導(dǎo)致你沒(méi)有增加流量等等。對(duì)于這種情況我盡量分離,你這樣了對(duì)其他人是不受影響的。分組,就是不同的部門(mén)調(diào)不同的分組,或者按照調(diào)用方分級(jí)進(jìn)行不同的分組。
到最后的時(shí)候,假設(shè)一個(gè)應(yīng)用里面牽扯的服務(wù)特別特別多,但是這些服務(wù)又特別重要,像價(jià)格一天可能幾百億的量,這個(gè)時(shí)候就可以做一個(gè)單獨(dú)服務(wù)。像促銷(xiāo)、庫(kù)存等等都可以單獨(dú)拆出來(lái)做一個(gè)服務(wù)。如果前期沒(méi)有問(wèn)題的話(huà),大家更多時(shí)候是把它做成一個(gè)大的項(xiàng)目。大項(xiàng)目一重啟就會(huì)產(chǎn)生抖動(dòng),而抖動(dòng)是對(duì)所有服務(wù)的。因此我們需要拆應(yīng)用隔離。
對(duì)于分布式緩存大家應(yīng)用比較多的可能是Redis、Memcached。這里我們前端Nginx會(huì)用一致性哈希的概念,如通過(guò)分類(lèi)進(jìn)行一致性哈希,讓它一致性哈希到不同的Nginx實(shí)例增加命中率。還有對(duì)于一些錯(cuò)誤數(shù)據(jù)或者一些兜底的數(shù)據(jù)是不做緩存的。
對(duì)于突發(fā)流量,我們使用比較多的是高效緩存,最有效的就是把數(shù)據(jù)拿到你這邊緩存,這樣這個(gè)數(shù)據(jù)就受你控制了。還有如你一個(gè)機(jī)房有一套數(shù)據(jù),這樣的話(huà)沒(méi)有跨機(jī)房,整體的效率可能會(huì)有提升。這里用的比較多的就是多級(jí)緩存,先做本地緩存,本地緩存沒(méi)有命中就走分布式。另外我們會(huì)做一些自動(dòng)降級(jí)處理,像一些不是特別重要,我們自動(dòng)根據(jù)超時(shí)時(shí)間降級(jí),如第三方的配送時(shí)效,對(duì)于這個(gè)信息幾秒鐘或者幾分鐘沒(méi)有給用戶(hù)展示,并不會(huì)影響他的購(gòu)買(mǎi),對(duì)于這種數(shù)據(jù)我們會(huì)做一個(gè),比如超過(guò)500毫秒或者200毫秒就自動(dòng)降級(jí),就是這個(gè)數(shù)據(jù)不輸出了。還有一些數(shù)據(jù)沒(méi)法兒降級(jí)的,比如價(jià)格,沒(méi)有的話(huà)可能頁(yè)面就是空,我們不會(huì)對(duì)它進(jìn)行緩存。還有庫(kù)存,我們沒(méi)法兒做很大的緩存。還有我們盡量減少回源量,就是用一致性哈希。我們還會(huì)用非阻塞鎖和304響應(yīng),如304響應(yīng)適合如秒殺時(shí)一直點(diǎn)刷新按鈕,而此時(shí)的一些異步加載數(shù)據(jù)沒(méi)必要請(qǐng)求到服務(wù)端重新計(jì)算,此時(shí)就適合設(shè)置過(guò)期時(shí)間,如10s,10s內(nèi)都返回304。還有對(duì)一些惡意訪問(wèn),這個(gè)我們只能更多的去提升我們的扛惡意的。比如我們通過(guò)KV存儲(chǔ)數(shù)據(jù),這樣在KV命中的情況下是不怕刷的,因?yàn)槲覀兞髁渴亲銐虻模撬鼈儼盐覀儙挻驖M(mǎn)。還有就是提升緩存命中率,減少回源沖擊。還有我們會(huì)考慮把一些惡意的流量導(dǎo)流到另外一個(gè)分組,就是給一些惡意的用戶(hù)使用的,就是它也能用,但是慢。還有就是對(duì)N頁(yè)以后的請(qǐng)求做特殊處理,比如訪問(wèn)一個(gè)列表的時(shí)候,像大家訪問(wèn)更多的是前十頁(yè),對(duì)后十頁(yè)就可以做特殊處理,比如限速,比如這個(gè)服務(wù)正常10毫秒就出來(lái)了,我給它放到100毫秒,這個(gè)我們都是在Nginx上做的,讓他把刷你的速度給降下來(lái)。
還有一些就是我們的兜底的數(shù)據(jù),一種就是做靜態(tài)化。像我們會(huì)對(duì)前幾頁(yè)數(shù)據(jù)進(jìn)行數(shù)據(jù)靜態(tài)化,像服務(wù)掛了,可以把這個(gè)靜態(tài)化的數(shù)據(jù)給大家提出來(lái),不至于大家看到503頁(yè)面或404的狀況。還有就是沒(méi)法兒做緩存,就是說(shuō)我們沒(méi)有降級(jí)方案的。
對(duì)于降級(jí)的話(huà)我們有兩種:
第一,人工降級(jí)。比如一些庫(kù)存,對(duì)于這種服務(wù)我們都是人工去監(jiān)控,我們后臺(tái)都會(huì)有報(bào)警系統(tǒng),像超過(guò)多少毫秒都會(huì)有報(bào)警,都會(huì)通過(guò)人工來(lái)控制。還有自動(dòng)降級(jí)。剛才提到了像超時(shí)降級(jí),還有大訪問(wèn)量的時(shí)候會(huì)自動(dòng)降級(jí),因?yàn)樵L問(wèn)量你的系統(tǒng)承載不住了,否則的就會(huì)掛掉。我們做這個(gè)就是對(duì)一些用戶(hù)可用,對(duì)一些就是降級(jí)掉。
還有連接池超時(shí)時(shí)間,像大家都不去設(shè)置或者設(shè)置比較大,像一般訪問(wèn)都沒(méi)有問(wèn)題,但是一旦發(fā)生異常情況,像網(wǎng)絡(luò)抖動(dòng)或者其他的情況,你的整個(gè)系統(tǒng)可能就會(huì)掛掉。還有就是重試時(shí)機(jī)和次數(shù)。重試時(shí)機(jī),第一次訪問(wèn)已經(jīng)掛,接著第二次、第三次訪問(wèn),其實(shí)這個(gè)請(qǐng)求是沒(méi)有作用的。通過(guò)階梯式的方式或者階程式的方法慢慢做恢復(fù)。
還有CDN回源,我們做了版本化,現(xiàn)在評(píng)價(jià)也是版本化,為什么做版本化呢?因?yàn)橹半p十一導(dǎo)致評(píng)價(jià)量非常非常大,你直接回源的話(huà)是扛不住的。所以我們現(xiàn)在做了評(píng)價(jià)版本化,有了版本號(hào),這個(gè)頁(yè)面可以緩存很長(zhǎng)時(shí)間,比如可以緩存一天、兩天;如果沒(méi)有版本號(hào),只能緩存幾分鐘,然后回源。對(duì)于這種方式可以更高效的做CDN緩存。爬蟲(chóng)不回源,不讓它到后端服務(wù)。返回歷史數(shù)據(jù),非阻塞鎖。
這里會(huì)做監(jiān)控和報(bào)警,首先要知道系統(tǒng)的狀況,還應(yīng)用實(shí)例存活,調(diào)用量,響應(yīng)時(shí)間和可用率。調(diào)用量大了,可能就有惡意人刷你,你就要提前預(yù)警。這個(gè)降了,可能你依賴(lài)的服務(wù)出問(wèn)題了,你要查哪些出問(wèn)題了。
對(duì)于日志,像我們看的比較多的就是Nginx的訪問(wèn)日志,訪問(wèn)日志看的比較多的就是IP,或者它的UA,看這些信息你就知道哪些是爬蟲(chóng),哪些是惡意訪問(wèn)的,哪些是正常流量。出問(wèn)題的時(shí)候,你可以干預(yù)或者通過(guò)其他的機(jī)制拒絕掉,不讓他請(qǐng)求。還有就是應(yīng)用日志,因?yàn)闃I(yè)務(wù)的話(huà)會(huì)在這里寫(xiě)業(yè)務(wù)代碼,所以可以看到。還有應(yīng)用日志,應(yīng)用的話(huà)比較多的就是業(yè)務(wù)的日志和異常日志。我們其實(shí)發(fā)現(xiàn)問(wèn)題,更多的是通過(guò)日志去發(fā)現(xiàn),還有一些在開(kāi)發(fā),在記錄日志的時(shí)候沒(méi)有任何含義,就一條,出錯(cuò)了,什么錯(cuò)不知道。所以我們?cè)趦?nèi)部的時(shí)候,要求把一些日志要記清楚,什么問(wèn)題,哪些位置發(fā)生了,什么異常都要記錄下來(lái)。對(duì)于比較重要的議程都直接報(bào)警。監(jiān)控日志會(huì)用調(diào)用量、響應(yīng)時(shí)間和可用率。
我們?cè)谧鱿到y(tǒng)的時(shí)候肯定要壓測(cè),第一就是吞吐量壓測(cè),就是看你系統(tǒng)最大壓測(cè)是多少。對(duì)于這種我們可能壓的是一個(gè)URL。這種方式存在一個(gè)很大的問(wèn)題,如果是單個(gè)URL肯定是熱點(diǎn),熱點(diǎn)壓沒(méi)有很大的意義。還有一種用的比較多的就是把線上的真實(shí)流量復(fù)制出來(lái),然后在線上直接壓測(cè)。我們直接把線上的流量定向一份來(lái)壓測(cè),來(lái)壓測(cè)你的極限。還有頁(yè)面埋點(diǎn)。壓測(cè)量的時(shí)候要考慮是讀還是寫(xiě),還是讀寫(xiě)壓測(cè)。我們?cè)趬簻y(cè)的時(shí)候,讀和寫(xiě)性能非常好,一旦讀寫(xiě)混合的時(shí)候在某一個(gè)點(diǎn)會(huì)抖動(dòng),它的響應(yīng)時(shí)候會(huì)非常非常慢。像有人壓測(cè)的時(shí)候,順序非常好,一旦離散(所謂離散,就是有的人訪問(wèn)1,有的人訪問(wèn)2,這個(gè)沒(méi)有順序去訪問(wèn),這個(gè)是離散的)在壓測(cè)的時(shí)候你要知道你壓測(cè)的場(chǎng)景是什么樣子的。
還有其他的,就是響應(yīng)頭記錄服務(wù)器真實(shí)IP,前端JS瘦身,業(yè)務(wù)邏輯服務(wù)化后置,接入層數(shù)據(jù)過(guò)濾,數(shù)據(jù)校驗(yàn),緩存前置,一些業(yè)務(wù)邏輯前置,智能DNS,減少跨機(jī)房調(diào)用,提供刷數(shù)據(jù)接口進(jìn)行異常數(shù)據(jù)更新或刪除,并發(fā)化提升性能。我們這里用的比較多的,一個(gè)商品頁(yè)在拿數(shù)據(jù)的時(shí)候調(diào)了十幾、二十個(gè)接口,這些接口是有規(guī)則的,就是先拿商品的,拿其他的,這些接口可以并行的調(diào)用。假如之前調(diào)用需要1-2秒,通過(guò)并發(fā)化我們提升了300-400毫秒。
作者介紹: 張開(kāi)濤,京東資深Java工程師,2014年加入京東,主要負(fù)責(zé)商品詳情頁(yè)、詳情頁(yè)統(tǒng)一服務(wù)架構(gòu)與開(kāi)發(fā)工作,設(shè)計(jì)并開(kāi)發(fā)了多個(gè)億級(jí)訪問(wèn)量系統(tǒng)。工作之余喜歡寫(xiě)技術(shù)博客,有《跟我學(xué) Spring》、《跟我學(xué)Spring MVC》、《跟我學(xué)Shiro》、《跟我學(xué)Nginx+Lua開(kāi)發(fā)》等系列教程,目前博客訪問(wèn)量有460萬(wàn)+。
關(guān)于into100沙龍:是TOP100Summit全球軟件案例研究峰會(huì)的一個(gè)下屬品牌,從2015年1月起,每月在北京、上海、深圳等地巡回舉辦的技術(shù)沙龍?;顒?dòng)旨在交流軟件研發(fā)及互聯(lián)網(wǎng)技術(shù)的實(shí)戰(zhàn)經(jīng)驗(yàn),分享TOP100峰會(huì)那些優(yōu)秀的案例實(shí)踐,通過(guò)平臺(tái)結(jié)識(shí)更多友人,挖掘并傳播業(yè)界最具價(jià)值的技術(shù)實(shí)踐。
(責(zé)編/ 錢(qián)曙光,關(guān)注架構(gòu)和算法領(lǐng)域,尋求報(bào)道或者投稿請(qǐng)發(fā)郵件qianshg@csdn.net,交流探討可加微信qshuguang2008,備注姓名+公司+職位)
「CSDN 高級(jí)架構(gòu)師群」,內(nèi)有SDCC 2015架構(gòu)專(zhuān)場(chǎng)的講師等諸多知名互聯(lián)網(wǎng)公司的大牛架構(gòu)師,如果你想進(jìn)群交流,請(qǐng)加微信qshuguang2008申請(qǐng)入群,備注姓名+公司+職位。
聯(lián)系客服