王向維,京東商城三級列表頁架構師。工做期間,完成了京東三級列表頁由Node.js版本到Nginx+Lua版本的變遷,並針對三級列表頁前端即服務器端作了大量的優化工做。html
列表頁是京東商城的三大核心繫統之一。京東三級列表頁是用戶選取商品類型後,展現同類商品的頁面,具體以下圖所示。前端
(點擊放大圖像)算法
用戶在首頁左側的導航樹中(以下圖所示)、所有商品分類列表頁或者頂部麪包屑導航中,選擇到商品的最小分類級別後,就能夠到達三級列表頁。後端
(點擊放大圖像)瀏覽器
相關廠商內容緩存
相關贊助商服務器
QCon全球軟件開發大會上海站,2016年10月20日-22日,上海寶華萬豪酒店,精彩內容搶先看!cookie
該頁面根據用戶選擇的商品類目進行檢索,將結果以列表的形式展示在頁面上。使用戶快速地找到本身須要的產品,提升用戶購買轉化率。網絡
涉及到多維度多因子質量分綜合排序,排序質量的效果直接關係到轉化率,客單價,進而影響到GMV,實質上是數據挖掘和機器學習技術在海量數據上的應用。架構
在不一樣三級類目下,經過複雜不肯定的查詢條件、屬性區域和列表查詢結果的實時聯動,以及跟區域相關的庫存、京東配送、貨到付款等複雜業務邏輯下,作到高併發實時計算。
三級列表頁的頁面周圍依賴的內部系統太多,要作到異步化展現,阻塞可降級。
在持續開發一個核心繫統過程當中,除了知足業務需求外,還應該考慮系統將來的架構,追求極致的系統的可用性、高性能和穩定性。這個過程是一個長期積累和重構的過程,京東三級列表頁的優化工做,就是這個過程的一部分。
優化前的三級列表頁有如下特色:
基於以上緣由,須要對三級列表頁作出改變,也就是對老版本進行重構。
經過優化,但願達到如下目的:
每一個應用都要知足本身特定的需求,由於其商業條件、應用場景、用戶指望,以及功能複雜性各不相同。儘管如此,若是應用必須對用戶做出響應,那咱們就必須從用戶角度來考慮可感知的處理時間這個常量。事實上,雖然生活節奏愈來愈快(至少咱們感受如此),但人類的感知和反應時間則一直都沒有變過:
下面這個表格展現了Web性能社區總結的經驗法則:必須在250 ms內渲染頁面,或者至少提供視覺反饋,才能保證用戶不走開。若是想讓人感受很快,就必須在幾百ms 內響應用戶操做。超過1s,用戶的預期流程就會中斷,心思就會向其餘任務轉移,而超過10s,除非你有反饋,不然用戶基本上就會終止任務!
(點擊放大圖像)
這次的優化工做遵循如下四個原則:
遵循這四個原則,進行了優化工做。
爲了保證首屏優先展現,HTML文檔進行了適當精簡。
目的:儘快渲染出頁面並達到可交互的狀態。
方法:
以下圖所示,列表頁的頭、麪包屑、品牌區、屬性篩選區、60個商品主圖數據,這些是服務端模板渲染輸出;而剩餘部分是在前端JS惰性加載或生成。
(點擊放大圖像)
惰性交互,即對需用戶交互的部分進行惰性加載。
對於三級列表頁品牌區,服務端只渲染18個品牌,用戶在點更多時,AJAX異步加載其餘的。對於整個屬性是篩選區服務端只渲染5行,其餘行用戶在點更多時,JS從文檔嵌入資源中取到數據,並渲染成HTML。這樣作能夠保證服務端計算量少,提高服務端性能,減小數據傳輸。
以下圖,點「更多」時才加載更多的品牌,由於有些三級類目有很是多品牌,若是不採用這種方式,整個頁面渲染很是慢。
(點擊放大圖像)
由於須要SEO的緣由,京東三級列表頁不能使用BigPipe等技術來進行更優的處理。
能不執行的先別執行,惰性執行。
上圖是三級列表頁最重要的商品區(商品主圖+N個關聯商品小圖),每一個商品的區域都是徹底同樣的;若是在服務端拼裝整個商品區域的話,尤爲涉及到小圖部分,會有很是多的重複HTML元素。
(點擊放大圖像)
咱們把體驗和減小頁面內容進行了折中處理:服務端渲染輸出商品主圖部分;小圖部分經過Json數據嵌入到頁面,而後經過JS惰性執行渲染。這樣能夠很好地對頁面進行瘦身。並且小圖資源是頁面嵌入的,非異步加載;沒有網絡請求。所以,用戶基本感知不到異步帶來的渲染閃動問題。
下圖就是頁面嵌入的小圖Json數據。
(點擊放大圖像)
三級列表頁的60個商品區域的圖片和頁尾都是當用戶向下滾動頁面時,纔去加載當前屏幕中的圖片和模塊。這樣能夠節省服務器帶寬和壓力,提高頁面總體渲染時間。
在實際優化過程當中,還涉及到很是多的優化細節。
把資源嵌入文檔能夠減小請求的次數。好比頁面須要的JS、CSS數據。以下圖所示:
(點擊放大圖像)
上圖中的這些JS對象,是後端渲染輸出的,所以不適合放入單獨的JS文件,直接在頁面中嵌入輸出會更好些。slaveWareList是小圖的列表對象。若是放在服務端模板渲染輸出的話,首先須要進行一些循環拼裝頁面;另外會使頁面體積變得很是大。
權衡以後決定放到前端JS渲染輸出。這樣也帶來了一些好處:
根據本身系統的業務,對每種資源定優先級:對必需的資源優先加載,而低優先級的請求保存在隊列中延時加載或等待必需資源加載完再加載;如:搜索推薦熱詞、頂部三個熱賣商品接口、60個主商品的圖片、價格優先加載。而對於庫存、促銷信息、廣告詞、預售商品、店鋪信息等,延後加載。對於點擊流,廣告統計數據則延時兩秒再加載。
三級列表頁中的每一個商品都是一個對象,存放在一個Map中,經過AJAX接口異步填充和維護商品的屬性。用於後續用戶交互用。同時維護成本也會下降;即頁面中用到的每一個商品數據放入一個map中,若是沒有則異步加載;若是有直接使用;即這些數據是公共數據。
(點擊放大圖像)
頁面每每依賴不少的異步接口,所以要對異步接口進行壓測,找出接口的最優調用方式。如京東三級列表頁依賴價格、庫存、廣告詞、店鋪信息等異步調用接口。而頁面有時候會出現多達300多個商品,若是用一個get請求把這些sku作參數,性能很是慢,那麼就要採用分組分批調用。如頁面商品在300個時,價格接口分六組,第一組30個,第二組30個,第三組60個,第四組60個,第五組100個,第六組100個。
對可能的域名進行提早解析,避免未來HTTP請求時的DNS延遲。如對價格、庫存、圖片、單品頁等服務預解析。
(點擊放大圖像)
>
HTTP 重定向極費時間,特別是不一樣域名之間的重定向,更加費時;這裏面既有額外的DNS 查詢、TCP 握手,還有其餘延遲。最佳的重定向次數爲零。好比三級列表頁之前是http://list.jd.com/9987-653-655.html,而如今是http://list.jd.com/list.html?cat=9987,653,655;在過渡期間能夠重定向,可是過渡完成後就不必重定向了。
把數據放到離用戶地理位置更近的地方,能夠顯著減小每次TCP鏈接的網絡延遲,增大吞吐量。好比京東三級列表頁、商品詳情頁、公共JS、CSS。
傳輸前應該壓縮應用資源,把要傳輸的字節減至最少:確保對每種要傳輸的資源採用最好的壓縮手段。全部文本資源都應該使用Gzip壓縮,而後再在客戶端與服務端間傳輸。通常來講,Gzip能夠減小60%~80%的文件大小,也是一個相對簡單(只要在服務器上配置一個選項),但優化效果較好的舉措。(對於壓縮級別,通過不一樣服務器屢次壓測,建議Nginx設置爲1-4)
任何請求都不如沒有請求快,把一些非必須的或者可異步的,或者可延遲的儘可能延遲請求。
應該緩存應用資源,從而避免每次請求都發送相同的內容。
對靜態資源CSS/JS或變化不頻繁的HTML塊,能夠放到前端localstorage。由於每次都傳輸一些不變的靜態文件或者HTML,實在是太浪費了。
Cookie 在不少應用中都是常見的性能瓶頸,不少開發者都會忽略它給每次請求增長的額外負擔;減小請求的HTTP首部數據(好比HTTP cookie),節省的時間至關於幾回往返的延遲時間。如列表頁依賴的價格、庫存接口,採用3.cn無狀態域名,從而減小主域下cookie傳輸。
請求和響應的排隊都會致使延遲,不管是客戶端仍是服務器端。這一點常常被忽視,但卻會無謂地致使很長延遲。
當頁面中很是多請求都是一個域名下資源時,因爲瀏覽器同時只能打開6個鏈接池,並且每一個連接池是對不一樣域名起做用,因此不少請求一個域名會出現排隊現象。若是把這些請求域名分區,讓請求並行,從而加快資源下載。如:頁面須要下載上百張圖片,對圖片進行域名分區調用。京東大部分頁面都對圖片進行了域名分區調用:
合併連接:把多個JavaScript 或CSS 文件組合爲一個文件。
拼合:把多張圖片組合爲一個更大的複合的圖片(CSS Sprites)。
把服務器IP後兩位寫到header,若是有問題,方便定位哪臺服務器。ups:後端路由的全部服務器都取到。把緩存命中信息或異常走兜底了,把後端運行狀態寫到header。Head-status:命中、未命中、異常等狀態。
(點擊放大圖像)
頁面依賴不少AJAX異步接口服務,不免保證這些服務從不出錯。因此在調用這些接口服務時都提早判斷該接口開關是否開啓,若是開關關閉則不調用該接口服務。頁面不展現相關模塊。保證在一個接口服務出問題時,咱們能夠快速降級。
當某個異步接口服務返回非200狀態碼、請求超時、數據格式不正確等異常,就會被動隱藏或不展現相應模塊。最上面三個熱賣商品依賴的廣告服務出問題時,會把每一個三級分類對應的三個兜底商品展現出來,防止開天窗。對於其餘模塊由於是商品的屬性,暫時作隱藏處理。
當頁面被動降級了,js就會上報該模塊,後臺程序記錄並報警。同時也會上報js運行中出錯的信息。記錄什麼瀏覽器,哪一個版本,什麼錯誤。咱們會對這些問題驗證和修改。保證每一個用戶都能訪問。
爲何要作Web性能監控,由於頁面可能放在CDN,前端JS執行不少業務邏輯不知道運行狀況,整個鏈路網絡偶爾不穩定、頁面依賴的模塊和第三方異步服務多人工難以實時監控等,這些狀況請求尚未到後端就可能出問題,因此後端監控無能爲力。
前端監控分兩個方向:用WebKit內核模擬瀏覽器,定時抓取設定的頁面;前端JS植入監控。
該Web監控項目採用一箇中心服務,多個終端服務來完成大量頁面抓取和校驗。
部署到全國各個機房,實時監控頁面是否打開正常(請求超時、返回非200)、頁面HTML關鍵元素是否丟失,頁面是否出現亂碼等。
每一個終端定時向中心服務請求須要處理的頁面URL和該頁面須要驗證的規則。若是驗證不經過,則記錄下來並報警。同時會保存現場(HTML文檔、頁面截圖)。
該項目在此次618起到很重要的做用,頁面出現任何問題,都會提早檢測出來。
該JS統計頁面白屏時間、首屏加載時間、每一個AJAX異步方法調用耗時和請求狀態碼。
同時也會上報異步模塊降級了,JS運行中錯誤信息等。
京東列表頁的埋點主要是來統計用戶點擊當前頁面位置記數,幫助廣告系統、業務、產品經理後續的工做。
埋點數據上報,就是經過onclick發送AJAX請求到後端服務。
其中對於點擊後刷新當前頁面的狀況,須要在新頁面記錄上次點擊的位置。由於在當前頁面點擊後上報AJAX方法還沒執行就關閉當前窗口加載點擊後的URL了。
下圖是點擊流插件的統計,數據敏感不作展現,你們只看功能。
(點擊放大圖像)
這次重構的時間段爲:2014年12月到2015年4月。
京東三級列表頁從優化到上線,已經經歷了兩個618和一個雙11的考驗,天天有上億的訪問量,頁面打開時間在20~80毫秒(在某些地區或低帶寬下會大於100ms)。
(點擊放大圖像)
後端方法調用tp99的性能數據以下圖所示。
(點擊放大圖像)
列表頁從開始200+ms到如今100ms內,QPS單臺機器幾百到如今的近萬,頁面從1MB到如今200KB內,包擴後臺系統的拆分,邏輯算法後移、後臺實時計算等優化。是須要有匠人的精神精雕細琢。
列表頁每週都會根據業務方和產品經理的需求在開發功能。對於每一個功能點都要深刻思考,列出多種方案,最終選擇一個簡單、易維護、不影響系統性能、不下降用戶體驗的方案。這個過程要不斷思考、或請教有這方面經驗的人、包括參考外部公司的方案。有趣的是可能晚上突發奇想就有更好的方案。
中間也遇到無數的坑。對於每次遇到各類問題,必須想方案避免再次出現。同時要分析Nginx日誌,分析每一個請求,進而對爬蟲、惡意參數訪問、惡意請求作相應處理。這些都是前端服務必作的。固然後端服務也是很是重要的,後續會有列表頁量身打造的緩存(加速、抗大流量、多樣化兜底基礎數據)、服務端架構、自動降級、架構高可用等方案。