京東商品詳情頁應對“雙11”大流量的技術實踐

大家來京東打開商品頁一般會看到如通用版、閃購、全球購等不同的頁面風格,這裏面會牽扯到各種各樣垂直化的模板頁面渲染。以前的解決方案是做靜態化,但是靜態化一個很大的問題就是頁面改版時需要重新全量生成新的靜態頁。我們有幾億個商品,對於這麼多商品,你如果生成頁面的話需要跑很多天,而且還無法應對一些突發情況。 

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy

比如新的《廣告法》,需要對一些數據進行清洗,後端清洗時間和成本來不及,那麼很多時候就是從前臺展示系統來進行數據過濾。因此需要非常靈活的前端展示架構來支持這種需求。 

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy 

首先這是我們前端首屏大體的結構。首屏有標題、價格、價格、庫存服務,服務支持,延保服務等,對於中心區有很多很多種服務。而這麼多的服務只是首屏裏的一部分。對於這麼多服務如何在這個頁面裏,或者在一個頁面裏讓它非常非常好的融合進來,這是我們要去解決的問題。 

而第二屏大家看到的就是廣告等等的。在這兒會有品牌服務,因爲京東有第三方商家,我們會提供廣告位,叫商家模板。還有像商品介紹、評價、諮詢等等,這一屏也包含了很多的服務。

商品詳情頁涉及的服務

對於商品詳情頁涉及瞭如下主要服務: 

  •   商品詳情頁HTML頁面渲染

  •   價格服務

  •   促銷服務

  •   庫存狀態/配送至服務

  •   廣告詞服務

  •   預售/秒殺服務

  •   評價服務

  •   試用服務

  •   推薦服務

  •   商品介紹服務

  •   各品類相關的一些特殊服務

對於詳情頁我們採用了KV結構存儲,但它是長尾,即數據是離散數據。這種方式的話,如果你做一般緩存的話,可能效率並不是特別高,只會緩存一些熱點,像一些秒殺的商品放在緩存會有效果。這裏還涉及到很多爬蟲和一些軟件會抓取我們頁面,如果你緩存有問題的話,你的數據很快就會從緩存中刷出去。所以設計的時候要考慮離散數據問題。

 



最早期的時候,我們商品詳情頁採用.NET技術,但是隨着商品數量增加,而且隨着商品數據庫結構設計複雜性的變化,後來我們就生成了靜態頁,通過JAVA生成頁面的片段,像商品介紹等等,都是通過一個一個片段輸送出去的。在這一層我們其實遇到過很多問題,比如這裏會生成很多的小文件,小文件如果你的磁盤用EXT3或者其他的話,會受到INODE的限制。

另外一個問題,我們生成這種頁面片段的話,經常會涉及到,如果頁面整體風格改變的話需要進行全量的數據刷新。比如要支持閃購單品也。對於這種的話,我們就需要把所有閃購頁面重新生成靜態頁。如果我們業務變化很快,說這個頁面不是我要的,就需要重新生成靜態頁,再重新刷一下。這對幾萬數量的商品沒問題,但是現在我們的商品規模量很龐大,這樣的話,可能會把依賴的系統刷掛,因爲你調用的依賴方會非常多。假設我們現在依賴的有二十個,每一個頁面要調動二十多個來源來拿到相應的數據。

後來我們發現這個問題,其實最主要的就是頁面模板變更的速度不能滿足我們需求;另一個,靜態頁我們用的機械盤,當遇到大流量時會非常非常慢。後來我們將它動態化,通過JAVA Worker把數據存到KV存儲裏,前端就是Nginx+Lua,這樣模板就是數據全動態化。對於這套架構我們現在已經在線跑了一年多,整體的性能非常穩定,平均響應時間在50毫秒之內,基本可以保持在30~40ms左右。對於這套設計,現在變更需求可以非常迅速的去響應。

 



我們有一個商品詳情頁異構系統,依賴的服務非常多。我們用它把相關的數據源抓過來,同步Worker會把數據按照維度進行聚合。有商品維度,還有其他維度,比如商品介紹、分類、商家、品牌,對於這些維度我們都會分開進行存儲。比如展示商品詳情頁時,讀取商品信息、商品相關信息:分類,商家,品牌等等信息然後渲染頁面即可;而商品介紹讀出來吐出去就可以了。

這個其實本質也是靜態化思想,是把數據做的靜態化,而沒有把頁面靜態化,這樣的好處是頁面模塊可以隨時變更。另外你只要保證數字是原子化,原子化就是你沒有對它進行再加工,這樣就可以對它再利用再處理。

商品詳情頁統一服務系統的建立

商品詳情頁上異步加載的服務非常多,因此我們做了一套統一服務系統。爲什麼做這個系統?我們的目標就是所有在頁面中接入的請求或者接入的服務,都必須經過我們這個系統。 

  • 監控,監控每個服務的服務質量; 

  • 隨時通過我們自己的開關去做一些降級的處理。比如促銷慢了,可以隨時對它降級,保證後端的服務不被異常的流量打出問題來。這個系統前端是用的Nginx+Lua。 

  • 數據異構系統。像我們的庫存,大家可能看到我們的庫存,跟淘寶的庫存不太一樣。因爲京東有自營的和第三方的,看庫存的話顯示的有如有貨還是沒貨,是否有預訂,以及第三方可能還有運費的概念,第三方還存在配送時效問題,比如你買了多少天之後發貨。對於這些數據我們可以做異構,異構過來我們只依賴於自己不依賴其他人。其他人服務出問題了,抖動了或者響應慢了,對我們是沒有影響的。

核心的設計思想

  • 異構的思想。我們把別人的數據按照我們自己的維度,或者按照我們自己想要消費的數據的格式進行存儲。存儲之後我們只消費我們自己的數據,其他人的數據我們都不依賴了。相當於別人的接口怎麼抖動對我不影響的。像雙十一我們有一個集羣,比如商品掛了,前端還是可以提供服務,只是數據不更新了。還有一個如雙十一期間一些商品不更新但是要做秒殺,我們可以通過前端邏輯處理,在系統裏進行人工打上標籤,打上之後就可以進行秒殺了。

  • 服務閉環的思想。假設我們在設計頁面的時候有很多服務依賴於別人,出問題之後肯定先找我們。找我們的時候我們又需要去聯繫其他的部門,就會存在溝通的問題。如果我們能夠及早發現這個問題,進行預案處理,比如降級,如庫存出問題了,讓我們第一時間知道,我們可以降級爲全部有貨,讓大家都有貨可買,這就形成了服務閉環。所有服務接入都通過我們的系統接入,出現問題我們及時發現,進行降級處理。

  • 維度化存儲。在存儲數據的時候我們都是按照維度進行存儲的。然後我們按照使用方式獲取。比如我們進行一個詳情頁的時候只需要兩次獲取,一次是拿商品信息,另外是拿商家分類等等。 

統一接入層和代理層

  • 統一入口,形成閉環。所有接入通過我們系統接入,這樣出問題後我非常容易找。

  • 做監控。比如這個接口響應慢了,我可以督促我這個依賴的業務。還有緩存前置,在前端有5-10秒緩存,對於這個時間大家是可以忍受的。我們把緩存前置,我們Nginx+Lua,它的併發是非常高的。緩存前置後很多流量導不到你的業務層;即我們儘量讓流量在前端處理掉,而不到達我們的業務層。

  • 業務前置,像庫存封裝,我們會在Nginx+Lua做一些簡單的處理。做一些簡單的數據處理,像一些人爲非法傳入的數據,都會在這一層過濾掉。

  • 新版測試。像我們做了一個延保服務,我想知道它的之前和之後的效果怎麼樣的,我就需要對一部分人用A版,一部分人用B版,在我們這層可以實現。比如根據用戶的ID,或者每次用戶訪問的時候都會用UUID。而且在這裏通過Nginx+Lua,通過Lua寫一些程序,在這裏都是通過程序控制AB測試的。還有像引流,發佈,流量切換都是在這層完成的。

  • 比如我們在上線的時候都會有一些開關的概念,在Nginx+Lua這一層我們會通過寫代碼的方式,有50%的用戶用新版,然後慢慢一步一步往上加,而且大多數流量控制在我們的前端。

  • 做一些線上壓測,通過Lua協程機制,把一個請求併發分成兩個請求打到後端,然後你再做一些邏輯的驗證。

  • 降級開關前置

  • 監控服務質量

  • 限流等 

我們做實踐的時候會做服務的隔離。爲什麼做隔離呢?非常簡單,假設你的一個系統裏進行http調用,而忘了設超時時間,此時流量很大時,http服務出問題了,這很可能會導致應用掛掉。所以我們設計的時候會把我們的業務進行分級,在一個應用裏對業務分級:0級業務,1級業務;如庫存,這裏面庫存就是必須的,沒有這個業務,頁面不會進行下一步流程,我們設置爲0級服務;而如延保服務沒有也不影響,我們設置爲1級。在這裏我們用了servlet3異步化,通過異步化我們把請求接收到,然後存到隔離的池子裏,然後這些池子的請求是相互隔離的,假如一個池子出問題了不會對另一個產生影響的。之前在做的時候其實是遇到過,比如在開發試用報告,沒有加超時時間,把我們的應用打掛了。

部署和分組隔離。比如我們有一個業務,這個業務可能非常非常多人依賴我,我就可以進行分組。A部門調這個分組,B部門調那個分組。爲什麼這麼做呢?因爲你不能保證所有人按照你的流程來做。像壓測沒有告訴你,導致你沒有增加流量等等。對於這種情況我儘量分離,你這樣了對其他人是不受影響的。分組,就是不同的部門調不同的分組,或者按照調用方分級進行不同的分組。

到最後的時候,假設一個應用裏面牽扯的服務特別特別多,但是這些服務又特別重要,像價格一天可能幾百億的量,這個時候就可以做一個單獨服務。像促銷、庫存等等都可以單獨拆出來做一個服務。如果前期沒有問題的話,大家更多時候是把它做成一個大的項目。大項目一重啓就會產生抖動,而抖動是對所有服務的。因此我們需要拆應用隔離。

對於分佈式緩存大家應用比較多的可能是Redis、Memcached。這裏我們前端Nginx會用一致性哈希的概念,如通過分類進行一致性哈希,讓它一致性哈希到不同的Nginx實例增加命中率。還有對於一些錯誤數據或者一些兜底的數據是不做緩存的。

對於突發流量,我們使用比較多的是高效緩存最有效的就是把數據拿到你這邊緩存,這樣這個數據就受你控制了。還有如你一個機房有一套數據,這樣的話沒有跨機房,整體的效率可能會有提升。這裏用的比較多的就是多級緩存,先做本地緩存,本地緩存沒有命中就走分佈式。另外我們會做一些自動降級處理,像一些不是特別重要,我們自動根據超時時間降級,如第三方的配送時效,對於這個信息幾秒鐘或者幾分鐘沒有給用戶展示,並不會影響他的購買,對於這種數據我們會做一個,比如超過500毫秒或者200毫秒就自動降級,就是這個數據不輸出了。還有一些數據沒法兒降級的,比如價格,沒有的話可能頁面就是空,我們不會對它進行緩存。還有庫存,我們沒法兒做很大的緩存。還有我們儘量減少回源量,就是用一致性哈希。我們還會用非阻塞鎖和304響應,如304響應適合如秒殺時一直點刷新按鈕,而此時的一些異步加載數據沒必要請求到服務端重新計算,此時就適合設置過期時間,如10s,10s內都返回304。還有對一些惡意訪問,這個我們只能更多的去提升我們的扛惡意的。比如我們通過KV存儲數據,這樣在KV命中的情況下是不怕刷的,因爲我們流量是足夠的,除非它們把我們帶寬打滿。還有就是提升緩存命中率,減少回源衝擊。還有我們會考慮把一些惡意的流量導流到另外一個分組,就是給一些惡意的用戶使用的,就是它也能用,但是慢。還有就是對N頁以後的請求做特殊處理,比如訪問一個列表的時候,像大家訪問更多的是前十頁,對後十頁就可以做特殊處理,比如限速,比如這個服務正常10毫秒就出來了,我給它放到100毫秒,這個我們都是在Nginx上做的,讓他把刷你的速度給降下來。

還有一些就是我們的兜底的數據,一種就是做靜態化。像我們會對前幾頁數據進行數據靜態化,像服務掛了,可以把這個靜態化的數據給大家提出來,不至於大家看到503頁面或404的狀況。還有就是沒法兒做緩存,就是說我們沒有降級方案的。

對於降級的話我們有兩種:

第一,人工降級。比如一些庫存,對於這種服務我們都是人工去監控,我們後臺都會有報警系統,像超過多少毫秒都會有報警,都會通過人工來控制。還有自動降級。剛纔提到了像超時降級,還有大訪問量的時候會自動降級,因爲訪問量你的系統承載不住了,否則的就會掛掉。我們做這個就是對一些用戶可用,對一些就是降級掉。

還有連接池超時時間,像大家都不去設置或者設置比較大,像一般訪問都沒有問題,但是一旦發生異常情況,像網絡抖動或者其他的情況,你的整個系統可能就會掛掉。還有就是重試時機和次數。重試時機,第一次訪問已經掛,接着第二次、第三次訪問,其實這個請求是沒有作用的。通過階梯式的方式或者階程式的方法慢慢做恢復。

還有CDN回源,我們做了版本化,現在評價也是版本化,爲什麼做版本化呢?因爲之前雙十一導致評價量非常非常大,你直接回源的話是扛不住的。所以我們現在做了評價版本化,有了版本號,這個頁面可以緩存很長時間,比如可以緩存一天、兩天;如果沒有版本號,只能緩存幾分鐘,然後回源。對於這種方式可以更高效的做CDN緩存。爬蟲不回源,不讓它到後端服務。返回歷史數據,非阻塞鎖。

這裏會做監控和報警,首先要知道系統的狀況,還應用實例存活,調用量,響應時間和可用率。調用量大了,可能就有惡意人刷你,你就要提前預警。這個降了,可能你依賴的服務出問題了,你要查哪些出問題了。

對於日誌,像我們看的比較多的就是Nginx的訪問日誌,訪問日誌看的比較多的就是IP,或者它的UA,看這些信息你就知道哪些是爬蟲,哪些是惡意訪問的,哪些是正常流量。出問題的時候,你可以干預或者通過其他的機制拒絕掉,不讓他請求。還有就是應用日誌,因爲業務的話會在這裏寫業務代碼,所以可以看到。還有應用日誌,應用的話比較多的就是業務的日誌和異常日誌。我們其實發現問題,更多的是通過日誌去發現,還有一些在開發,在記錄日誌的時候沒有任何含義,就一條,出錯了,什麼錯不知道。所以我們在內部的時候,要求把一些日誌要記清楚,什麼問題,哪些位置發生了,什麼異常都要記錄下來。對於比較重要的議程都直接報警。監控日誌會用調用量、響應時間和可用率。

我們在做系統的時候肯定要壓測,第一就是吞吐量壓測,就是看你係統最大壓測是多少。對於這種我們可能壓的是一個URL。這種方式存在一個很大的問題,如果是單個URL肯定是熱點,熱點壓沒有很大的意義。還有一種用的比較多的就是把線上的真實流量複製出來,然後在線上直接壓測。我們直接把線上的流量定向一份來壓測,來壓測你的極限。還有頁面埋點。壓測量的時候要考慮是讀還是寫,還是讀寫壓測。我們在壓測的時候,讀和寫性能非常好,一旦讀寫混合的時候在某一個點會抖動,它的響應時候會非常非常慢。像有人壓測的時候,順序非常好,一旦離散(所謂離散,就是有的人訪問1,有的人訪問2,這個沒有順序去訪問,這個是離散的)在壓測的時候你要知道你壓測的場景是什麼樣子的。

還有其他的,就是響應頭記錄服務器真實IP,前端JS瘦身,業務邏輯服務化後置,接入層數據過濾,數據校驗,緩存前置,一些業務邏輯前置,智能DNS,減少跨機房調用,提供刷數據接口進行異常數據更新或刪除,併發化提升性能。我們這裏用的比較多的,一個商品頁在拿數據的時候調了十幾、二十個接口,這些接口是有規則的,就是先拿商品的,拿其他的,這些接口可以並行的調用。假如之前調用需要1-2秒,通過併發化我們提升了300-400毫秒。



640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章