京東服務(wù)市場高并發(fā)下SOA服務(wù)化演進架構(gòu)
京東服務(wù)市場是京東商家與第三方獨立軟件提供商(ISV)進行服務(wù)類的在線交易平臺。作為京東生態(tài)圈重要的一環(huán),伴隨著整個京東的快速增長,也在快速的發(fā)展。隨著服務(wù)市場訪問、交易量指數(shù)級的增長,系統(tǒng)由原來的ALL IN ONE架構(gòu),快速的演進成為SOA架構(gòu)。
木桶的容量由木桶最短的木板決定,高并發(fā)環(huán)境下,單個服務(wù)的性能決定了整個服務(wù)市場的性能。 “可用插件列表服務(wù)”是服務(wù)市場的核心服務(wù)之一,優(yōu)化該服務(wù)性能的過程,帶動整個服務(wù)市場服務(wù)架構(gòu)的演進。
宏觀的看,大到系統(tǒng)小到模塊都由自身+外部依賴組成,性能優(yōu)化主要從自身與外部依賴兩個方面來進行。
一、優(yōu)化自身
單線程到多線程的升級,嘗試通過并行提高服務(wù)性能。
根據(jù)日志分析,整體調(diào)用中“服務(wù)詳細信息”占用時間最多,并行雖然壓縮了一些可并行服務(wù)的調(diào)用時間,但對于無法并行的“服務(wù)詳細信息”環(huán)節(jié),依然沒有改善。要改善必須找到“商品服務(wù)”性能不高的原因。
可見自身優(yōu)化能起一些作用,但外部依賴起著更決定性的作用。
二、解決外部依賴沖突
“商品服務(wù)”性能不高,這是為什么呢?先從“商品服務(wù)”的依賴開始分析。單獨調(diào)用該服務(wù),或壓測該服務(wù),性能都不差,但為何線上性能卻不佳?
1. 不同服務(wù)外部依賴資源沖突
對“商品服務(wù)”依賴的資源進行梳理,發(fā)現(xiàn)“商品服務(wù)”與“類目服務(wù)”使用相同數(shù)據(jù)庫資源,非調(diào)用高峰期資源足夠不相互影響,大并發(fā)環(huán)境下兩個服務(wù)開始爭奪資源。
將依賴資源分開,不同的服務(wù)使用不同的資源,通過調(diào)用不同的數(shù)據(jù)源解決沖突。
2. 相同服務(wù)外部資源依賴沖突
解決了兩個服務(wù)對數(shù)據(jù)庫資源的依賴沖突,性能有所提高,但性能總有很大的波動,排除其他服務(wù)外部資源的依賴沖突,看看“商品服務(wù)”自身對資源是如何使用的。
“商品服務(wù)”所有功能都單一的依賴數(shù)據(jù)庫資源。服務(wù)上線后,自身多個功能開始爭搶數(shù)據(jù)庫資源。
按使用場景進行外部依賴資源解耦:
- 為保證交易一致性,繼續(xù)采用MySQL。MySQL的 INNODB引擎長于 OLTP 在線事務(wù)處理,為了保證數(shù)據(jù)強一致性的場景繼續(xù)選擇使用MySQL數(shù)據(jù)庫。
- 客戶端登錄用戶需要獲得***的數(shù)據(jù)反饋,且有PIN這個固定的維度。查詢條件簡單,能符合KEY-VALUE方式,Redis很適合這個場景。
- 大前端非登錄狀態(tài)下,訪問的用戶無須登錄,有很大的訪問量,更多的是獲取服務(wù)的一些介紹。大數(shù)據(jù)量,可容忍一定程度的延遲,所以采用ES來進行查詢支撐。
- 外部系統(tǒng)希望獲得***服務(wù)的變化,推的方式遠強于輪訓拉取的方式。通過MQ訂閱服務(wù)的變化情況。
- 有復(fù)雜計算,但對實時性要求不高,服務(wù)統(tǒng)計分析系統(tǒng)通過大數(shù)據(jù)平臺獲取數(shù)據(jù)進行分析。
三、建立統(tǒng)一的內(nèi)存緩存模型
計算機的世界里沒有魔法,時間換空間、空間換時間是所有方案的基礎(chǔ)。
參考常用的MySQL INNODB引擎,為加快查詢速度會在內(nèi)存中設(shè)置一塊內(nèi)存作為緩沖區(qū),將查詢結(jié)果從硬盤中加載到緩沖區(qū),下次相同的查詢直接使用緩沖區(qū)數(shù)據(jù)。同樣的,如果要提高查詢響應(yīng)速度,必須把服務(wù)數(shù)據(jù)緩存到內(nèi)存中。單機內(nèi)存有限,無法容納所有數(shù)據(jù),且服務(wù)器重啟時整個內(nèi)存重建所耗費的時間也是無法接受的,于是選擇用Redis與ES按照不同的使用場景來構(gòu)造內(nèi)存緩存。
1. 選擇主動緩存
常規(guī)的緩存方案:查詢構(gòu)建+定期失效。對有大量重復(fù)查詢的環(huán)境效果很好,但在實際情況下,在某些場景卻無法發(fā)揮預(yù)想中的作用。
場景特征:
- 每個用戶只會打開一次客戶端,獲取一次插件信息,不會重復(fù)頻繁的去拉取列表。
- 訪問集中在8點到9點這個時間段。
- 使用被動緩存的后果:
- 8點前Redis緩存內(nèi)是空的。
- 8點到9點,所有的列表信息都是***次獲取,查詢?nèi)看┩妇彺嬷苯哟虻綌?shù)據(jù)庫。
- 8點到9點之間獲取插件列表后做了插件的續(xù)訂或權(quán)限變更,由于緩存定時失效,導(dǎo)致更新無法反饋,用戶不斷刷新插件列表直到緩存失效獲取到更新結(jié)果。人為制造流量洪峰,Redis抗住的也是這些無用的人為重復(fù)調(diào)用量。
- 9點以后緩存逐漸過期,不再被使用。
一個測試性能很好,實際卻沒有用的緩存。
基于以上,緩存層決定通過主動構(gòu)建的方式建立緩存。在數(shù)據(jù)修改后,將變化數(shù)據(jù)主動的加載到Redis緩存中,緩存不再設(shè)置過期時間。
有的服務(wù)每次獲取結(jié)果都要通過非常繁瑣的計算,如果這些繁瑣的計算集中在同一時間點,對于后端資源(數(shù)據(jù)庫)是非常大的負擔。
錯峰使用資源,把構(gòu)建緩存的過程分散在離散的調(diào)用中,集中使用時直接調(diào)用緩存獲取最終結(jié)果。
上面提到過“類目服務(wù)”獲取類目層級列表需要多次查詢數(shù)據(jù)庫,這對數(shù)據(jù)庫是很大的負擔。
提前構(gòu)建,在類目創(chuàng)建或類目變更時就重新構(gòu)建類目層級列表,將結(jié)果存入緩存,高峰期使用時直接獲取已構(gòu)建完成的類目層級列表。
2. 緩存碎片化
系統(tǒng)使用一段時間后,由于業(yè)務(wù)系統(tǒng)對服務(wù)數(shù)據(jù)需求的不一致,服務(wù)開發(fā)人員開始為每個外部系統(tǒng)提供一塊主動緩存。這些緩存完全不具備通用性但又數(shù)量眾多。每次服務(wù)模型修改,研發(fā)人員都要花大量時間去維護這些不通用的緩存。占用的緩存越來越多,但緩存的使用率并不高。
為去除冗余,降低維護工作量,最初按照數(shù)據(jù)表的維度將每一個表作為一個緩存。作為ES緩存可以采用這個方案,但是對于Redis緩存,這種緩存方式卻帶來了很大的麻煩。
數(shù)據(jù)庫表設(shè)計為保證強一致性,建表的時候嚴格依照范式,數(shù)據(jù)中很少有冗余,表也切的很小,查詢時通過聯(lián)合查詢來獲取整體數(shù)據(jù)。但Redis沒有聯(lián)合查詢的功能,因此不得不多次調(diào)用不同的緩存,多次調(diào)用大大降低了性能。對于查詢而言,數(shù)據(jù)庫會進行一些反范式操作。既然Reids緩存能夠支撐查詢,那么也可以做一定的冗余把這些關(guān)聯(lián)數(shù)據(jù)作為一個整體對象緩存起來。
對于服務(wù)開發(fā)人員而言,主要職責是根據(jù)環(huán)境變化,不斷的進化服務(wù)模型。服務(wù)開發(fā)人員維護一套***、最完整的服務(wù)模型并將模型開放出來;服務(wù)調(diào)用者,特別是只獲取服務(wù)數(shù)據(jù)的調(diào)用者完全可以通過對服務(wù)完整模型的自定義裁剪獲取自己所需要的數(shù)據(jù),各開發(fā)人員只關(guān)注自己需要關(guān)注的地方,大大提高了工作效率。
3. 緩存構(gòu)建方案
面臨問題:
- 服務(wù)緩存構(gòu)建與變更屬于非核心流程,所以只能異步執(zhí)行,通過MQ的方式與主流程解耦。
- 服務(wù)屬性修改入口眾多,通過MQ會出現(xiàn)操作重排序問題。
- 服務(wù)屬性修改入口眾多,每次修改或添加入口都必須跟著修改,業(yè)務(wù)侵入性強。
- 發(fā)送MQ的時機,事務(wù)中影響事務(wù)性能,當事務(wù)回滾時還需要發(fā)送補償;事務(wù)后又無法保證一定能發(fā)送。
解決方案:
- 采用binlake的方式進行異步緩存構(gòu)建,與主流程解耦。 Binlake是京東一款通過解析MySQL的binlog日志,并通過MQ隊列進行解析受數(shù)據(jù)變更事件傳遞的數(shù)據(jù)異構(gòu)產(chǎn)品。
- 數(shù)據(jù)庫是功能修改后唯一進行數(shù)據(jù)持久化的地方,僅需監(jiān)控數(shù)據(jù)庫修改,就可獲知所有的服務(wù)屬性修改,不再需要跟著業(yè)務(wù)走,也不用擔心操作重排序。
- 事務(wù)提交才能產(chǎn)生binlog日志,binlog的產(chǎn)生標志數(shù)據(jù)修改出于確定狀態(tài),不會出現(xiàn)回滾,解決MQ發(fā)送時機的問題。
- Binlog事件通過MQ發(fā)送,發(fā)送不成功不修改日志偏移量,下次繼續(xù)發(fā)送。接收隊列為回執(zhí)確認式隊列,消費完成回執(zhí)確認前會不斷進行重試,解決發(fā)送丟失或接收后丟失問題。
初期采取直接解析binlog報文,按照消息內(nèi)容更新數(shù)據(jù)。為保證消費順序性,必須只有一個隊列進行消息傳遞,大大降低了效率,并埋下了單點的隱患。
解決方法是,MQ不作為數(shù)據(jù)變化的承載者,而是作為一個通知者。當緩存構(gòu)造者接受到MQ的時候,從數(shù)據(jù)庫獲取***的服務(wù)屬性,更新到緩存中。通過拉式獲取完整的服務(wù)屬性數(shù)據(jù),保證了數(shù)據(jù)的完整性、一致性。而主動拉取數(shù)據(jù),不限制于消息本身,也不需要保證消息順序性,***解決效率與單點問題。在屬性被多次修改時,更能在其他修改消息未接收到時,就已經(jīng)拉取到***數(shù)據(jù)更新了緩存數(shù)據(jù),進一步提高了實時性。
***,單向事件觸發(fā)有很小的概率還是會發(fā)生數(shù)據(jù)不一致。解決辦法是,采用定時比對的方式,每個小時(可調(diào)整)通過時間戳比對當日數(shù)據(jù)與緩存數(shù)據(jù)差異,進行最終補償。
四、后記
解決了不同服務(wù)對相同資源的調(diào)用沖突,服務(wù)內(nèi)不同的場景使用不同的資源支撐,創(chuàng)建了統(tǒng)一緩存層擺脫對數(shù)據(jù)庫的依賴。使用不同的方法解決了當統(tǒng)一緩存建立以后,如何使查詢擺脫了對數(shù)據(jù)庫的強依賴,服務(wù)性能得到了非常大的提升。
改造前支撐調(diào)用量:
改造后支撐調(diào)用量:
通過以上演進,“可用插件列表服務(wù)”并發(fā)性能有了很大的提升。 2018年11.11零點調(diào)用量10分鐘內(nèi)陡增6倍,平穩(wěn)度過。
作者簡介:張俊卿,研發(fā)老兵,熱愛技術(shù),喜歡挑戰(zhàn)。熟悉各種開源框架,對大型分布式系統(tǒng)有豐富的架構(gòu)、設(shè)計經(jīng)驗。性能卓越、設(shè)計優(yōu)雅是其一生的追求。
【本文來自51CTO專欄作者張開濤的微信公眾號(開濤的博客),公眾號id: kaitao-1234567】