京東技術架構(二)構建需求響應式億級商品詳情頁
該文章是根據velocity 2015技術大會的演講《京東網站單品頁618實戰》細化而來,希望對大家有用。
商品詳情頁是什麼
商品詳情頁是展示商品詳細資訊的一個頁面,承載在網站的大部分流量和訂單的入口。京東商城目前有通用版、全球購、閃購、易車、惠買車、服裝、拼購、今日抄底等許多套模板。各套模板的元資料是一樣的,只是展示方式不一樣。目前商品詳情頁個性化需求非常多,資料來源也是非常多的,而且許多基礎服務做不了的都放我們這,因此我們需要一種架構能快速響應和優雅的解決這些需求問題。因此我們重新設計了商品詳情頁的架構,主要包括三部分:商品詳情頁系統、商品詳情頁統一服務系統和商品詳情頁動態服務系統;商品詳情頁系統負責靜的部分,而統一服務負責動的部分,而動態服務負責給內網其他系統提供一些資料服務。
商品詳情頁前端結構
前端展示可以分為這麼幾個維度:商品維度(標題、圖片、屬性等)、主商品維度(商品介紹、規格引數)、分類維度、商家維度、店鋪維度等;另外還有一些實時性要求比較高的如實時價格、實時促銷、廣告詞、配送至、預售等是通過非同步載入。
京東商城還有一些特殊維度資料:比如套裝、手機合約機等,這些資料是主商品資料外掛的。
我們的效能資料
618當天PV數億,618當天伺服器端響應時間<38ms。此處我們用的是第1000次中第99次排名的時間。
單品頁流量特點
離散資料,熱點少,各種爬蟲、比價軟體抓取。
單品頁技術架構發展
架構1.0
IIS+C#+Sql Server,最原始的架構,直接呼叫商品庫獲取相應的資料,扛不住時加了一層
架構2.0
該方案使用了靜態化技術,按照商品維度生成靜態化HTML。主要思路:
1、通過MQ得到變更通知;
2、通過Java Worker呼叫多個依賴系統生成詳情頁HTML;
3、通過rsync同步到其他機器;
4、通過Nginx直接輸出靜態頁;
5、接入層負責負載均衡。
該方案的主要缺點:
1、假設只有分類、麵包屑變更了,那麼所有相關的商品都要重刷;
2、隨著商品數量的增加,rsync會成為瓶頸;
3、無法迅速響應一些頁面需求變更,大部分都是通過JavaScript動態改頁面元素。
隨著商品數量的增加這種架構的儲存容量到達了瓶頸,而且按照商品維度生成整個頁面會存在如分類維度變更就要全部刷一遍這個分類下所有資訊的問題,因此我們又改造了一版按照尾號路由到多臺機器。
主要思路:
1、容量問題通過按照商品尾號做路由分散到多臺機器,按照自營商品單獨一臺,第三方商品按照尾號分散到11臺;
2、按維度生成HTML片段(框架、商品介紹、規格引數、麵包屑、相關分類、店鋪資訊),而不是一個大HTML;
3、通過Nginx SSI合併片段輸出;
4、接入層負責負載均衡;
5、多機房部署也無法通過rsync同步,而是使用部署多套相同的架構來實現。
該方案主要缺點:
1、碎片檔案太多,導致如無法rsync;
2、機械盤做SSI合併時,高併發時效能差,此時我們還沒有嘗試使用SSD;
3、模板如果要變更,數億商品需要數天才能刷完;
4、到達容量瓶頸時,我們會刪除一部分靜態化商品,然後通過動態渲染輸出,動態渲染系統在高峰時會導致依賴系統壓力大,抗不住;
5、還是無法迅速響應一些業務需求。
我們的痛點
1、之前架構的問題存在容量問題,很快就會出現無法全量靜態化,還是需要動態渲染;不過對於全量靜態化可以通過分散式檔案系統解決該問題,這種方案沒有嘗試;
2、最主要的問題是隨著業務的發展,無法滿足迅速變化、還有一些變態的需求。
架構3.0
我們要解決的問題:
1、能迅速響瞬變的需求,和各種變態需求;
2、支援各種垂直化頁面改版;
3、頁面模組化;
4、AB測試;
5、高效能、水平擴容;
6、多機房多活、異地多活。
主要思路:
1、資料變更還是通過MQ通知;
2、資料異構Worker得到通知,然後按照一些維度進行資料儲存,儲存到資料異構JIMDB叢集(JIMDB:Redis+持久化引擎),儲存的資料都是未加工的原子化資料,如商品基本資訊、商品擴充套件屬性、商品其他一些相關資訊、商品規格引數、分類、商家資訊等;
3、資料異構Worker儲存成功後,會發送一個MQ給資料同步Worker,資料同步Worker也可以叫做資料聚合Worker,按照相應的維度聚合資料儲存到相應的JIMDB叢集;三個維度:基本資訊(基本資訊+擴充套件屬性等的一個聚合)、商品介紹(PC版、移動版)、其他資訊(分類、商家等維度,資料量小,直接Redis儲存);
4、前端展示分為兩個:商品詳情頁和商品介紹,使用Nginx+Lua技術獲取資料並渲染模板輸出。
另外我們目前架構的目標不僅僅是為商品詳情頁提供資料,只要是Key-Value獲取的而非關係的我們都可以提供服務,我們叫做動態服務系統。
該動態服務分為前端和後端,即公網還是內網,如目前該動態服務為列表頁、商品對比、微信單品頁、總代等提供相應的資料來滿足和支援其業務。
詳情頁架構設計原則
1、資料閉環
2、資料維度化
3、拆分系統
4、Worker無狀態化+任務化
5、非同步化+併發化
6、多級快取化
7、動態化
8、彈性化
9、降級開關
10、多機房多活
11、多種壓測方案
資料閉環
資料閉環即資料的自我管理,或者說是資料都在自己系統裡維護,不依賴於任何其他系統,去依賴化;這樣得到的好處就是別人抖動跟我沒關係。
資料異構,是資料閉環的第一步,將各個依賴系統的資料拿過來,按照自己的要求儲存起來;
資料原子化,資料異構的資料是原子化資料,這樣未來我們可以對這些資料再加工再處理而響應變化的需求;
資料聚合,將多個原子資料聚合為一個大JSON資料,這樣前端展示只需要一次get,當然要考慮系統架構,比如我們使用的Redis改造,Redis又是單執行緒系統,我們需要部署更多的Redis來支援更高的併發,另外儲存的值要儘可能的小;
資料儲存,我們使用JIMDB,Redis加持久化儲存引擎,可以儲存超過記憶體N倍的資料量,我們目前一些系統是Redis+LMDB引擎的儲存,目前是配合SSD進行儲存;另外我們使用Hash Tag機制把相關的資料雜湊到同一個分片,這樣mget時不需要跨分片合併。
我們目前的異構資料時鍵值結構的,用於按照商品維度查詢,還有一套異構時關係結構的用於關係查詢使用。
詳情頁架構設計原則 / 資料維度化
對於資料應該按照維度和作用進行維度化,這樣可以分離儲存,進行更有效的儲存和使用。我們資料的維度比較簡單:
1、商品基本資訊,標題、擴充套件屬性、特殊屬性、圖片、顏色尺碼、規格引數等;
2、商品介紹資訊,商品維度商家模板、商品介紹等;
3、非商品維度其他資訊,分類資訊、商家資訊、店鋪資訊、店鋪頭、品牌資訊等;
4、商品維度其他資訊(非同步載入),價格、促銷、配送至、廣告詞、推薦配件、最佳組合等。
拆分系統
將系統拆分為多個子系統雖然增加了複雜性,但是可以得到更多的好處,比如資料異構系統儲存的資料是原子化資料,這樣可以按照一些維度對外提供服務;而資料同步系統儲存的是聚合資料,可以為前端展示提供高效能的讀取。而前端展示系統分離為商品詳情頁和商品介紹,可以減少相互影響;目前商品介紹系統還提供其他的一些服務,比如全站非同步頁尾服務。
Worker無狀態化+任務化
1、資料異構和資料同步Worker無狀態化設計,這樣可以水平擴充套件;
2、應用雖然是無狀態化的,但是配置檔案還是有狀態的,每個機房一套配置,這樣每個機房只讀取當前機房資料;
3、任務多佇列化,等待佇列、排重佇列、本地執行佇列、失敗佇列;
4、佇列優先順序化,分為:普通佇列、刷資料佇列、高優先順序佇列;例如一些秒殺商品會走高優先順序佇列保證快速執行;
5、副本佇列,當上線後業務出現問題時,修正邏輯可以回放,從而修復資料;可以按照比如固定大小佇列或者小時佇列設計;
6、在設計訊息時,按照維度更新,比如商品資訊變更和商品上下架分離,減少每次變更介面的呼叫量,通過聚合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到同一塊磁碟容量不足。解決方案:
1、一臺物理機掛2塊SSD(512GB),單掛raid0;啟動8個jimdb例項;這樣每例項差不多125GB左右;目前是掛4塊,raid0;新機房計劃8塊raid10;
2、目前是千兆網絡卡同步,同步峰值在100MB/s左右;
3、dump和sync資料時是順序讀寫,因此掛一塊SAS盤專門來同步資料;
4、使用檔案鎖保證一臺物理機多個例項同時只有一個dump;
5、後續計劃改造為直接記憶體轉發而不做dump。
切換主從
之前儲存架構是一主二從(主機房一主一從,備機房一從)切換到備機房時,只有一個主服務,讀寫壓力大時有抖動,因此我們改造為之前架構圖中的一主三從。
分片配置
之前的架構是分片邏輯分散到多個子系統的配置檔案中,切換時需要操作很多系統;解決方案:
1、引入Twemproxy中介軟體,我們使用本地部署的Twemproxy來維護分片邏輯;
2、使用自動部署系統推送配置和重啟應用,重啟之前暫停mq消費保證資料一致性;
3、用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頻繁抓取;而且按照商品編號遍歷,但是會有一些不存在的編號;解決方案:
1、讀取KV儲存的部分不限流;
2、回源到服務介面的進行請求限流,保證服務質量。
開啟Nginx Proxy Cache效能不升反降
開啟Nginx Proxy Cache後,效能下降,而且過一段記憶體使用率到達98%;解決方案:
1、對於記憶體佔用率高的問題是核心問題,核心使用LRU機制,本身不是問題,不過可以通過修改核心引數
sysctl -w vm.extra_free_kbytes=6436787
sysctl -w vm.vfs_cache_pressure=10000
2、使用Proxy Cache在機械盤上效能差可以通過tmpfs快取或nginx共享字典快取元資料,或者使用SSD,我們目前使用記憶體檔案系統。
配送至讀服務因依賴太多,響應時間偏慢
配送至服務每天有數十億呼叫量,響應時間偏慢。解決方案:
1、序列獲取變併發獲取,這樣一些服務可以併發呼叫,在我們某個系統中能提升一倍多的效能,從原來TP99差不多1s降到500ms以下;
2、預取依賴資料回傳,這種機制還一個好處,比如我們依賴三個下游服務,而這三個服務都需要商品資料,那麼我們可以在當前服務中取資料,然後回傳給他們,這樣可以減少下游系統的商品服務呼叫量,如果沒有傳,那麼下游服務再自己查一下。
假設一個讀服務是需要如下資料:
1、資料A 10ms
2、資料B 15ms
3、資料C 20ms
4、資料D 5ms
5、資料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的話,就不再適用了。