原文出處: 張開濤
該文章是根據velocity 2015技術大會的演講《京東網站單品頁618實戰》細化而來,希望對大家有用。
商品詳情頁是展示商品詳細信息的一個頁面,承載在網站的大部分流量和訂單的入口。京東商城目前有通用版、全球購、閃購、易車、惠買車、服裝、拼購、今日抄底等許多套模板。各套模板的元數據是一樣的,只是展示方式不一樣。目前商品詳情頁個性化需求非常多,數據來源也是非常多的,而且許多基礎服務做不了的都放我們這,因此我們需要一種架構能快速響應和優雅的解決這些需求問題。因此我們重新設計了商品詳情頁的架構,主要包括三部分:商品詳情頁系統、商品詳情頁統一服務系統和商品詳情頁動態服務系統;商品詳情頁系統負責靜的部分,而統一服務負責動的部分,而動態服務負責給內網其他系統提供一些數據服務。
前端展示可以分為這麼幾個維度:商品維度(標題、圖片、屬性等)、主商品維度(商品介紹、規格參數)、分類維度、商家維度、店鋪維度等;另外還有一些實時性要求比較高的如實時價格、實時促銷、廣告詞、配送至、預售等是通過異步加載。
京東商城還有一些特殊維度數據:比如套裝、手機合約機等,這些數據是主商品數據外掛的。
618當天PV數億,618當天服務器端響應時間<38ms。此處我們用的是第1000次中第99次排名的時間。
離散數據,熱點少,各種爬蟲、比價軟件抓取。
單品頁技術架構發展
IIS+C#+Sql Server,最原始的架構,直接調用商品庫獲取相應的數據,扛不住時加了一層memcached來緩存數據。這種方式經常受到依賴的服務不穩定而導致的性能抖動。
架構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、最主要的問題是隨著業務的發展,無法滿足迅速變化、還有一些變態的需求。
我們要解決的問題:
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、商品維度其他信息(異步加載),價格、促銷、配送至、廣告詞、推薦配件、最佳組合等。
將系統拆分為多個子系統雖然增加了復雜性,但是可以得到更多的好處,比如數據異構系統存儲的數據是原子化數據,這樣可以按照一些維度對外提供服務;而數據同步系統存儲的是聚合數據,可以為前端展示提供高性能的讀取。而前端展示系統分離為商品詳情頁和商品介紹,可以減少相互影響;目前商品介紹系統還提供其他的一些服務,比如全站異步頁腳服務。
1、數據異構和數據同步Worker無狀態化設計,這樣可以水平擴展;
2、應用雖然是無狀態化的,但是配置文件還是有狀態的,每個機房一套配置,這樣每個機房只讀取當前機房數據;
3、任務多隊列化,等待隊列、排重隊列、本地執行隊列、失敗隊列;
4、隊列優先級化,分為:普通隊列、刷數據隊列、高優先級隊列;例如一些秒殺商品會走高優先級隊列保證快速執行;
5、副本隊列,當上線後業務出現問題時,修正邏輯可以回放,從而修復數據;可以按照比如固定大小隊列或者小時隊列設計;
6、在設計消息時,按照維度更新,比如商品信息變更和商品上下架分離,減少每次變更接口的調用量,通過聚合Worker去做聚合。
我們系統大量使用異步化,通過異步化機制提升並發能力。首先我們使用了消息異步化 進行系統解耦合,通過消息通知我變更,然後我再調用相應接口獲取相關數據;之前老系統使用同步推送機制,這種方式系統是緊耦合的,出問題需要聯系各個負責人重新推送還要考慮失敗重試機制。數據更新異步化 ,更新緩存時,同步調用服務,然後異步更新緩存。可並行任務並發化, 商品數據系統來源有多處,但是可以並發調用聚合,這樣本來串行需要1s的經過這種方式我們提升到300ms之內。異步請求合並,異步請求做合並,然後一次請求調用就能拿到所有數據。前端服務異步化/聚合,實時價格、實時庫存異步化, 使用如線程或協程機制將多個可並發的服務聚合。異步化還一個好處就是可以對異步請求做合並,原來N次調用可以合並為一次,還可以做請求的排重。
浏覽器緩存,當頁面之間來回跳轉時走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做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數據同步時要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減少連接數和端口占用不釋放啟動不了服務的問題。
起初不確定Lua做邏輯和渲染模板性能如何,就盡量減少for、if/else之類的邏輯;通過java worker組裝html片段存儲到jimdb,html片段會存儲諸多問題,假設未來變了也是需要全量刷出的,因此存儲的內容最好就是元數據。因此通過線上不斷壓測,最終jimdb只存儲元數據,lua做邏輯和渲染;邏輯代碼在3000行以上;模板代碼1500行以上,其中大量for、if/else,目前渲染性能可以接受。
線上真實流量,整體性能從TP99 53ms降到32ms。
綁定8 CPU測試的,渲染模板的性能可以接受。
商品詳情頁庫存接口2014年被惡意刷,每分鐘超過600w訪問量,tomcat機器只能定時重啟;因為是詳情頁展示的數據,緩存幾秒鐘是可以接受的,因此開啟nginx proxy cache來解決該問題,開啟後降到正常水平;我們目前正在使用Nginx+Lua架構改造服務,數據過濾、URL重寫等在Nginx層完成,通過URL重寫+一致性哈希負載均衡,不怕隨機URL,一些服務提升了10%+的緩存命中率。
通過訪問日志發現某IP頻繁抓取;而且按照商品編號遍歷,但是會有一些不存在的編號;解決方案:
1、讀取KV存儲的部分不限流;
2、回源到服務接口的進行請求限流,保證服務質量。
開啟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,此處我們主要就是並發調用+超時時間限制,超時直接降級。
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的話,就不再適用了。
QQ技術交流群290551701 http://cxy.liuzhihengseo.com/550.html