億級商品詳情頁架構(gòu)演進技術(shù)解密 | 高可用架構(gòu)系列

此文是開濤在【三體高可用架構(gòu)群】之分享內(nèi)容,“三體”是為了紀(jì)念三體一書對技術(shù)人的偉大影響而冠名。

張開濤:2014年加入京東,主要負責(zé)商品詳情頁、詳情頁統(tǒng)一服務(wù)架構(gòu)與開發(fā)工作,設(shè)計并開發(fā)了多個億級訪問量系統(tǒng)。工作之余喜歡寫技術(shù)博客,有《跟我學(xué)Spring》、《跟我學(xué)Spring

MVC》、《跟我學(xué)Shiro》、《跟我學(xué)Nginx+Lua開發(fā)》等系列教程,博客

http://jinnianshilongnian.iteye.com/? 的訪問量超過500W。

京東618的硝煙雖已散去,可開發(fā)和備戰(zhàn)618期間總結(jié)過的一些設(shè)計原則和遇到的一些坑還歷歷在目。伴隨著網(wǎng)站業(yè)務(wù)發(fā)展,需求日趨復(fù)雜多樣并隨時變化;傳統(tǒng)靜態(tài)化方案會遇到業(yè)務(wù)瓶頸,不能滿足瞬變的需求。因此,需要一種能高性能實時渲染的動態(tài)化模板技術(shù)來解決這些問題。

今夜,我們將進行服裝品類的垂直詳情頁的AB測試和切新庫存服務(wù)的1/n流量。就此機會,和大家分享一下最近一年做的京東商品詳情頁的架構(gòu)升級的心路歷程。

商品詳情頁是什么

商品詳情頁是展示商品詳細信息的一個頁面,承載在網(wǎng)站的大部分流量和訂單的入口。京東商城目前有通用版、全球購、閃購、易車、惠買車、服裝、拼購、今日抄底等許多套詳情頁模板,通過一些特殊屬性、商家類型和打標(biāo)來區(qū)分,每套模板數(shù)據(jù)是一樣的,核心邏輯基本一樣,但是一些前端邏輯是有差別的。

目前商品詳情頁個性化需求非常多,數(shù)據(jù)來源也是非常多的(目前統(tǒng)計后端有差不多數(shù)十個依賴服務(wù)),而且許多基礎(chǔ)服務(wù)做不了的不想做的或者說需要緊急處理的都放我們這處理,比如一些屏蔽商品需求等。因此我們需要一種架構(gòu)能快速響應(yīng)和優(yōu)雅的解決這些需求問題,來了問題能在5~10分鐘內(nèi)搞定。我們這邊經(jīng)還常收到一些緊急需求,比如工商的一些投訴等需要及時響應(yīng)。之前架構(gòu)是靜態(tài)化的,肯定無法滿足這種日趨復(fù)雜和未知的需求。靜態(tài)化時做屏蔽都是通過js,所以我們重新設(shè)計了商品詳情頁的架構(gòu)。

它主要包括以下三部分:

商品詳情頁系統(tǒng)

負責(zé)靜的部分(整個頁面)

商品詳情頁動態(tài)服務(wù)系統(tǒng)和商品詳情頁統(tǒng)一服務(wù)系統(tǒng)

統(tǒng)一服務(wù)系統(tǒng)負責(zé)動的部分,比如實時庫存。目前已經(jīng)上線了幾個核心服務(wù),今晚計劃切新庫存服務(wù)的1/n流量。

動態(tài)服務(wù)系統(tǒng)負責(zé)給內(nèi)網(wǎng)其他系統(tǒng)提供一些數(shù)據(jù)服務(wù)(比如大客戶系統(tǒng)需要商品數(shù)據(jù)),目前商品詳情頁系統(tǒng)已經(jīng)穩(wěn)定運行半年了,目前主要給列表頁提供一些數(shù)據(jù)。

鍵值結(jié)構(gòu)的異構(gòu)數(shù)據(jù)集群

商品主數(shù)據(jù)因為是存儲在DB中,對于一些聚合數(shù)據(jù)需要聯(lián)合查詢非常多,會導(dǎo)致查詢性能差的問題,因此對于鍵值類型的查詢,我們這套異構(gòu)數(shù)據(jù)非常有用。我們這次架構(gòu)的調(diào)整的主要目的是滿足日趨復(fù)雜的業(yè)務(wù)需求,能及時開發(fā)業(yè)務(wù)方的需求。我們的系統(tǒng)主要處理鍵值數(shù)據(jù)的邏輯,關(guān)系查詢我們有另一套異構(gòu)系統(tǒng)。

下圖是我們的模板頁,核心數(shù)據(jù)都是一樣的,只是展示方式和一些前端邏輯不太一樣。

我們詳情頁的前端展示主要分為這么幾個維度:

商品維度(標(biāo)題、圖片、屬性等)

主商品維度(商品介紹、規(guī)格參數(shù))

分類維度

商家維度

店鋪維度

另外還有一些實時性要求比較高的如實時價格、實時促銷、廣告詞、配送至、預(yù)售等是通過異步加載。

我們目前把數(shù)據(jù)按維度化存儲,比如一些維度直接redis存,性能好。

京東商城還有一些特殊維度數(shù)據(jù):比如套裝、手機合約機等,這些數(shù)據(jù)是主商品數(shù)據(jù)外掛的,通過異步加載來實現(xiàn)的邏輯。還有一些與第三方合作的,如易車,很多數(shù)據(jù)都是無法異構(gòu)的,都是直接異步加載的。目前有易車、途牛等一些公司有這種合作。

我們618當(dāng)天PV數(shù)億,服務(wù)器端TOP99響應(yīng)時間低于38ms(此處是第1000次中第99次排名的時間,PV具體數(shù)據(jù)不便公開,但TOP99基本在40ms之內(nèi))。

上圖是我們的一個監(jiān)控圖。我們詳情頁流量特點是離散數(shù)據(jù),熱點少,各種爬蟲、比價軟件抓??;所以如果直接查庫,防刷沒做好,很容易被刷掛。

商品詳情頁發(fā)展史

這是我們的一個架構(gòu)歷史。

架構(gòu)1.0

IIS+C#+Sql Server,最原始的架構(gòu),直接調(diào)用商品庫獲取相應(yīng)的數(shù)據(jù),扛不住時加了一層memcached來緩存數(shù)據(jù)。

這種方式經(jīng)常受到依賴的服務(wù)不穩(wěn)定而導(dǎo)致的性能抖動。基本發(fā)展初期都是這個樣子的,扛不住加層緩存。因此我們設(shè)計了架構(gòu)2.0。

架構(gòu)2.0

該方案使用了靜態(tài)化技術(shù),按照商品維度生成靜態(tài)化HTML,這就是一個靜態(tài)化方案。

主要思路:

通過MQ得到變更通知;

通過Java Worker調(diào)用多個依賴系統(tǒng)生成詳情頁HTML;

通過rsync同步到其他機器;

通過Nginx直接輸出靜態(tài)頁;

接入層負責(zé)負載均衡。

主要缺點:

假設(shè)只有分類、面包屑變更了,那么所有相關(guān)的商品都要重刷;

隨著商品數(shù)量的增加,rsync會成為瓶頸;

無法迅速響應(yīng)一些頁面需求變更,大部分都是通過JavaScript動態(tài)改頁面元素。

之前需求沒那么多,因此頁面變更不是很頻繁,基本沒什么問題。但是隨著商品數(shù)量的增加這種架構(gòu)的存儲容量到達了瓶頸,而且按照商品維度生成整個頁面會存在如分類維度變更就要全部刷一遍這個分類下所有信息的問題,因此我們又改造了一版按照尾號路由到多臺機器。這種生成整個頁面的方案會存在比如只有分類信息變了,也需要把這個分類下的商品重新刷一遍。

架構(gòu)2.1

主要思路:

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

按維度生成HTML片段(框架、商品介紹、規(guī)格參數(shù)、面包屑、相關(guān)分類、店鋪信息),而不是一個大HTML;

通過Nginx SSI合并片段輸出;

接入層負責(zé)負載均衡;

多機房部署也無法通過rsync同步,而是使用部署多套相同的架構(gòu)來實現(xiàn)。

這種方式通過尾號路由的方式分散到多臺機器擴容,然后生成HTML片段,按需靜態(tài)化;當(dāng)時我們做閃購的時候,需要加頁頭,都是通過js搞定的。但對于大的頁面結(jié)構(gòu)變更,需要全量生成。尤其像面包屑不一樣的話會很麻煩,需要生成多個版本。

主要缺點:

碎片文件太多,導(dǎo)致如無法rsync;

機械盤做SSI合并時,高并發(fā)時性能差,此時我們還沒有嘗試使用SSD;

模板如果要變更,數(shù)億商品需要數(shù)天才能刷完;

到達容量瓶頸時,我們會刪除一部分靜態(tài)化商品,然后通過動態(tài)渲染輸出,動態(tài)渲染系統(tǒng)在高峰時會導(dǎo)致依賴系統(tǒng)壓力大,抗不?。?/p>

還是無法迅速響應(yīng)一些業(yè)務(wù)需求。

當(dāng)時我記得印象最深的就是碎片文件太多,我們的inode不夠了,經(jīng)常要半夜去公司刪文件。因為存在刪除問題,每臺服務(wù)器并不是全量,所以我們需要一個動態(tài)生成的服務(wù),當(dāng)靜態(tài)化不存在的時候還原到動態(tài)服務(wù);但這樣雙十一時壓力非常大,我們依賴的系統(tǒng)隨時都給我們降級。

架構(gòu)3.0

我們的痛點:

之前架構(gòu)的問題存在容量問題,很快就會出現(xiàn)無法全量靜態(tài)化,還是需要動態(tài)渲染;(對于全量靜態(tài)化可以通過分布式文件系統(tǒng)解決該問題,這種方案沒有嘗試)

最主要的問題是隨著業(yè)務(wù)的發(fā)展,無法滿足迅速變化、還有一些變態(tài)的需求。

其實最痛快的是業(yè)務(wù)來說我們要搞垂直,我們要模塊化,我們要個性化;這些統(tǒng)統(tǒng)不好搞,因此我們就考慮做一版全動態(tài)的。其實思路和靜態(tài)化差不多, 數(shù)據(jù)靜態(tài)化聚合、頁面模板化。

我們要考慮和要解決的問題:

能迅速響瞬變的需求,各種變態(tài)需求;

支持各種垂直化頁面改版;

頁面模塊化;

AB測試;

高性能、水平擴容;

多機房多活、異地多活。

這是我們新的系統(tǒng):三個子系統(tǒng)。

主要思路:

數(shù)據(jù)變更還是通過MQ通知;

數(shù)據(jù)異構(gòu)Worker得到通知,然后按照一些維度進行數(shù)據(jù)存儲,存儲到數(shù)據(jù)異構(gòu)JIMDB集群(JIMDB:Redis+持久化引擎,是基于Redis改造的一個加了持久化引擎的KV存儲),存儲的數(shù)據(jù)都是未加工的原子化數(shù)據(jù),如商品基本信息、商品擴展屬性、商品其他一些相關(guān)信息、商品規(guī)格參數(shù)、分類、商家信息等;

數(shù)據(jù)異構(gòu)Worker存儲成功后,會發(fā)送一個MQ給數(shù)據(jù)同步

Worker,數(shù)據(jù)同步Worker也可以叫做數(shù)據(jù)聚合Worker,按照相應(yīng)的維度聚合數(shù)據(jù)存儲到相應(yīng)的JIMDB集群;三個維度:基本信息(基本信息+擴展屬性等的一個聚合)、商品介紹(PC版、移動版)、其他信息(分類、商家等維度,數(shù)據(jù)量小,直接Redis存儲);

前端展示分為兩個:商品詳情頁和商品介紹,使用Nginx+Lua技術(shù)獲取數(shù)據(jù)并渲染模板輸出。

思路差不多: MQ得到變更通知,Worker刷元數(shù)據(jù)到JIMDB,前端展示系統(tǒng)取數(shù)據(jù)渲染模板。另外我們當(dāng)時架構(gòu)的目標(biāo)是詳情頁上有的數(shù)據(jù),我們都可以提供服務(wù)出去,主要提供單個商品的查詢服務(wù),所以我們把這個系統(tǒng)叫做動態(tài)服務(wù)系統(tǒng)。

該動態(tài)服務(wù)分為前端和后端,即公網(wǎng)還是內(nèi)網(wǎng),如目前該動態(tài)服務(wù)為列表頁、商品對比、微信單品頁、總代等提供相應(yīng)的數(shù)據(jù)來滿足和支持其業(yè)務(wù)。

目前每天為列表頁提供增量數(shù)據(jù)服務(wù)。微信上京東入口看到的詳情頁 也是我們這個服務(wù)提供的數(shù)據(jù)。APP的數(shù)據(jù)暫時沒走我們的系統(tǒng),不過我們目前系統(tǒng)實現(xiàn)的是平常流量的50倍左右,性能和流量基本不是問題。我們詳情頁架構(gòu)設(shè)計的一些原則:

數(shù)據(jù)閉環(huán)

數(shù)據(jù)維度化

拆分系統(tǒng)

Worker無狀態(tài)化+任務(wù)化

異步化+并發(fā)化

多級緩存化

動態(tài)化

彈性化

降級開關(guān)

多機房多活

多種壓測方案

因為我們這邊主要是讀服務(wù),因此我們架構(gòu)可能偏讀為主的設(shè)計;目前我設(shè)計的幾個系統(tǒng)都遵循這些原則去設(shè)計:

數(shù)據(jù)閉環(huán)

數(shù)據(jù)閉環(huán),即數(shù)據(jù)的自我管理,或者說是數(shù)據(jù)都在自己系統(tǒng)里維護,不依賴于任何其他系統(tǒng),去依賴化,這樣得到的好處就是別人抖動跟我沒關(guān)系。因此我們要先數(shù)據(jù)異構(gòu)。

數(shù)據(jù)異構(gòu),是數(shù)據(jù)閉環(huán)的第一步,將各個依賴系統(tǒng)的數(shù)據(jù)拿過來,按照自己的要求存儲起來;我們把很多數(shù)據(jù)劃分為三個主要維度進行異構(gòu):商品信息、商品介紹和其他信息(分類、商家、店鋪等)。

數(shù)據(jù)原子化處理,數(shù)據(jù)異構(gòu)的數(shù)據(jù)是原子化數(shù)據(jù),這樣未來我們可以對這些數(shù)據(jù)再加工再處理而響應(yīng)變化的需求。我們有了一份原子化異構(gòu)數(shù)據(jù)雖然方便處理新需求,但恰恰因為第一份數(shù)據(jù)是原子化的,那么它會很分散,前端讀取時mget的話 性能不是很好,因此我們又做了數(shù)據(jù)聚合。

數(shù)據(jù)聚合,是將多個原子數(shù)據(jù)聚合為一個大JSON數(shù)據(jù),這樣前端展示只需要一次get,當(dāng)然要考慮系統(tǒng)架構(gòu),比如我們使用的Redis改造,Redis又是單線程系統(tǒng),我們需要部署更多的Redis來支持更高的并發(fā),另外存儲的值要盡可能的小。

數(shù)據(jù)存儲,我們使用JIMDB,Redis加持久化存儲引擎,可以存儲超過內(nèi)存N倍的數(shù)據(jù)量,我們目前一些系統(tǒng)是Redis+LMDB引擎的存儲,目前是配合SSD進行存儲;另外我們使用Hash Tag機制把相關(guān)的數(shù)據(jù)哈希到同一個分片,這樣mget時不需要跨分片合并。分片邏輯使用的是Twemproxy,和應(yīng)用端混合部署在一起;減少了一層中間層,也節(jié)約一部分機器。

我們目前的異構(gòu)數(shù)據(jù)是鍵值結(jié)構(gòu)的,用于按照商品維度查詢,還有一套異構(gòu)時關(guān)系結(jié)構(gòu)的用于關(guān)系查詢使用。

數(shù)據(jù)維度化

對于數(shù)據(jù)應(yīng)該按照維度和作用進行維度化,這樣可以分離存儲,進行更有效的存儲和使用。我們數(shù)據(jù)的維度比較簡單:

商品基本信息,標(biāo)題、擴展屬性、特殊屬性、圖片、顏色尺碼、規(guī)格參數(shù)等;

這些信息都是商品維度的。

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

京東的商品比較特殊:自營和第三方。

自營的商品可以任意組合,選擇其中一個作為主商品,因此他的商品介紹是商品維度。

第三方的組合是固定的,有一個固定的主商品,商品介紹是主商品維度。

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

這些數(shù)據(jù)量不是很大,一個redis實例就能存儲。

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

這些數(shù)據(jù)很多部門在維護,只能異步加載;目前這些服務(wù)比較穩(wěn)定,性能也不錯,我們在把這些服務(wù)在服務(wù)端聚合,然后一次性吐出去?,F(xiàn)在已經(jīng)這么做了幾個,比如下面這個就是在服務(wù)端聚合吐出去的情況。

http://c.3.cn/recommend?callback=jQuery4132621&methods=accessories%2Csuit&p=103003&sku=1217499&cat=9987%2C653%2C655&lid=1&uuid=1156941855&pin=zhangkaitao1987&ck=pin%2CipLocation%2Catw%2Caview&lim=6&cuuid=1156941855&csid=122270672.4.1156941855%7C91.1440679162&c1=9987&c2=653&c3=655&_=1440679196326

這是我們url的一些規(guī)則,methods指定聚合的服務(wù)。我們還對系統(tǒng)按照其作用做了拆分。

拆分系統(tǒng)

將系統(tǒng)拆分為多個子系統(tǒng)雖然增加了復(fù)雜性,但是可以得到更多的好處。比如,數(shù)據(jù)異構(gòu)系統(tǒng)存儲的數(shù)據(jù)是原子化數(shù)據(jù),這樣可以按照一些維度對外提供服務(wù);而數(shù)據(jù)同步系統(tǒng)存儲的是聚合數(shù)據(jù),可以為前端展示提供高性能的讀取。而前端展示系統(tǒng)分離為商品詳情頁和商品介紹,可以減少相互影響;目前商品介紹系統(tǒng)還提供其他的一些服務(wù),比如全站異步頁腳服務(wù)。我們后端還是一個任務(wù)系統(tǒng)。

Worker無狀態(tài)化+任務(wù)化

數(shù)據(jù)異構(gòu)和數(shù)據(jù)同步Worker無狀態(tài)化設(shè)計,這樣可以水平擴展;

應(yīng)用雖然是無狀態(tài)化的,但是配置文件還是有狀態(tài)的,每個機房一套配置,這樣每個機房只讀取當(dāng)前機房數(shù)據(jù);

任務(wù)多隊列化,等待隊列、排重隊列、本地執(zhí)行隊列、失敗隊列;

隊列優(yōu)先級化,分為:普通隊列、刷數(shù)據(jù)隊列、高優(yōu)先級隊列;

例如,一些秒殺商品會走高優(yōu)先級隊列保證快速執(zhí)行。

副本隊列,當(dāng)上線后業(yè)務(wù)出現(xiàn)問題時,修正邏輯可以回放,從而修復(fù)數(shù)據(jù);可以按照比如固定大小隊列或者小時隊列設(shè)計;

在設(shè)計消息時,按照維度更新,比如商品信息變更和商品上下架分離,減少每次變更接口的調(diào)用量,通過聚合Worker去做聚合。

異步化+并發(fā)化

我們系統(tǒng)大量使用異步化,通過異步化機制提升并發(fā)能力。首先我們使用了消息異步化進行系統(tǒng)解耦合,通過消息通知我變更,然后我再調(diào)用相應(yīng)接口獲取相關(guān)數(shù)據(jù);之前老系統(tǒng)使用同步推送機制,這種方式系統(tǒng)是緊耦合的,出問題需要聯(lián)系各個負責(zé)人重新推送還要考慮失敗重試機制。數(shù)據(jù)更新異步化,更新緩存時,同步調(diào)用服務(wù),然后異步更新緩存。

可并行任務(wù)并發(fā)化,商品數(shù)據(jù)系統(tǒng)來源有多處,但是可以并發(fā)調(diào)用聚合,這樣本來串行需要1s的經(jīng)過這種方式我們提升到300ms之內(nèi)。異步請求合并,異步請求做合并,然后一次請求調(diào)用就能拿到所有數(shù)據(jù)。前端服務(wù)異步化/聚合,實時價格、實時庫存異步化,使用如線程或協(xié)程機制將多個可并發(fā)的服務(wù)聚合。異步化還一個好處就是可以對異步請求做合并,原來N次調(diào)用可以合并為一次,還可以做請求的排重。

多級緩存化

因之前的消息粒度較粗,我們目前在按照一些維度拆分消息,因此讀服務(wù)肯定需要大量緩存設(shè)計,所以我們是一個多級緩存的系統(tǒng)。

瀏覽器緩存,當(dāng)頁面之間來回跳轉(zhuǎn)時走local cache,或者打開頁面時拿著Last-Modified去CDN驗證是否過期,減少來回傳輸?shù)臄?shù)據(jù)量;

CDN緩存,用戶去離自己最近的CDN節(jié)點拿數(shù)據(jù),而不是都回源到北京機房獲取數(shù)據(jù),提升訪問性能;

服務(wù)端應(yīng)用本地緩存,我們使用Nginx+Lua架構(gòu),使用HttpLuaModule模塊的shared dict做本地緩存( reload不丟失)或內(nèi)存級Proxy Cache,從而減少帶寬。

我們的應(yīng)用就是通過Nginx+Lua寫的,每次重啟共享緩存不丟,這點我們受益頗多,重啟沒有抖動,另外我們還使用使用一致性哈希(如商品編號/分類)做負載均衡內(nèi)部對URL重寫提升命中率;我們對mget做了優(yōu)化,如去商品其他維度數(shù)據(jù),分類、面包屑、商家等差不多8個維度數(shù)據(jù),如果每次mget獲取性能差而且數(shù)據(jù)量很大,30KB以上;而這些數(shù)據(jù)緩存半小時也是沒有問題的,因此我們設(shè)計為先讀local

cache,然后把不命中的再回源到remote cache獲取,這個優(yōu)化減少了一半以上的remote

cache流量;這個優(yōu)化減少了這個數(shù)據(jù)獲取的一半流量;

服務(wù)端分布式緩存,我們使用內(nèi)存+SSD+JIMDB持久化存儲。

動態(tài)化

我們整個頁面是動態(tài)化渲染,輸出的數(shù)據(jù)獲取動態(tài)化,商品詳情頁:按維度獲取數(shù)據(jù),商品基本數(shù)據(jù)、其他數(shù)據(jù)(分類、商家信息等);而且可以根據(jù)數(shù)據(jù)屬性,按需做邏輯,比如虛擬商品需要自己定制的詳情頁,那么我們就可以跳轉(zhuǎn)走,比如全球購的需要走jd.hk域名,那么也是沒有問題的;未來比如醫(yī)藥的也要走單獨域名。

模板渲染實時化,支持隨時變更模板需求;我們目前模板變更非常頻繁,需求非常多,一個頁面8個開發(fā)。

重啟應(yīng)用秒級化,使用Nginx+Lua架構(gòu),重啟速度快,重啟不丟共享字典緩存數(shù)據(jù);其實我們有一些是Tomcat應(yīng)用,我們也在考慮使用如Tomcat+Local Redis 或 Tomcat+Nginx Local Shared Dict 做一些本地緩存,防止重啟堆緩存失效的問題。

需求上線速度化,因為我們使用了Nginx+Lua架構(gòu),可以快速上線和重啟應(yīng)用,不會產(chǎn)生抖動;另外Lua本身是一種腳本語言,我們也在嘗試把代碼如何版本化存儲,直接內(nèi)部驅(qū)動Lua代碼更新上線而不需要重啟Nginx。

彈性化

我們所有應(yīng)用業(yè)務(wù)都接入了Docker容器,存儲還是物理機;我們會制作一些基礎(chǔ)鏡像,把需要的軟件打成鏡像,這樣不用每次去運維那安裝部署軟件了;未來可以支持自動擴容,比如按照CPU或帶寬自動擴容機器,目前京東一些業(yè)務(wù)支持一分鐘自動擴容,下個月會進行彈性調(diào)度嘗試。

降級開關(guān)

一個前端提供服務(wù)的系統(tǒng)必須考慮降級,推送服務(wù)器推送降級開關(guān),開關(guān)集中化維護,然后通過推送機制推送到各個服務(wù)器;

可降級的多級讀服務(wù),前端數(shù)據(jù)集群—->數(shù)據(jù)異構(gòu)集群—->動態(tài)服務(wù)(調(diào)用依賴系統(tǒng));這樣可以保證服務(wù)質(zhì)量,假設(shè)前端數(shù)據(jù)集群壞了一個磁盤,還可以回源到數(shù)據(jù)異構(gòu)集群獲取數(shù)據(jù);基本不怕磁盤壞或一些機器故障、或者機架故障。

開關(guān)前置化,如Nginx代替Tomcat,在Nginx上做開關(guān),請求就到不了后端,減少后端壓力;我們目前很多開關(guān)都是在Nginx上。

可降級的業(yè)務(wù)線程池隔離,從Servlet3開始支持異步模型,Tomcat7/Jetty8開始支持,相同的概念是Jetty6的Continuations。我們可以把處理過程分解為一個個的事件。

通過這種將請求劃分為事件方式我們可以進行更多的控制。如,我們可以為不同的業(yè)務(wù)再建立不同的線程池進行控制:即我們只依賴tomcat線程池進行請求的解析,對于請求的處理我們交給我們自己的線程池去完成;這樣tomcat線程池就不是我們的瓶頸,造成現(xiàn)在無法優(yōu)化的狀況。通過使用這種異步化事件模型,我們可以提高整體的吞吐量,不讓慢速的A業(yè)務(wù)處理影響到其他業(yè)務(wù)處理。慢的還是慢,但是不影響其他的業(yè)務(wù)。我們通過這種機制還可以把tomcat線程池的監(jiān)控拿出來,出問題時可以直接清空業(yè)務(wù)線程池,另外還可以自定義任務(wù)隊列來支持一些特殊的業(yè)務(wù)。

去年使用的是JDK7+Tomcat7 最近一個月我們升級到了JDK8+Tomcat8+G1。

多機房多活

對于我們這種核心系統(tǒng),我們需要考慮多機房多活的問題。目前是應(yīng)用無狀態(tài),通過在配置文件中配置各自機房的數(shù)據(jù)集群來完成數(shù)據(jù)讀取。

其實我們系統(tǒng)只要存儲多機房就多活了,因為系統(tǒng)天然就是。數(shù)據(jù)集群采用一主三從結(jié)構(gòu),防止當(dāng)一個機房掛了,另一個機房壓力大產(chǎn)生抖動。

各個機房都是讀本機房的從另外每個機房都是倆份數(shù)據(jù),不怕因為機房突然中斷恢復(fù)后的影響。

多種壓測方案

我們在驗證系統(tǒng)時需要進行壓測。

線下壓測,Apache ab,Apache Jmeter,這種方式是固定url壓測,一般通過訪問日志收集一些url進行壓測,可以簡單壓測單機峰值吞吐量,但是不能作為最終的壓測結(jié)果,因為這種壓測會存在熱點問題;

線上壓測,可以使用Tcpcopy直接把線上流量導(dǎo)入到壓測服務(wù)器,這種方式可以壓測出機器的性能,而且可以把流量放大,也可以使用Nginx+Lua協(xié)程機制把流量分發(fā)到多臺壓測服務(wù)器,或者直接在頁面埋點,讓用戶壓測,此種壓測方式可以不給用戶返回內(nèi)容。服務(wù)剛開始的時候大量使用tcpcopy做驗證,對于一些新服務(wù),如果無法使用tcpcopy我們就在頁面埋url讓用戶來壓。

另外壓測時,要考慮讀、寫、讀或?qū)懲瑫r壓。只壓某一種場景可能都會不真實。

遇到的一些問題和解決方案

SSD性能差

使用SSD做KV存儲時發(fā)現(xiàn)磁盤IO非常低。配置成RAID10的性能只有36MB/s;配置成RAID0的性能有130MB/s,系統(tǒng)中沒有發(fā)現(xiàn)CPU,MEM,中斷等瓶頸。一臺服務(wù)器從RAID1改成RAID0后,性能只有~60MB/s。這說明我們用的SSD盤性能不穩(wěn)定。

據(jù)以上現(xiàn)象,初步懷疑以下幾點:SSD盤,線上系統(tǒng)用的三星840Pro是消費級硬盤;RAID卡設(shè)置,Write back和Write through策略(后來測試驗證,有影響,但不是關(guān)鍵);RAID卡類型,線上系統(tǒng)用的是LSI 2008,比較陳舊。

下面是使用dd做的簡單測試。

我們現(xiàn)實竟然使用的是民用級盤, 一個月壞幾塊很正常。后來我們?nèi)可暾垞Q成了INTEL企業(yè)級盤,線上用3500型號。

鍵值存儲選型壓測

在系統(tǒng)設(shè)計初期最頭痛的就是存儲選型,我們對于存儲選型時嘗試過LevelDB、RocksDB、BeansDB、LMDB、Riak等,最終根據(jù)我們的需求選擇了LMDB。

機器:2臺

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

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

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

壓測工具:tcpcopy直接線上導(dǎo)流

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

LevelDB壓測時,隨機讀+隨機寫會產(chǎn)生抖動(我們的數(shù)據(jù)出自自己的監(jiān)控平臺,分鐘級采樣)。

我們線上一些順序?qū)懙姆?wù)在使用leveldb,RocksDB是改造自LevelDB,對SSD做了優(yōu)化,我們壓測時單獨寫或讀,性能非常好,但是讀寫混合時就會因為歸并產(chǎn)生抖動。

在歸并時基本達到了我們磁盤的瓶頸,LMDB引擎沒有大的抖動,基本滿足我們的需求。

我們目前一些線上服務(wù)器使用的是LMDB,新機房正在嘗試公司自主研發(fā)的CycleDB引擎。目前我看到的應(yīng)該很少使用LMDB引擎的。

數(shù)據(jù)量大時Jimdb同步不動

Jimdb數(shù)據(jù)同步時要dump數(shù)據(jù),SSD盤容量用了50%以上,dump到同一塊磁盤容量不足。

解決方案:

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

目前是千兆網(wǎng)卡同步,同步峰值在100MB/s左右;

dump和sync數(shù)據(jù)時是順序讀寫,因此掛一塊SAS盤專門來同步數(shù)據(jù);

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

后續(xù)計劃改造為直接內(nèi)存轉(zhuǎn)發(fā)而不做dump。

切換主從

因為是基于Redis的,目前是先做數(shù)據(jù)RDB dump然后同步。后續(xù)計劃改造為直接內(nèi)存復(fù)制,之前存儲架構(gòu)是一主二從(主機房一主一從,備機房一從)切換到備機房時,只有一個主服務(wù),讀寫壓力大時有抖動,因此我們改造為之前架構(gòu)圖中的一主三從。

分片配置

之前的架構(gòu)是存儲集群的分片邏輯分散到多個子系統(tǒng)的配置文件中,切換時需要操作很多系統(tǒng)。

解決方案:

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

使用自動部署系統(tǒng)推送配置和重啟應(yīng)用,重啟之前暫停mq消費保證數(shù)據(jù)一致性;

用unix domain socket減少連接數(shù)和端口占用不釋放啟動不了服務(wù)的問題。

我們都是在應(yīng)用本地部署的Twemproxy,然后通過中間系統(tǒng)對外提供數(shù)據(jù)。

模板元數(shù)據(jù)存儲HTML

我們前端應(yīng)用使用的是Nginx+Lua,起初不確定Lua做邏輯和渲染模板性能如何,就盡量減少for、if/else之類的邏輯;通過java

worker組裝html片段存儲到j(luò)imdb,html片段會存儲諸多問題,假設(shè)未來變了也是需要全量刷出的,因此存儲的內(nèi)容最好就是元數(shù)據(jù)。

因此通過線上不斷壓測,最終jimdb只存儲元數(shù)據(jù),lua做邏輯和渲染;邏輯代碼在3000行以上;模板代碼1500行以上,其中大量for、if/else,目前渲染性可以接受。

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

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

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

商品詳情頁庫存接口2014年被惡意刷,每分鐘超過600w訪問量,tomcat機器只能定時重啟;因為是詳情頁展示的數(shù)據(jù),緩存幾秒鐘是可以接受的,因此開啟nginx proxy

cache來解決該問題,開啟后降到正常水平;我們目前正在使用Nginx+Lua架構(gòu)改造服務(wù),數(shù)據(jù)過濾、URL重寫等在Nginx層完成,通過URL重寫+一致性哈希負載均衡,不怕隨機URL,一些服務(wù)提升了10%+的緩存命中率。

目前我們大量使用內(nèi)存級nginx proxy cache和nginx共享字典做數(shù)據(jù)緩存。

http://c.3.cn/recommend?callback=jQuery4132621&methods=accessories%2Csuit&p=103003&sku=1217499&cat=9987%2C653%2C655&lid=1&uuid=1156941855&pin=zhangkaitao1987&ck=pin%2CipLocation%2Catw%2Caview&lim=6&cuuid=1156941855&csid=122270672.4.1156941855%7C91.1440679162&c1=9987&c2=653&c3=655&_=1440679196326

還有我們會對這些前端的url進行重寫,所以不管怎么加隨機數(shù),都不會影響我們服務(wù)端的命中率,我們服務(wù)端做了參數(shù)的重新拼裝和驗證。

微信接口調(diào)用量暴增

14年的一段時間微信接口調(diào)用量暴增,通過訪問日志發(fā)現(xiàn)某IP頻繁抓??;而且按照商品編號遍歷,但是會有一些不存在的編號。

解決方案:

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

回源到服務(wù)接口的進行請求限流,保證服務(wù)質(zhì)量。

回源到DB的或者依賴系統(tǒng)的,我們也只能通過限流保證服務(wù)質(zhì)量。

開啟Nginx Proxy Cache性能不升反降

開啟Nginx Proxy Cache后,性能下降,而且過一段內(nèi)存使用率到達98%。

解決方案:

對于內(nèi)存占用率高的問題是內(nèi)核問題,內(nèi)核使用LRU機制,本身不是問題,不過可以通過修改內(nèi)核參數(shù)

sysctl -w vm.extra_free_kbytes=6436787

sysctl -w vm.vfs_cache_pressure=10000

使用Proxy Cache在機械盤上性能差可以通過tmpfs緩存或nginx共享字典緩存元數(shù)據(jù),或者使用SSD,我們目前使用內(nèi)存文件系統(tǒng)。

配送至讀服務(wù)因依賴太多,響應(yīng)時間偏慢

配送至服務(wù)每天有數(shù)十億調(diào)用量,響應(yīng)時間偏慢。

解決方案:

串行獲取變并發(fā)獲取,這樣一些服務(wù)可以并發(fā)調(diào)用,在我們某個系統(tǒng)中能提升一倍多的性能,從原來TP99差不多1s降到500ms以下;

預(yù)取依賴數(shù)據(jù)回傳,這種機制還一個好處,比如我們依賴三個下游服務(wù),而這三個服務(wù)都需要商品數(shù)據(jù),那么我們可以在當(dāng)前服務(wù)中取數(shù)據(jù),然后回傳給他們,這樣可以減少下游系統(tǒng)的商品服務(wù)調(diào)用量,如果沒有傳,那么下游服務(wù)再自己查一下。

假設(shè)一個讀服務(wù)是需要如下數(shù)據(jù):

數(shù)據(jù)A 10ms

數(shù)據(jù)B 15ms

數(shù)據(jù)C? 20ms

數(shù)據(jù)D? 5ms

數(shù)據(jù)E? 10ms

那么如果串行獲取那么需要:60ms;而如果數(shù)據(jù)C依賴數(shù)據(jù)A和數(shù)據(jù)B、數(shù)據(jù)D誰也不依賴、數(shù)據(jù)E依賴數(shù)據(jù)C;那么我們可以這樣子來獲取數(shù)據(jù):

那么如果并發(fā)化獲取那么需要:30ms;能提升一倍的性能。

假設(shè)數(shù)據(jù)E還依賴數(shù)據(jù)F(5ms),而數(shù)據(jù)F是在數(shù)據(jù)E服務(wù)中獲取的,此時就可以考慮在此服務(wù)中在取數(shù)據(jù)A/B/D時預(yù)取數(shù)據(jù)F,那么整體性能就變?yōu)榱耍?5ms。

我們目前大量使用并發(fā)獲取和預(yù)取數(shù)據(jù),通過這種優(yōu)化我們服務(wù)提升了差不多10ms性能。 而且能顯著減少一些依賴服務(wù)的重復(fù)調(diào)用,給他們減流。

如下服務(wù)是在抖動時的性能,老服務(wù)TOP99 211ms,新服務(wù)118ms,此處我們主要就是并發(fā)調(diào)用+超時時間限制,超時直接降級。

網(wǎng)絡(luò)抖動時,返回502錯誤

Twemproxy配置的timeout時間太長,之前設(shè)置為5s,而且沒有分別針對連接、讀、寫設(shè)置超時。后來我們減少超時時間,內(nèi)網(wǎng)設(shè)置在150ms以內(nèi),當(dāng)超時時訪問動態(tài)服務(wù)。對于讀服務(wù)的話,應(yīng)該設(shè)置合理的超時時間,比如超時了直接降級。

機器流量太大

2014年雙11期間,服務(wù)器網(wǎng)卡流量到了400Mbps,CPU30%左右。原因是我們所有壓縮都在接入層完成,因此接入層不再傳入相關(guān)請求頭到應(yīng)用,隨著流量的增大,接入層壓力過大,因此我們把壓縮下方到各個業(yè)務(wù)應(yīng)用,添加了相應(yīng)的請求頭,Nginx

GZIP壓縮級別在2~4吞吐量最高;應(yīng)用服務(wù)器流量降了差不多5倍;目前正常情況CPU在4%以下。

因為之前壓縮都是接入層做的,后來因為接入的服務(wù)太多,因此我們決定在各應(yīng)用去做。

一些總結(jié)

數(shù)據(jù)閉環(huán)

數(shù)據(jù)維度化

拆分系統(tǒng)

Worker無狀態(tài)化+任務(wù)化

異步化+并發(fā)化

多級緩存化

動態(tài)化

彈性化

降級開關(guān)

多機房多活

多種壓測方案

Nginx接入層線上灰度引流

接入層轉(zhuǎn)發(fā)時只保留有用請求頭

使用不需要cookie的無狀態(tài)域名(如c.3.cn),減少入口帶寬

Nginx Proxy Cache只緩存有效數(shù)據(jù),如托底數(shù)據(jù)不緩存

使用非阻塞鎖應(yīng)對local cache失效時突發(fā)請求到后端應(yīng)用(lua-resty-lock/proxy_cache_lock)

使用Twemproxy減少Redis連接數(shù)

使用unix domain socket套接字減少本機TCP連接數(shù)

設(shè)置合理的超時時間(連接、讀、寫)

使用長連接減少內(nèi)部服務(wù)的連接數(shù)

去數(shù)據(jù)庫依賴(協(xié)調(diào)部門遷移數(shù)據(jù)庫是很痛苦的,目前內(nèi)部使用機房域名而不是ip),服務(wù)化

客戶端同域連接限制,進行域名分區(qū):c0.3.cn c1.3.cn,如果未來支持HTTP/2.0的話,就不再適用了。

Q&A

Q1:對于依賴服務(wù)的波動,導(dǎo)致我們系統(tǒng)的不穩(wěn)定,我們是怎么設(shè)計的?

我們的數(shù)據(jù)源有三套:前端數(shù)據(jù)集群 該數(shù)據(jù)每個機房有兩套,目前兩個機房。數(shù)據(jù)異構(gòu)集群同上動態(tài)服務(wù)(調(diào)用依賴系統(tǒng))。

設(shè)置好超時時間,尤其連接超時,內(nèi)網(wǎng)我們一般100ms左右;

每個機房讀從、如果從掛了降級讀主;

如果前端集群掛了,我們會讀取動態(tài)服務(wù)(1、先mget原子數(shù)據(jù)異構(gòu)集群;2、失敗了讀依賴系統(tǒng))。

Q2:靜態(tài)化屏蔽通過js是怎么做的?

緊急上線js,做跳轉(zhuǎn);

上線走流程需要差不多10分鐘,然后還有清理CDN緩存,用戶端還有本地緩存無法清理;

我們目前文件都放到我們的自動部署上,出問題直接修改,然后同步上去,重啟nginx搞定。

Q3:內(nèi)網(wǎng)的服務(wù)通過什么方式提供?

我偏好使用HTTP,目前也在學(xué)習(xí)HTTP2.0,有一些服務(wù)使用我們自己開發(fā)類似于DUBBO的服務(wù)化框架,之前的版本就是DUBBO改造的。

Q4:對于mq 的處理如果出現(xiàn)異常是怎么發(fā)現(xiàn)和處理的?

MQ,我們目前是接收下來存Redis,然后會寫一份到本地磁盤文件;

我們會把消息存一天的;

一般出問題是投訴,我們會緊急回滾消息到一天中的某個時間點。

Q5:對于模板這塊,有做預(yù)編譯處理?或者直接使用Lua寫模板嗎?

luajit,類似于java jit;

lua直接寫模板。

Q6: jimdb能否介紹下,特性是什么,為什么選用?

jimdb就是我們起的一個名字,之前版本就是redis+lmdb持久化引擎,做了持久化;

我們根據(jù)當(dāng)時的壓測結(jié)果選擇的,按照對比結(jié)果選擇的,我們當(dāng)時也測了豆瓣的beansdb,性能非常好,就是需要定期人工歸并。

Q7:咨詢下對于價格這類敏感數(shù)據(jù),前端有緩存么?還是都靠價格服務(wù)扛?

價格前端不緩存;

價格實時同步到Nginx+Lua本地的redis集群(大內(nèi)存);

不命中的才回源到tomcat查主redis/DB。

價格數(shù)據(jù)也是通過MQ得到變更存儲到本地redis的,nginx+lua直接讀本機redis,性能沒的說。

Q8:庫存和價格一樣的模式處理嗎?

前端展示庫存不是,我們做了幾秒的服務(wù)端緩存。

Q9:github里有一個開源的基于lmdb的redis 你們用的這個嗎?

我們內(nèi)部自己寫的,有一版基于LevelDB的,我記得github上叫ardb。

Q10:看測試條件說測試的是大小5~30KB左右的數(shù)據(jù),有沒有測試過更大文件lmdb的表現(xiàn)?

這個沒有,我們的數(shù)據(jù)都是真實數(shù)據(jù),最大的有50KB左右的,但是分布比較均勻;當(dāng)時還考慮壓縮,但是發(fā)現(xiàn)沒什么性能問題,就沒有壓縮做存儲了。

Q11:關(guān)于redis緩存,是每個子系統(tǒng)擁有自己的一套緩存;還是使用統(tǒng)一的緩存服務(wù)?是否有進行過對比測試?(看到又說使用單機緩存防止服務(wù)掛掉,影響整體服務(wù))

我們公司有統(tǒng)一的緩存服務(wù)接入并提供運維;

我們的服務(wù)自己運維,因為我們是多機房從,每機房讀自己的從,目前不支持這種方式;

(看到又說使用單機緩存防止服務(wù)掛掉,影響整體服務(wù))這個主要是降級+備份+回源解決。

Q12: “我們目前一些線上服務(wù)器使用的是LMDB,其他一些正在嘗試公司自主研發(fā)的CycleDB引擎”。 開始自主研發(fā),這個是由于lmdb有坑還是處于別的考慮?

寫放大問題;

掛主從需要dump整個文件進行同步。


----------------------------------------------------------------------------------------------

閱讀更多好文,請關(guān)注公眾號:

新雨社

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容