京東商品詳情頁應對“雙11”大流量的技術實踐
【編者按】此文是根據京東資深Java工程師張開濤11月21日在msup主辦的 into100沙龍第14期《京東商品詳情頁應對大流量的一些實踐》演講中的分享內容整理而成。
以下為主題內容:
大家來京東開啟商品頁一般會看到如通用版、閃購、全球購等不同的頁面風格,這裡面會牽扯到各種各樣垂直化的模板頁面渲染。以前的解決方案是做靜態化,但是靜態化一個很大的問題就是頁面改版時需要重新全量生成新的靜態頁。我們有幾億個商品,對於這麼多商品,你如果生成頁面的話需要跑很多天,而且還無法應對一些突發情況。
比如新的《廣告法》,需要對一些資料進行清洗,後端清洗時間和成本來不及,那麼很多時候就是從前臺展示系統來進行資料過濾。因此需要非常靈活的前端展示架構來支援這種需求。
首先這是我們前端首屏大體的結構。首屏有標題、價格、價格、庫存服務,服務支援,延保服務等,對於中心區有很多很多種服務。而這麼多的服務只是首屏裡的一部分。對於這麼多服務如何在這個頁面裡,或者在一個頁面裡讓它非常非常好的融合進來,這是我們要去解決的問題。
而第二屏大家看到的就是廣告等等的。在這兒會有品牌服務,因為京東有第三方商家,我們會提供廣告位,叫商家模板。還有像商品介紹、評價、諮詢等等,這一屏也包含了很多的服務。
商品詳情頁涉及的服務
對於商品詳情頁涉及瞭如下主要服務:
- 商品詳情頁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毫秒。
作者介紹: 張開濤,京東資深Java工程師,2014年加入京東,主要負責商品詳情頁、詳情頁統一服務架構與開發工作,設計並開發了多個億級訪問量系統。工作之餘喜歡寫技術部落格,有《跟我學 Spring》、《跟我學Spring MVC》、《跟我學Shiro》、《跟我學Nginx+Lua開發》等系列教程,目前部落格訪問量有460萬