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