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