蘇寧11.11:蘇寧易購訂單搜尋系統架構及實現

weixin_34127717發表於2018-11-11

背景

隨著蘇寧易購平臺規模的飛速發展,平臺的訂單量呈現指數級的增長,儲存容量已達TB級,訂單量更是到了萬億級別,尤其在雙11大促流量洪峰的場景下,面臨兩個挑戰:

1、如何儲存如此巨大的資料量
2、如何提供高併發、低延遲、多維度的檢索服務
傳統關係型資料庫無法支撐多維度的模糊檢索,為此,我們選用了elasticsearch來提供索引服務,原因如下:

1、技術及配套元件成熟
2、有較大的使用者群體,且社群活躍
3、提供簡便易用的api服務,易上手
4、具有快速的水平及垂直擴容能力,具備高可用,高效能的特徵

叢集整體架構

按查詢維度以及目標使用人群,分為以下叢集
1:全量訂單欄位叢集:儲存了全部訂單資料,目前主要用於:1)其他索引叢集欄位初始化時提供資料來源。2)搜尋出訂單ID時,根據ID取出該訂單所有欄位詳情,由於訂單號即為docId,所以直接get速度很快。數以億計的訂單,不可全由一個索引承載,應進行分索引處理。由於訂單號本身就是分段使用的,根據訂單號生成規則,我們將這些訂單均勻分配到多個索引中,這樣可以控制索引大小並有效分散資料。索引規則定下來了,shard數按照每個shard不超過30G的原則來分。如果單個shard的容量突破30G時,可以根據訂單號生成的時間維度,建立新的叢集,在應用層路由到不同的叢集和索引。

Elasticsearch的正確使用姿勢應該只是用於建索引,而不是儲存資料,但是該叢集由於歷史原因一直儲存了下來,我們後續會將該部分資料遷移到公司大資料平臺上。

2:會員搜尋叢集:該叢集搜尋欄位相對較少,每次搜尋請求需要附帶會員號,主要用於提供給網際網路使用者搜尋“我的訂單”時使用。在索引設計上,我們按日期段分索引,以便橫向擴充套件,備份數量根據查詢請求量來設計。查詢時會帶上日期及會員號,根據日期即可定位到索引,按照會員號routing,能直接定位到某個shard。由於是按日期分索引,所以當叢集規模變得很大時,可以水平無限擴充套件叢集。

3:客服搜尋叢集:該叢集搜尋欄位相對較多,查詢條件不定,為了避免寬泛的搜尋條件而對線上顧客查詢造成影響,我們單獨為客服訂單查詢建了一套叢集。該叢集類似會員叢集,按日期段分索引,由於該叢集對搜尋時效沒有那麼高的要求,所以備份數可以少些。

4:頭行關係叢集:訂單頭和訂單行關係,快取記憶體。

5: Redis叢集:流量高峰時做削峰處理,線性輸出且做到對上游系統無感知,以保護ES叢集

6: wildfly叢集:對外提供RPC服務,外界對ES發起的查詢和資料初始化時一律經過此叢集,將查詢或寫入指令轉換成ES操作指令,這樣遮蔽底層實現,做索引或叢集調整時可以做到對上游系統透明,而且可以在介面層靈活控制訪問流量。

7: Nginx叢集:提供ES外掛鑑權服務,防止不受控制的訪問head,kopf外掛及呼叫REST服務,該叢集還提供反向代理服務,遮蔽master節點IP(提供http服務)。

8: DB叢集:發生錯誤時,錯誤指令入DB,後續做補償處理。
整體叢集示意圖如下:

\"\"

擴容

當系統能力不足時,可選的擴容方案如下:

1)副本數,shard數都不變,直接新增機器,讓ES自動再平衡資料,適用於單個節點上有多個分片時。機器數量增加後,單個機器上的索引分片數就相應減少,可以有效降低單個機器的IO壓力。

2)副本數增加,shard數不變,副本數增加後,對寫入tps會有一定的影響,但是能有效提升讀tps。

3)副本數不變,shard數增加。
此方案需要重建索引,所以在先期建索引時就需要考慮好資料量及增長速度。
由於叢集規模大,擴容時機器數量多,所以使用指令碼搭建機器環境,在一臺機器上操作所有機器的JDK,ES引數的配置。SSH配置,ES引數配置及服務啟動指令碼都是通用的,此處不再贅述。

增加搜尋欄位處理邏輯

在實際系統執行中,經常會發生需要增加搜尋條件的場景(會員搜尋叢集或客服搜尋叢集都可能增加索引欄位),這時候就需要重新灌資料,需要做好初始化和實時更新的順序邏輯。

1:當要初始化時,開啟初始化模式開關(基於ZooKeeper實現的實時配置中心)。

2:從全量叢集scroll資料集到其他搜尋叢集。

3:有某個文件的update報文過來時,不直接更新搜尋叢集的目標索引,而是從全量叢集get出所有目標欄位,然後全量覆蓋搜尋叢集中該文件。
這麼做是因為初始化灌資料和實時接收報文並更新是不同的執行緒,如果初始化過程中又接收到更新資料的指令,如果先更新了索引叢集,然後再拿到全量叢集的初始化資料,而拿到後全量叢集又發生了更新,則拿到的初始化資料是舊版本的資料,導致搜尋叢集和全量叢集的資料不一致。

\"\"

引入redis叢集

為應對寫入高峰,在wildfly叢集前置了一組redis叢集,填谷削峰,用於降低瞬時寫壓力。

為解決非同步問題,在寫入請求到來時,先入全量叢集再入redis,成功後再返回上游系統成功,上游系統只有在拿到這個成功標識後才會再次寫入後續指令,這樣就能保證全量叢集的資料正確性。目前這個方案的效能可以滿足需求,如果需要進一步提升效能,則寫入報文全部入redis然後直接返回上游系統成功或失敗標識,再開啟新執行緒讀取報文到全量叢集及其他搜尋叢集,當然用此方案時需要處理好非同步執行緒之間的關係及快取中的資料順序。

在寫入redis時既要防止熱點分片,也要防止亂序,還要防止資料遊離沒有執行緒去消費,為此我們處理邏輯如下:
1:報文先寫入全量叢集。

2:由於有10個redis分片,所以取訂單號的末位數字,根據此數字找到位於某個分片上的待處理集合(集合名:pending_X),並將訂單號塞入該集合。這樣可以防止待處理集合產生熱點。

3:在redis中建立以該單號為key的列表,列表中存放的是報文指令(如果列表已存在則直接將報文追加到列表最後)。到此步,上半部分寫入就完成了,可以返回上游系統成功標識。

4:新開執行緒,根據指令對應的訂單號,取出待處理集合中的該訂單號的key並執行setNx,如果取到鎖,則一直處理該列表中的報文,直到拿不到資料再退出迴圈。最後刪除待處理集合中該訂單號,刪除後再做一次檢驗是否有該訂單號的列表,防止刪除待處理集合中該key後又有新的請求過來。

5:定時任務巡檢待處理集合中的訂單號,如果有某個訂單號且setNx成功,則說明之前執行佇列消費的執行緒掛掉了,此時定時任務檢漏消費。

6:定時任務巡檢所有列表,如果某個列表對應的訂單號不在待處理集合中,則撿漏消費,防止以上步驟3中的最後一步刪除了待處理集合中該訂單號後又有新資料進來時且消費執行緒又突然掛掉了。
寫入各個叢集的示意圖如下:

\"\"

監控及管理

前臺應用提供RPC服務,當然後端需要有監控管理措施,我們主要做了以下幾方面:

安裝必要管理外掛,包括marvel,head,kopf,並將外掛入口統一整合到admin系統,下文有詳述。

機器資源使用監控:定時任務請求ES自帶系統狀態服務,拿到各個節點資源使用情況,如有即將達到閾值的資源會及時告警。

快取監控:監控redis中有多少待處理資料,依此判斷系統是否有資料積壓,以便動態調整消費執行緒數。
資料修復:如果有資料狀態不一致,丟欄位的現象發生,則請求上游系統重新下傳錯誤訂單資料。

壓測資料清理:壓測,各個大促節點前必做事項,檢測出系統極限能力,判斷瓶頸點,以便有針對性的改進。這些資料量大的垃圾資料需要及時清理,釋放寶貴系統資源。

此外,為方便運維,減少登入head外掛的頻率,以防誤操作,在後臺管理系統開發了查詢功能。

\"\"

許可權控制

日常運維必用的head/kopf外掛的安全機制:預設的head/kopf外掛是不帶許可權管理的,任何人只要知道域名就能訪問(不能直接訪問到ES機器,生產辦公網段是隔離的),這給生產系統帶來極大隱患。在後臺管理及外掛管理我們先後做了兩套方案:

最初方案:在外掛域名所在的nginx上我們配置了訪問許可權控制,這個方案執行過一段時間,但是後來發現,許可權難免會洩露,對於head和kopf外掛來說還是有一定的隱患,所以用下面的替代方案。

優化後方案:把head/kopf外掛的原始碼拿到應用的後臺管理系統,訪問外掛頁面時需要輸入動態密碼(公司內部應用提供的服務),只有配置了認證許可權的工號才能訪問外掛所在頁面,對外掛頁面的請求通過應用伺服器發起http請求到原先外掛域名所在的nginx伺服器,拿到資料後再在本地展現,原先外掛所在域名的nginx只有配置了白名單的伺服器才能訪問,白名單機器限定為應用後臺系統的伺服器,這樣徹底杜絕了許可權洩露帶來的隱患。

進入叢集連結初始頁:

\"\"

點選marvel連結後,由於不能操作叢集配置,所以還是用原先的nginx靜態許可權:

\"\"

點選head或kopf連結後則需要輸入動態令牌:

\"\"

一些需要注意的地方:

ES對記憶體的需求較大,設定java最大堆時,不要超過32G,因為一單超過32G,會有指標壓縮問題,不同機器具體閾值不一樣,為保險起見,我們設定-Xmx31g,垃圾回收器我們選擇了更適合於大堆記憶體的G1。以下是一些我們 的ES配置項:
#資料安全方面,需要防止一次性刪除所有索引,可以設定以下配置項:
action.disable_delete_all_indices:true

#分配shard時,考慮磁碟空間:
cluster.routing.allocation.disk.threshold_enabled:true

#鎖定記憶體,同時也要允許elasticsearch的程式可以鎖住記憶體, linux命令: ulimit -l unlimited
bootstrap.mlockall: true

#快取型別設定為Soft Reference, 最大限度的使用記憶體而不引起OutOfMemory
index.cache.field.type: soft

#設定單播
discovery.zen.ping.multicast.enabled:false
discovery.zen.ping.unicast.hosts: #所有master的ip:port,

#防止腦裂,(masterNode數量/2) + 1
discovery.zen.minimum_master_nodes:2

#擴容時,新機器加入叢集之前需要關掉自動平衡,機器全部加入叢集后再開啟自動平衡。

#關閉自動平衡:
PUT http://xx.xx.xx.xx:9200/_cluster/settings
{“transient”:{“cluster.routing.allocation.enable”:“none”}}
開啟自動平衡:
PUT http://xx.xx.xx.xx:9200/_cluster/settings
{“transient”:{“cluster.routing.allocation.enable”:“all”}}

#在重啟es或者關閉索引之間,建議先執行flush行為,確保所有資料都被寫入磁碟,避免資料丟失:
curl –XPOST xx.xx.xx.xx:9200/index-name/_flush?v

#將ES的資料目錄放到記憶體檔案系統(遮蔽磁碟I/O瓶頸,記憶體檔案系統寫入速度能達到1GB/S以上)
mount -t tmpfs -o size=10G,mode=0755 tmpfs /home/elasticsearch/data

還有其他一些ES的優化配置,可以參考ES官方文件,此處不再贅述。

作者:
劉發亮,蘇寧易購IT總部中臺研發中心技術總監,主要負責基礎技術元件相關研發工作。10多年從事java系統相關開發及架構設計,主導過蘇寧人事共享專案,蘇寧資金系統,蘇寧金融APP閘道器,蘇寧雲商城等系統研發,對高併發,大資料量資料處理有較豐富的經驗。

相關文章