京東技術架構(二)構建需求響應式億級商品詳情頁

商品詳情頁是什麼

商品詳情頁是展現商品詳細信息的一個頁面,承載在網站的大部分流量和訂單的入口。京東商城目前有通用版、全球購、閃購、易車、惠買車、服裝、拼購、今日抄底等許多套模板。各套模板的元數據是同樣的,只是展現方式不同。目前商品詳情頁個性化需求很是多,數據來源也是很是多的,並且許多基礎服務作不了的都放咱們這,所以咱們須要一種架構能快速響應和優雅的解決這些需求問題。所以咱們從新設計了商品詳情頁的架構,主要包括三部分:商品詳情頁系統、商品詳情頁統一服務系統和商品詳情頁動態服務系統;商品詳情頁系統負責靜的部分,而統一服務負責動的部分,而動態服務負責給內網其餘系統提供一些數據服務。html

 

 

商品詳情頁前端結構

前端展現能夠分爲這麼幾個維度:商品維度(標題、圖片、屬性等)、主商品維度(商品介紹、規格參數)、分類維度、商家維度、店鋪維度等;另外還有一些實時性要求比較高的如實時價格、實時促銷、廣告詞、配送至、預售等是經過異步加載。前端

京東商城還有一些特殊維度數據:好比套裝、手機合約機等,這些數據是主商品數據外掛的。java

咱們的性能數據

618當天PV數億,618當天服務器端響應時間<38ms。此處咱們用的是第1000次中第99次排名的時間。nginx

單品頁流量特色

離散數據,熱點少,各類爬蟲、比價軟件抓取。數據庫

單品頁技術架構發展

架構1.0


IIS+C#+Sql Server,最原始的架構,直接調用商品庫獲取相應的數據,扛不住時加了一層memcached來緩存數據。這種方式常常受到依賴的服務不穩定而致使的性能抖動。後端

架構2.0


該方案使用了靜態化技術,按照商品維度生成靜態化HTML。主要思路:瀏覽器

一、經過MQ獲得變動通知;緩存

二、經過Java Worker調用多個依賴系統生成詳情頁HTML;tomcat

三、經過rsync同步到其餘機器;服務器

四、經過Nginx直接輸出靜態頁;

五、接入層負責負載均衡。

該方案的主要缺點:

一、假設只有分類、麪包屑變動了,那麼全部相關的商品都要重刷;

二、隨着商品數量的增長,rsync會成爲瓶頸;

三、沒法迅速響應一些頁面需求變動,大部分都是經過JavaScript動態改頁面元素。

 

隨着商品數量的增長這種架構的存儲容量到達了瓶頸,並且按照商品維度生成整個頁面會存在如分類維度變動就要所有刷一遍這個分類下全部信息的問題,所以咱們又改造了一版按照尾號路由到多臺機器。



主要思路:

一、容量問題經過按照商品尾號作路由分散到多臺機器,按照自營商品單獨一臺,第三方商品按照尾號分散到11臺;

二、按維度生成HTML片斷(框架、商品介紹、規格參數、麪包屑、相關分類、店鋪信息),而不是一個大HTML;

三、經過Nginx SSI合併片斷輸出;

四、接入層負責負載均衡;

五、多機房部署也沒法經過rsync同步,而是使用部署多套相同的架構來實現。

 

該方案主要缺點:

一、碎片文件太多,致使如沒法rsync;

二、機械盤作SSI合併時,高併發時性能差,此時咱們尚未嘗試使用SSD;

三、模板若是要變動,數億商品須要數天才能刷完;

四、到達容量瓶頸時,咱們會刪除一部分靜態化商品,而後經過動態渲染輸出,動態渲染系統在高峯時會致使依賴系統壓力大,抗不住;

五、仍是沒法迅速響應一些業務需求。

 

咱們的痛點

一、以前架構的問題存在容量問題,很快就會出現沒法全量靜態化,仍是須要動態渲染;不過對於全量靜態化能夠經過分佈式文件系統解決該問題,這種方案沒有嘗試;

二、最主要的問題是隨着業務的發展,沒法知足迅速變化、還有一些變態的需求。

 

架構3.0

咱們要解決的問題:

一、能迅速響瞬變的需求,和各類變態需求;

二、支持各類垂直化頁面改版;

三、頁面模塊化;

四、AB測試;

五、高性能、水平擴容;

六、多機房多活、異地多活。

主要思路:

一、數據變動仍是經過MQ通知;

二、數據異構Worker獲得通知,而後按照一些維度進行數據存儲,存儲到數據異構JIMDB集羣(JIMDB:Redis+持久化引擎),存儲的數據都是未加工的原子化數據,如商品基本信息、商品擴展屬性、商品其餘一些相關信息、商品規格參數、分類、商家信息等;

三、數據異構Worker存儲成功後,會發送一個MQ給數據同步Worker,數據同步Worker也能夠叫作數據聚合Worker,按照相應的維度聚合數據存儲到相應的JIMDB集羣;三個維度:基本信息(基本信息+擴展屬性等的一個聚合)、商品介紹(PC版、移動版)、其餘信息(分類、商家等維度,數據量小,直接Redis存儲);

四、前端展現分爲兩個:商品詳情頁和商品介紹,使用Nginx+Lua技術獲取數據並渲染模板輸出。

 

另外咱們目前架構的目標不只僅是爲商品詳情頁提供數據,只要是Key-Value獲取的而非關係的咱們均可以提供服務,咱們叫作動態服務系統。

該動態服務分爲前端和後端,即公網仍是內網,如目前該動態服務爲列表頁、商品對比、微信單品頁、總代等提供相應的數據來知足和支持其業務。

 

詳情頁架構設計原則

一、數據閉環

二、數據維度化

三、拆分系統

四、Worker無狀態化+任務化

五、異步化+併發化

六、多級緩存化

七、動態化

八、彈性化

九、降級開關

十、多機房多活

十一、多種壓測方案

 

數據閉環


數據閉環即數據的自我管理,或者說是數據都在本身系統裏維護,不依賴於任何其餘系統,去依賴化;這樣獲得的好處就是別人抖動跟我不要緊。

 

數據異構,是數據閉環的第一步,將各個依賴系統的數據拿過來,按照本身的要求存儲起來;

 

數據原子化,數據異構的數據是原子化數據,這樣將來咱們能夠對這些數據再加工再處理而響應變化的需求;

 

數據聚合,將多個原子數據聚合爲一個大JSON數據,這樣前端展現只須要一次get,固然要考慮系統架構,好比咱們使用的Redis改造,Redis又是單線程系統,咱們須要部署更多的Redis來支持更高的併發,另外存儲的值要儘量的小;

 

數據存儲,咱們使用JIMDB,Redis加持久化存儲引擎,能夠存儲超過內存N倍的數據量,咱們目前一些系統是Redis+LMDB引擎的存儲,目前是配合SSD進行存儲;另外咱們使用Hash Tag機制把相關的數據哈希到同一個分片,這樣mget時不須要跨分片合併。

 

咱們目前的異構數據時鍵值結構的,用於按照商品維度查詢,還有一套異構時關係結構的用於關係查詢使用。

 

詳情頁架構設計原則 / 數據維度化

對於數據應該按照維度和做用進行維度化,這樣能夠分離存儲,進行更有效的存儲和使用。咱們數據的維度比較簡單:

一、商品基本信息,標題、擴展屬性、特殊屬性、圖片、顏色尺碼、規格參數等;

二、商品介紹信息,商品維度商家模板、商品介紹等;

三、非商品維度其餘信息,分類信息、商家信息、店鋪信息、店鋪頭、品牌信息等;

四、商品維度其餘信息(異步加載),價格、促銷、配送至、廣告詞、推薦配件、最佳組合等。

拆分系統


將系統拆分爲多個子系統雖然增長了複雜性,可是能夠獲得更多的好處,好比數據異構系統存儲的數據是原子化數據,這樣能夠按照一些維度對外提供服務;而數據同步系統存儲的是聚合數據,能夠爲前端展現提供高性能的讀取。而前端展現系統分離爲商品詳情頁和商品介紹,能夠減小相互影響;目前商品介紹系統還提供其餘的一些服務,好比全站異步頁腳服務。

 

 

Worker無狀態化+任務化

一、數據異構和數據同步Worker無狀態化設計,這樣能夠水平擴展;

二、應用雖然是無狀態化的,可是配置文件仍是有狀態的,每一個機房一套配置,這樣每一個機房只讀取當前機房數據;

三、任務多隊列化,等待隊列、排重隊列、本地執行隊列、失敗隊列;

四、隊列優先級化,分爲:普通隊列、刷數據隊列、高優先級隊列;例如一些秒殺商品會走高優先級隊列保證快速執行;

五、副本隊列,當上線後業務出現問題時,修正邏輯能夠回放,從而修復數據;能夠按照好比固定大小隊列或者小時隊列設計;

六、在設計消息時,按照維度更新,好比商品信息變動和商品上下架分離,減小每次變動接口的調用量,經過聚合Worker去作聚合。

 

 

多級緩存化

瀏覽器緩存,當頁面之間來回跳轉時走local cache,或者打開頁面時拿着Last-Modified去CDN驗證是否過時,減小來回傳輸的數據量;

CDN緩存,用戶去離本身最近的CDN節點拿數據,而不是都回源到北京機房獲取數據,提高訪問性能;

服務端應用本地緩存,咱們使用Nginx+Lua架構,使用HttpLuaModule模塊的shared dict作本地緩存( reload不丟失)或內存級Proxy Cache,從而減小帶寬;

另外咱們還使用使用一致性哈希(如商品編號/分類)作負載均衡內部對URL重寫提高命中率;

咱們對mget作了優化,如去商品其餘維度數據,分類、麪包屑、商家等差很少8個維度數據,若是每次mget獲取性能差並且數據量很大,30KB以上;而這些數據緩存半小時也是沒有問題的,所以咱們設計爲先讀local cache,而後把不命中的再回源到remote cache獲取,這個優化減小了一半以上的remote cache流量;

服務端分佈式緩存,咱們使用內存+SSD+JIMDB持久化存儲。

 

詳情頁架構設計原則 / 動態化

數據獲取動態化,商品詳情頁:按維度獲取數據,商品基本數據、其餘數據(分類、商家信息等);並且能夠根據數據屬性,按需作邏輯,好比虛擬商品須要本身定製的詳情頁,那麼咱們就能夠跳轉走,好比全球購的須要走jd.hk域名,那麼也是沒有問題的;

模板渲染實時化,支持隨時變動模板需求;

重啓應用秒級化,使用Nginx+Lua架構,重啓速度快,重啓不丟共享字典緩存數據;

需求上線速度化,由於咱們使用了Nginx+Lua架構,能夠快速上線和重啓應用,不會產生抖動;另外Lua自己是一種腳本語言,咱們也在嘗試把代碼如何版本化存儲,直接內部驅動Lua代碼更新上線而不須要重啓Nginx。

 

彈性化

咱們全部應用業務都接入了Docker容器,存儲仍是物理機;咱們會製做一些基礎鏡像,把須要的軟件打成鏡像,這樣不用每次去運維那安裝部署軟件了;將來能夠支持自動擴容,好比按照CPU或帶寬自動擴容機器,目前京東一些業務支持一分鐘自動擴容。

降級開關

推送服務器推送降級開關,開關集中化維護,而後經過推送機制推送到各個服務器;

可降級的多級讀服務,前端數據集羣—>數據異構集羣—>動態服務(調用依賴系統);這樣能夠保證服務質量,假設前端數據集羣壞了一個磁盤,還能夠回源到數據異構集羣獲取數據;

開關前置化,如Nginx–àTomcat,在Nginx上作開關,請求就到不了後端,減小後端壓力;

可降級的業務線程池隔離,從Servlet3開始支持異步模型,Tomcat7/Jetty8開始支持,相同的概念是Jetty6的Continuations。咱們能夠把處理過程分解爲一個個的事件。經過這種將請求劃分爲事件方式咱們能夠進行更多的控制。如,咱們能夠爲不一樣的業務再創建不一樣的線程池進行控制:即咱們只依賴tomcat線程池進行請求的解析,對於請求的處理咱們交給咱們本身的線程池去完成;這樣tomcat線程池就不是咱們的瓶頸,形成如今沒法優化的情況。經過使用這種異步化事件模型,咱們能夠提升總體的吞吐量,不讓慢速的A業務處理影響到其餘業務處理。慢的仍是慢,可是不影響其餘的業務。咱們經過這種機制還能夠把tomcat線程池的監控拿出來,出問題時能夠直接清空業務線程池,另外還能夠自定義任務隊列來支持一些特殊的業務。

多機房多活

應用無狀態,經過在配置文件中配置各自機房的數據集羣來完成數據讀取。

數據集羣採用一主三從結構,防止當一個機房掛了,另外一個機房壓力大產生抖動。


多種壓測方案

線下壓測,Apache ab,Apache Jmeter,這種方式是固定url壓測,通常經過訪問日誌收集一些url進行壓測,能夠簡單壓測單機峯值吞吐量,可是不能做爲最終的壓測結果,由於這種壓測會存在熱點問題;

線上壓測,可使用Tcpcopy直接把線上流量導入到壓測服務器,這種方式能夠壓測出機器的性能,並且能夠把流量放大,也可使用Nginx+Lua協程機制把流量分發到多臺壓測服務器,或者直接在頁面埋點,讓用戶壓測,此種壓測方式能夠不給用戶返回內容。

 

遇到的一些坑和問題

SSD性能差

使用SSD作KV存儲時發現磁盤IO很是低。配置成RAID10的性能只有3~6MB/s;配置成RAID0的性能有~130MB/s,系統中沒有發現CPU,MEM,中斷等瓶頸。一臺服務器從RAID1改爲RAID0後,性能只有~60MB/s。這說明咱們用的SSD盤性能不穩定。

根據以上現象,初步懷疑如下幾點:SSD盤,線上系統用的三星840Pro是消費級硬盤。RAID卡設置,Write back和Write through策略。後來測試驗證,有影響,但不是關鍵。RAID卡類型,線上系統用的是LSI 2008,比較陳舊。


本實驗使用dd順序寫操做簡單測試,嚴格測試須要用FIO等工具。

 

鍵值存儲選型壓測

咱們對於存儲選型時嘗試過LevelDB、RocksDB、BeansDB、LMDB、Riak等,最終根據咱們的需求選擇了LMDB。

機器:2臺

配置:32核CPU、32GB內存、SSD((512GB)三星840Pro–> (600GB)Intel 3500 /Intel S3610)

數據:1.7億數據(800多G數據)、大小5~30KB左右

KV存儲引擎:LevelDB、RocksDB、LMDB,每臺啓動2個實例

壓測工具:tcpcopy直接線上導流

壓測用例:隨機寫+隨機讀

 

LevelDB壓測時,隨機讀+隨機寫會產生抖動(咱們的數據出自本身的監控平臺,分鐘級採樣)。


RocksDB是改造自LevelDB,對SSD作了優化,咱們壓測時單獨寫或讀,性能很是好,可是讀寫混合時就會由於歸併產生抖動。


LMDB引擎沒有大的抖動,基本知足咱們的需求。

 

咱們目前一些線上服務器使用的是LMDB,其餘一些正在嘗試公司自主研發的CycleDB引擎。

 

數據量大時JIMDB同步不動

Jimdb數據同步時要dump數據,SSD盤容量用了50%以上,dump到同一塊磁盤容量不足。解決方案:

一、一臺物理機掛2塊SSD(512GB),單掛raid0;啓動8個jimdb實例;這樣每實例差很少125GB左右;目前是掛4塊,raid0;新機房計劃8塊raid10;

二、目前是千兆網卡同步,同步峯值在100MB/s左右;

三、dump和sync數據時是順序讀寫,所以掛一塊SAS盤專門來同步數據;

四、使用文件鎖保證一臺物理機多個實例同時只有一個dump;

五、後續計劃改造爲直接內存轉發而不作dump。

切換主從

以前存儲架構是一主二從(主機房一主一從,備機房一從)切換到備機房時,只有一個主服務,讀寫壓力大時有抖動,所以咱們改造爲以前架構圖中的一主三從。

分片配置

以前的架構是分片邏輯分散到多個子系統的配置文件中,切換時須要操做不少系統;解決方案:

一、引入Twemproxy中間件,咱們使用本地部署的Twemproxy來維護分片邏輯;

二、使用自動部署系統推送配置和重啓應用,重啓以前暫停mq消費保證數據一致性;

三、用unix domain socket減小鏈接數和端口占用不釋放啓動不了服務的問題。

 

模板元數據存儲HTML

起初不肯定Lua作邏輯和渲染模板性能如何,就儘可能減小for、if/else之類的邏輯;經過java worker組裝html片斷存儲到jimdb,html片斷會存儲諸多問題,假設將來變了也是須要全量刷出的,所以存儲的內容最好就是元數據。所以經過線上不斷壓測,最終jimdb只存儲元數據,lua作邏輯和渲染;邏輯代碼在3000行以上;模板代碼1500行以上,其中大量for、if/else,目前渲染性能能夠接受。

 

線上真實流量,總體性能從TP99 53ms降到32ms。

綁定8 CPU測試的,渲染模板的性能能夠接受。


 

庫存接口訪問量600w/分鐘

商品詳情頁庫存接口2014年被惡意刷,每分鐘超過600w訪問量,tomcat機器只能定時重啓;由於是詳情頁展現的數據,緩存幾秒鐘是能夠接受的,所以開啓nginx proxy cache來解決該問題,開啓後降到正常水平;咱們目前正在使用Nginx+Lua架構改造服務,數據過濾、URL重寫等在Nginx層完成,經過URL重寫+一致性哈希負載均衡,不怕隨機URL,一些服務提高了10%+的緩存命中率。

 

微信接口調用量暴增

經過訪問日誌發現某IP頻繁抓取;並且按照商品編號遍歷,可是會有一些不存在的編號;解決方案:

一、讀取KV存儲的部分不限流;

二、回源到服務接口的進行請求限流,保證服務質量。

開啓Nginx Proxy Cache性能不升反降

開啓Nginx Proxy Cache後,性能降低,並且過一段內存使用率到達98%;解決方案:

一、對於內存佔用率高的問題是內核問題,內核使用LRU機制,自己不是問題,不過能夠經過修改內核參數

sysctl -w vm.extra_free_kbytes=6436787

sysctl -w vm.vfs_cache_pressure=10000

二、使用Proxy Cache在機械盤上性能差能夠經過tmpfs緩存或nginx共享字典緩存元數據,或者使用SSD,咱們目前使用內存文件系統。

配送至讀服務因依賴太多,響應時間偏慢

配送至服務天天有數十億調用量,響應時間偏慢。解決方案:

一、串行獲取變併發獲取,這樣一些服務能夠併發調用,在咱們某個系統中能提高一倍多的性能,從原來TP99差很少1s降到500ms如下;

二、預取依賴數據回傳,這種機制還一個好處,好比咱們依賴三個下游服務,而這三個服務都須要商品數據,那麼咱們能夠在當前服務中取數據,而後回傳給他們,這樣能夠減小下游系統的商品服務調用量,若是沒有傳,那麼下游服務再本身查一下。

 

假設一個讀服務是須要以下數據:

一、數據A  10ms

二、數據B  15ms

三、數據C   20ms

四、數據D   5ms

五、數據E   10ms

那麼若是串行獲取那麼須要:60ms;

而若是數據C依賴數據A和數據B、數據D誰也不依賴、數據E依賴數據C;那麼咱們能夠這樣子來獲取數據:


那麼若是併發化獲取那麼須要:30ms;能提高一倍的性能。

 

假設數據E還依賴數據F(5ms),而數據F是在數據E服務中獲取的,此時就能夠考慮在此服務中在取數據A/B/D時預取數據F,那麼總體性能就變爲了:25ms。

 

經過這種優化咱們服務提高了差很少10ms性能。


以下服務是在抖動時的性能,老服務TP99 211ms,新服務118ms,此處咱們主要就是併發調用+超時時間限制,超時直接降級。


 

網絡抖動時,返回502錯誤

Twemproxy配置的timeout時間太長,以前設置爲5s,並且沒有分別針對鏈接、讀、寫設置超時。後來咱們減小超時時間,內網設置在150ms之內,當超時時訪問動態服務。

機器流量太大

2014年雙11期間,服務器網卡流量到了400Mbps,CPU 30%左右。緣由是咱們全部壓縮都在接入層完成,所以接入層再也不傳入相關請求頭到應用,隨着流量的增大,接入層壓力過大,所以咱們把壓縮下方到各個業務應用,添加了相應的請求頭,Nginx GZIP壓縮級別在2~4吞吐量最高;應用服務器流量降了差很少5倍;目前正常狀況CPU在4%如下。


總結

數據閉環

數據維度化

拆分系統

Worker無狀態化+任務化

異步化+併發化

多級緩存化

動態化

彈性化

降級開關

多機房多活

多種壓測方案

Nginx接入層線上灰度引流

接入層轉發時只保留有用請求頭

使用不須要cookie的無狀態域名(如c.3.cn),減小入口帶寬

Nginx Proxy Cache只緩存有效數據,如託底數據不緩存

使用非阻塞鎖應對local cache失效時突發請求到後端應用(lua-resty-lock/proxy_cache_lock)

使用Twemproxy減小Redis鏈接數

使用unix domain socket套接字減小本機TCP鏈接數

設置合理的超時時間(鏈接、讀、寫)

使用長鏈接減小內部服務的鏈接數

去數據庫依賴(協調部門遷移數據庫是很痛苦的,目前內部使用機房域名而不是ip),服務化

客戶端同域鏈接限制,進行域名分區:c0.3.cn  c1.3.cn,若是將來支持HTTP/2.0的話,就再也不適用了。

原文地址:http://click.aliyun.com/m/21852/            

相關文章
相關標籤/搜索