企業(yè)微信大規(guī)模組織架構(gòu)性能優(yōu)化實(shí)踐(企業(yè)微信大規(guī)模組織架構(gòu)性能優(yōu)化實(shí)踐報(bào)告)
作者:yecong,騰訊 WXG 客戶端開發(fā)工程師
本文主要講述企業(yè)微信大規(guī)模組織架構(gòu)(后文簡稱為大架構(gòu))的性能優(yōu)化過程。分成兩部分講述,第一部分是短線迭代的優(yōu)化,主要是并發(fā)性能的優(yōu)化。第二部分是長線迭代的優(yōu)化,主要是從業(yè)務(wù)模式上做了根本性優(yōu)化。
一、并發(fā)性能優(yōu)化
1.1 背景
當(dāng)私有化的組織架構(gòu)上升到100W的量級時,出現(xiàn)了嚴(yán)重影響組織架構(gòu)使用的問題:打開二級部門時,加載緩慢。如圖所示,loading可能持續(xù)一分鐘以上。
問題:打開二級部門加載緩慢
1.2 分析
我們分析一下加載二級部門的流程,下面是加載二級部門的流程圖。
- 如果從來沒加載過該部門,需要從服務(wù)端拉取部門下的節(jié)點(diǎn)詳情。這里是因?yàn)橹拔覀円呀?jīng)做了優(yōu)化,首次登錄時只拉取了部門的節(jié)點(diǎn)ID,沒有拉取詳情。
- 如果加載過該部門,就直接從DB讀取該部門的數(shù)據(jù),然后返回UI展示。
當(dāng)只有一條DB線程時,組織架構(gòu)更新的任務(wù),可能會插入到加載二級部門的任務(wù)的前面。而在百萬級別的組織架構(gòu)中,全量更新的DB任務(wù)有可能比較久,全量更新的插入或者更新節(jié)點(diǎn)可能比較多,導(dǎo)致本來很快可以完成的二級部門加載任務(wù),要排隊(duì)比較久才能執(zhí)行完。
下面是組織架構(gòu)全量更新的流程圖。
全量更新
在這里,讀寫并發(fā)上出現(xiàn)了明顯的瓶頸。原因總結(jié)如下:
- 加載二級部門和全量更新共用一條DB線程
- 當(dāng)全量更新大量節(jié)點(diǎn)時,全量更新的低優(yōu)先級任務(wù)卡住加載二級部門的高優(yōu)先級任務(wù)
1.3 方案
讀寫分離為了提高組織架構(gòu)在大規(guī)模數(shù)據(jù)下的讀寫并發(fā)性能,我們開啟了wal模式,把讀寫任務(wù)分別放在不同的線程中執(zhí)行。
針對加載二級部門的流程,可以在讀線程中讀取部門的詳情節(jié)點(diǎn),而組織架構(gòu)更新可以在寫線程中單獨(dú)執(zhí)行。
由于加載二級部門的原流程是拉取數(shù)據(jù)、寫入DB、再從DB讀取數(shù)據(jù),而且WAL只支持一寫多讀,因此我們調(diào)整了緩存策略,把保存節(jié)點(diǎn)詳情的寫任務(wù)延遲到流程最后,優(yōu)先構(gòu)造了cache返回UI。這樣從DB中讀出數(shù)據(jù)的讀任務(wù),就不需要等待保存節(jié)點(diǎn)詳情的寫任務(wù)。避免了保存節(jié)點(diǎn)的寫任務(wù)再次被其他寫任務(wù)阻塞,讀任務(wù)又被保存節(jié)點(diǎn)的寫任務(wù)阻塞,退化成串行操作。
WAL機(jī)制的原理
調(diào)用方修改的數(shù)據(jù)并不直接寫入到數(shù)據(jù)庫文件中,而是寫入到另外一個稱為WAL的文件中,然后在隨后的某個時間點(diǎn)被寫回到數(shù)據(jù)庫文件中。在這個時間點(diǎn)的回寫操作,會降低數(shù)據(jù)庫當(dāng)時的讀寫性能。但是通過設(shè)置對WAL文件大小的限制,這種性能影響是可控的。實(shí)際上線后也沒有遇到由于checkpoint同步導(dǎo)致數(shù)據(jù)庫慢的反饋。
緩存策略
寫策略的步驟:先更新緩存中的數(shù)據(jù),再更新數(shù)據(jù)庫中的數(shù)據(jù)。
讀策略的步驟:
如果讀取的數(shù)據(jù)命中了緩存,則直接返回?cái)?shù)據(jù);如果讀取的數(shù)據(jù)沒有命中緩存,則從數(shù)據(jù)庫中讀取數(shù)據(jù),然后將數(shù)據(jù)寫入到緩存,并且返回給UI。
方案總結(jié)
方案優(yōu)點(diǎn)缺點(diǎn)1. 開啟WAL,拆分DB讀寫線程 2. 緩存策略適配:先保證UI展示,再讓數(shù)據(jù)落地1. 讀寫并發(fā)
2. 最大化利用緩存WAL文件同步回?cái)?shù)據(jù)庫文件的時候,會降低當(dāng)時的讀寫性能。
1.4 效果
在優(yōu)化前,只有52%的用戶能在1s內(nèi)加載完二級部門。而上線之后,93%的用戶都能在1s內(nèi)打開二級部門。耗時小于1s的用戶占比提升40%!
二、業(yè)務(wù)模式優(yōu)化
2.1 問題
2.1.1 背景
當(dāng)業(yè)務(wù)進(jìn)一步發(fā)展時,我們預(yù)估未來將要到達(dá)300W量級的組織架構(gòu)。于是我們就開始提前規(guī)劃如何能在組織架構(gòu)數(shù)量一直增長的情況下,還能讓組織架構(gòu)流暢好用。
2.1.2 問題
- 選人控件閃退和ANR
- 組織架構(gòu)全量更新閃退
在300w的組織架構(gòu)環(huán)境中,舊的組織架構(gòu)加載方案,在全量更新、選人控件中均出現(xiàn)了占用內(nèi)存過大甚至閃退的問題。而且舊方案的加載時間會隨著節(jié)點(diǎn)數(shù)量的增加,不可避免地成正比增長。
2.1.3 分析
當(dāng)前方案的耗時、內(nèi)存占用與用戶組織架構(gòu)的大小成正比,單點(diǎn)優(yōu)化無法滿足組織架構(gòu)持續(xù)增長的需求。具體來說,會造成下面的一些問題:
- 選人控件會加載全量的組織架構(gòu)ID樹,數(shù)量過多時容易發(fā)生閃退和ANR。
- 組織架構(gòu)全量更新占用內(nèi)存過大,造成閃退。
因此,我們需要一個新的業(yè)務(wù)模式,即便總的組織架構(gòu)規(guī)模一直上漲的情況下,也能維持較好的性能。
2.2 方案比較
比較容易想到的一個方案是web加載的模式,不保存本地?cái)?shù)據(jù),但是體驗(yàn)比較差,每層都會出loading。
聯(lián)系到我們的具體業(yè)務(wù),由于私有化對不同的部門,劃分出了具有意義的獨(dú)立組織機(jī)構(gòu)–單位。單位是具有管理意義的部門,不同單位可以獨(dú)立加載。而每個人,也擁有主單位和兼崗單位。所以可以按照單位加載的方式,從根本上解決目前組織架構(gòu)面臨的瓶頸。
按單位加載,可以簡單理解為按部門加載。
方案缺點(diǎn)優(yōu)點(diǎn)Web加載模式:不保存本地?cái)?shù)據(jù)體驗(yàn)太差,每層都要出loading理論上可支持的數(shù)據(jù)量上限最大單位加載模式:按單位加載需要推廣到企業(yè)符合業(yè)務(wù)邏輯,可支持到500萬量級
概念定義
- 單位:政府行政組織結(jié)構(gòu)中的職能部門,組建架構(gòu)并承擔(dān)對應(yīng)責(zé)任
- 主單位:【我】所在的單位
- 其他單位:除了【我】所在的其他單位
- 骨架:通訊錄骨架包含了所有的單位節(jié)點(diǎn)
- 普通部門:不屬于任何單位的部門節(jié)點(diǎn)
下圖是組織架構(gòu)樹的示意圖,藍(lán)色節(jié)點(diǎn)是優(yōu)先加載的本單位,灰色節(jié)點(diǎn)是其他單位,紅色節(jié)點(diǎn)是骨架。不同的單位獨(dú)立加載。
2.3 按單位加載
2.3.1 加載策略
接下來我們看看加載策略。
第一是對自己所在的主單位(藍(lán)色節(jié)點(diǎn)),每次喚醒時就會更新,跟舊組織架構(gòu)的邏輯類似,但是會限制拉取節(jié)點(diǎn)的數(shù)量。
第二對于其他單位(灰色節(jié)點(diǎn)),點(diǎn)擊到該單位時才會拉取,2個小時后會淘汰刪除,避免數(shù)據(jù)表過大。
第三對于骨架(紅色節(jié)點(diǎn)),會全量加載節(jié)點(diǎn)ID,再拉取節(jié)點(diǎn)詳情。
拉取策略限制了能夠拉取的節(jié)點(diǎn)詳情數(shù)量,如果單位節(jié)點(diǎn)數(shù)量超過了限制,首先拉取全量ID,再按照優(yōu)先規(guī)則,拉取配置的節(jié)點(diǎn)詳請數(shù)量。
2.3.2 加載流程
加載的流程是先拉取自己的單位列表,然后拉取每個單位的全量通訊錄ID,再按照后臺策略,拉取所需的詳細(xì)節(jié)點(diǎn),最后拉取骨架。
- 如果點(diǎn)擊到主單位:
- 如果只有ID沒有節(jié)點(diǎn),會立刻拉取節(jié)點(diǎn)詳情返回界面。
- 如果ID和節(jié)點(diǎn)詳情都有,可以直接返回UI展示,然后延遲刷新節(jié)點(diǎn)。
- 如果是點(diǎn)擊到其他單位,可能出現(xiàn)ID和詳情都沒有的情況,需要拉取其他單位的節(jié)點(diǎn),界面loading等待。
- 如果是骨架,就一定有節(jié)點(diǎn)和詳情,只需要延遲刷新。
2.4 跨平臺設(shè)計(jì):分層設(shè)計(jì)
接下來我們看看如何分層。在500萬量級的大規(guī)模組織架構(gòu)下,移動端和pc端都出現(xiàn)了組織架構(gòu)卡頓、閃退的問題,所以我們希望能夠開發(fā)一套各端共用的邏輯,統(tǒng)一維護(hù)。
第一是要抽取公共的基礎(chǔ)庫,包括boost庫、任務(wù)框架、線程管理框架等。
第二是設(shè)計(jì)公共的數(shù)據(jù)結(jié)構(gòu)。
第三,因?yàn)椴煌说木W(wǎng)絡(luò)庫差異比較大,這里不好完全共用,所以需要抽取網(wǎng)絡(luò)任務(wù)接口,由各端獨(dú)立實(shí)現(xiàn)。
具體到框架圖,我們從下往上看。底層是基礎(chǔ)庫,接著是C 實(shí)現(xiàn)的跨平臺業(yè)務(wù)層,Service層是移動端和pc端分開實(shí)現(xiàn),主要是做接口調(diào)用和回調(diào)的簡單封裝,上層則各端界面實(shí)現(xiàn)。上層界面為了兼容新舊兩套組織架構(gòu),也做了接口抽象,可以通過開關(guān)自由切換。這樣優(yōu)點(diǎn)就是有統(tǒng)一的業(yè)務(wù)邏輯代碼、DB設(shè)計(jì)和線程管理。
- 關(guān)鍵點(diǎn)
- 抽取公共基礎(chǔ)庫
- 抽象公共的數(shù)據(jù)結(jié)構(gòu)
- 抽象網(wǎng)絡(luò)層和數(shù)據(jù)庫層接口
- 優(yōu)點(diǎn)
- 統(tǒng)一的業(yè)務(wù)邏輯代碼、DB設(shè)計(jì)、線程管理
2.5 跨平臺設(shè)計(jì):架構(gòu)設(shè)計(jì)
在具體實(shí)現(xiàn)之前,我們來看看架構(gòu)設(shè)計(jì)的一些概念。
2.5.1 架構(gòu)整潔之道
業(yè)務(wù)實(shí)體和用例
關(guān)鍵業(yè)務(wù)邏輯和關(guān)鍵業(yè)務(wù)數(shù)據(jù)是緊密相關(guān)的,所以它們很適合被放在同一個對象中處理。我們將這種對象稱為“業(yè)務(wù)實(shí)體”。業(yè)務(wù)實(shí)體這個概念中應(yīng)該只有業(yè)務(wù)邏輯,沒有別的,與數(shù)據(jù)庫、用戶界面、第三方框架等內(nèi)容無關(guān)。
用例所描述的是某種特定應(yīng)用情景下的業(yè)務(wù)邏輯,可以理解為:輸入 業(yè)務(wù)實(shí)體 輸出 = 用例
軟件架構(gòu)
軟件的系統(tǒng)架構(gòu)應(yīng)該為該系統(tǒng)的用例提供支持。一個良好的架構(gòu)設(shè)計(jì)應(yīng)該圍繞著用例來展開,這樣的架構(gòu)設(shè)計(jì)可以在脫離框架、工具以及使用環(huán)境的情況下完整地描述用例。
整潔架構(gòu)
下圖的同心圓分別代表了軟件系統(tǒng)中的不同層次,越靠近中心,其所在的軟件層次就越高?;旧希鈱訄A代表的是機(jī)制,內(nèi)層圓代表的是策略。
這其中有一條貫穿整個架構(gòu)設(shè)計(jì)的規(guī)則,即依賴關(guān)系規(guī)則:
源碼中的依賴關(guān)系必須只指向同心圓的內(nèi)層,即由底層機(jī)制指向高層策略。依賴關(guān)系與數(shù)據(jù)流控制流脫鉤,而與組件所在層次掛鉤,始終從低層次指向高層次。
2.5.2 我們的架構(gòu)
我們的類圖與架構(gòu)設(shè)計(jì)概念的對應(yīng)關(guān)系如下:
- 業(yè)務(wù)實(shí)體:ArchTask
- 用例:ArchProto
- 模型層,即最外層:各種第三方框架,如DbInterface(數(shù)據(jù)庫模塊)、ArchLogicHandler(網(wǎng)絡(luò)模塊)等。我們從一次具體的業(yè)務(wù)調(diào)用流程來看看這樣設(shè)計(jì)的意義。下面是從UI發(fā)起的一次架構(gòu)更新流程,大家可以主要關(guān)注控制流是怎么穿越各層的邊界:控制流從最外層的用戶界面開始,穿過用例(Arch),最后調(diào)用最外層的組件:網(wǎng)絡(luò)模塊和數(shù)據(jù)庫模塊。但是我們源碼中的依賴方向卻都是向內(nèi)指向用例的。
這里,我們采用的是依賴反轉(zhuǎn)原則(DIP)來解決這種相反性。我們可以通過調(diào)整代碼中的接口和繼承關(guān)系,利用源碼中的依賴關(guān)系,限制控制流只能在正確的地方跨域架構(gòu)邊界。
在上面的流程圖中,主要有兩個應(yīng)用依賴反轉(zhuǎn)原則的地方:
一、CalcPreLoadArchIDs是從SyncUnitArchTask(業(yè)務(wù)實(shí)體)調(diào)用調(diào)用到ArchProto(用例)。業(yè)務(wù)實(shí)體這樣的高層概念,是無須了解像用例這樣的底層概念的。反之,底層業(yè)務(wù)用例卻需要了解高層的業(yè)務(wù)實(shí)體。所以在SyncUnitArchTask中,其實(shí)是通過調(diào)用ArchProto的接口來調(diào)用CalcPreLoadArchIDs。SyncUnitArchTask中的調(diào)用代碼如下:
arch_service_context_->CalcPreLoadArchIDs(unit_id_, arch_service_context_->GetCurrentVid(), other_unit_click_partyid_, vecHashNode, all_tmp_ids, arch_ids, ptr_map_);
ArchProto會在Task初始化時,把自己設(shè)置進(jìn)Task中,給各類型的Task反向調(diào)用。
class ArchProto : public ArchServiceContext{...};
二、最外層的模型層一般是由工具、數(shù)據(jù)庫、網(wǎng)絡(luò)框架等組成的??蚣芘c驅(qū)動程序?qū)又邪怂械膶?shí)現(xiàn)細(xì)節(jié)。從系統(tǒng)架構(gòu)的角度看,工具通常是無關(guān)緊要的,因?yàn)檫@只是一個底層的實(shí)現(xiàn)細(xì)節(jié),一種達(dá)成目標(biāo)的手段。當(dāng)Task需要調(diào)用網(wǎng)絡(luò)模塊收發(fā)請求或者調(diào)用數(shù)據(jù)庫模塊獲取數(shù)據(jù)時,為了避免內(nèi)層策略依賴外層機(jī)制,Task只會調(diào)用外層工具的接口層,而不會依賴實(shí)現(xiàn)細(xì)節(jié)。這樣的架構(gòu)設(shè)計(jì)給我們帶來的好處是,我們可以輕松替換框架,而不影響內(nèi)層策略。比如在桌面端,我們會有另外一套完全不同的網(wǎng)絡(luò)模塊實(shí)現(xiàn),只需要掛接不同的網(wǎng)絡(luò)實(shí)現(xiàn)子類,我們就可以在桌面端復(fù)用新的大架構(gòu)模塊。
良好的架構(gòu)設(shè)計(jì)應(yīng)該盡可能地允許用戶推遲和延后決定采用什么框架、數(shù)據(jù)庫、網(wǎng)絡(luò)框架以及其他與環(huán)境相關(guān)的工具。總之,良好的架構(gòu)設(shè)計(jì)應(yīng)該只關(guān)注用例,并能將它們與其他的周邊因素隔離。
2.5.3 新舊組織架構(gòu)模塊的交互
大架構(gòu)跨平臺層,跟原來的組織架構(gòu)模塊是怎么交互的呢?原來的組織架構(gòu)的數(shù)據(jù)表主要分成三部分:部門表、人員信息表、部門人員關(guān)系表,而出現(xiàn)性能問題的主要在于關(guān)系表上。所以數(shù)據(jù)設(shè)計(jì)上,人員信息保留在原組織架構(gòu)底層,部門人員關(guān)系表、部門表在大架構(gòu)底層。
- 表結(jié)構(gòu)設(shè)計(jì):
- 主要組成:人員信息表、部門表、部門人員關(guān)系表。
- 大架構(gòu)底層保存部門和部門人員關(guān)系表,人員信息保留在原組織架構(gòu)底層。
- 大架構(gòu)底層與原組織架構(gòu)底層的業(yè)務(wù)關(guān)聯(lián):
- 人員展示的部門鏈路如何獲取?—-從大架構(gòu)底層獲取,因?yàn)殛P(guān)系表存放在大架構(gòu)底層。
- 搜索如何做?—- 部門名字保存到原組織架構(gòu)底層,復(fù)用原組織架構(gòu)底層的索引建立邏輯。
2.6 雙DB切換
2.6.1 舊的讀寫表切換方式
舊方案里組織架構(gòu)的全量更新流程
當(dāng)后臺告訴客戶端需要全量更新時,客戶端會將所有節(jié)點(diǎn)標(biāo)為待刪除,然后同步后臺的節(jié)點(diǎn),清除待刪除標(biāo)記。同步完成后,將寫表的數(shù)據(jù)同步到讀表,更新版本號。最后UI就可以從讀表中讀取到最新的數(shù)據(jù)。
而之前通過用戶日志案例分析,最長的耗時主要是在將寫表的數(shù)據(jù)拷貝到讀表上面。在這個過程中,大架構(gòu)下部分用戶的日志里有更新57w節(jié)點(diǎn)的數(shù)據(jù)用了2個半小時的情況,而且這個步驟是原子操作,如果不能夠一次完成,下次還得重新執(zhí)行。
原有流程里,讀表和寫表是固定的,導(dǎo)致全量更新需要等讀表同步完數(shù)據(jù),界面才能讀到新數(shù)據(jù)。
- 分析:寫表同步數(shù)據(jù)到讀表耗時很久,當(dāng)全量更新時,如果有大量節(jié)點(diǎn)需要更新,會耗時很長。
- 缺點(diǎn):寫表和讀表固定,全量更新需要等數(shù)據(jù)同步完成,界面才能讀取到新數(shù)據(jù)。
2.6.2 新的雙DB切換方式
針對舊方案中讀寫表同步過久的問題,大架構(gòu)方案里我們換成了雙DB切換的模式。下面是我們的狀態(tài)機(jī)設(shè)計(jì)和業(yè)務(wù)代碼獲取表名的邏輯。
這樣修改之后,不需要等讀寫表同步完,UI就可以讀取到最新數(shù)據(jù)。而同步的過程可以在后臺慢慢完成,并且不會受原子性操作的限制。業(yè)務(wù)代碼獲取讀表的邏輯,也收攏到了一個函數(shù)。
因?yàn)閱挝荒J较拢總€單位的節(jié)點(diǎn)數(shù)量都不會很多,而且大多數(shù)用戶只會加載日常有交流的幾個單位,所以讀寫表同步這里,我們采用了把原表刪掉,全量拷貝的方式。
2.7 效果
對于耗時,優(yōu)化前使用全量加載的方式使得耗時很長,而優(yōu)化后采用的“本單位 骨架”的預(yù)加載邏輯使得加載耗時大幅度減小。優(yōu)化后的內(nèi)存占用大小在各場景下均有減小,通訊錄頁面的流暢度也得到了一定的提升。
一、耗時
二、CPU占用率
三、內(nèi)存占用大小
四、卡頓
作者:yecong
來源:微信公眾號:騰訊技術(shù)工程
出處:https://mp.weixin.qq.com/s/eK47AzCSSf8-W3wZdjrXXQ