本文由騰訊WXG客戶端開發工程師yecong分享,本文做了修訂和改動。
1、引言
相對於傳統的消費級IM應用,企業級IM應用的特殊之外在於它的使用者關係是按照所屬企業的組織架構來關聯的起來,而組織架構的大小是無法預設上限的,這也要求企業級IM應用在遇到真正的超大規模組織架構時,如何保證它的應用效能不受限於(或者說是儘可能不受限於)企業架構規模,這是個比較有難度的技術問題。
本文主要分享的是企業微信在百對百萬級大規模組織架構(後文簡稱大架構)時,是如何對客戶端進行效能最佳化過程的,希望帶給你啟發。
內容分成兩部分講述,第一部分是短線迭代的最佳化,主要是併發效能的最佳化。第二部分是長線迭代的最佳化,主要是從業務模式上做了根本性最佳化。
以下是相關文章,推薦一併閱讀:
《企業微信客戶端中組織架構資料的同步更新方案最佳化實戰》
《企業微信的IM架構設計揭秘:訊息模型、萬人群、已讀回執、訊息撤回等》
《釘釘——基於IM技術的新一代企業OA平臺的技術挑戰(影片+PPT) [附件下載]》
《阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處》
技術交流:
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架原始碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點此)
(本文已同步釋出於:http://www.52im.net/thread-4437-1-1.html)
2、100萬級組織架構時的效能問題
當私有化的組織架構上升到100W的量級時,出現了嚴重影響組織架構使用的問題:開啟二級部門時,載入緩慢。如圖所示,loading可能持續一分鐘以上:
3、100萬級組織架構的問題分析
我們分析一下載入二級部門的流程。
下面是載入二級部門的流程圖:
1)如果從來沒載入過該部門,需要從服務端拉取部門下的節點詳情(這裡是因為之前我們已經做了最佳化,首次登入時只拉取了部門的節點ID,沒有拉取詳情);
2)如果載入過該部門,就直接從DB讀取該部門的資料,然後返回UI展示。
當只有一條DB執行緒時,組織架構更新的任務,可能會插入到載入二級部門的任務的前面。而在百萬級別的組織架構中,全量更新的DB任務有可能比較久,全量更新的插入或者更新節點可能比較多,導致本來很快可以完成的二級部門載入任務,要排隊比較久才能執行完。下面是組織架構全量更新的流程圖:
在這裡,讀寫併發上出現了明顯的瓶頸。
原因總結如下:
1)載入二級部門和全量更新共用一條DB執行緒;
2)當全量更新大量節點時,全量更新的低優先順序任務卡住載入二級部門的高優先順序任務。
4、針對100萬級組織架構的最佳化方案
4.1基本
讀寫分離為了提高組織架構在大規模資料下的讀寫併發效能,我們開啟了wal模式,把讀寫任務分別放在不同的執行緒中執行。針對載入二級部門的流程,可以在讀執行緒中讀取部門的詳情節點,而組織架構更新可以在寫執行緒中單獨執行。由於載入二級部門的原流程是拉取資料、寫入DB、再從DB讀取資料,而且wal只支援一寫多讀,因此我們調整了快取策略,把儲存節點詳情的寫任務延遲到流程最後,優先構造了cache返回UI。這樣從DB中讀出資料的讀任務,就不需要等待儲存節點詳情的寫任務。避免了儲存節點的寫任務再次被其他寫任務阻塞,讀任務又被儲存節點的寫任務阻塞,退化成序列操作。
4.2WAL機制的原理
呼叫方修改的資料並不直接寫入到資料庫檔案中,而是寫入到另外一個稱為WAL的檔案中,然後在隨後的某個時間點被寫回到資料庫檔案中。在這個時間點的回寫操作,會降低資料庫當時的讀寫效能。但是透過設定對WAL檔案大小的限制,這種效能影響是可控的。實際上線後也沒有遇到由於checkpoint同步導致資料庫慢的反饋。
4.3快取策略
寫策略的步驟:先更新快取中的資料,再更新資料庫中的資料。
讀策略的步驟:
1)如果讀取的資料命中了快取,則直接返回資料;
2)如果讀取的資料沒有命中快取,則從資料庫中讀取資料,然後將資料寫入到快取,並且返回給UI。
4.4方案總結
5、100萬級最佳化後的效果
在最佳化前,只有52%的使用者能在1s內載入完二級部門。上線之後,93%的使用者都能在1s內開啟二級部門。耗時小於1s的使用者佔比提升40%!
6、當對面300萬組織架構時的問題
6.1概述
當業務進一步發展時,我們預估未來將要到達300W量級的組織架構。於是我們就開始提前規劃如何能在組織架構數量一直增長的情況下,還能讓組織架構流暢好用。
6.2問題
主要是:
1)選人控制元件閃退和ANR;
2)組織架構全量更新閃退。在300w的組織架構環境中,舊的組織架構載入方案,在全量更新、選人控制元件中均出現了佔用記憶體過大甚至閃退的問題。而且舊方案的載入時間會隨著節點數量的增加,不可避免地成正比增長。
6.3分析
當前方案的耗時、記憶體佔用與使用者組織架構的大小成正比,單點最佳化無法滿足組織架構持續增長的需求。
具體來說,會造成下面的一些問題:
1)選人控制元件會載入全量的組織架構ID樹,數量過多時容易發生閃退和ANR;
2)組織架構全量更新佔用記憶體過大,造成閃退。
因此,我們需要一個新的業務模式,即便總的組織架構規模一直上漲的情況下,也能維持較好的效能。
7、針對300萬組織架構的最佳化方案
比較容易想到的一個方案是web載入的模式,不儲存本地資料,但是體驗比較差,每層都會出loading。聯絡到我們的具體業務,由於私有化對不同的部門,劃分出了具有意義的獨立組織機構——單位。單位是具有管理意義的部門,不同單位可以獨立載入。而每個人,也擁有主單位和兼崗單位。所以可以按照單位載入的方式,從根本上解決目前組織架構面臨的瓶頸。按單位載入,可以簡單理解為按部門載入:
概念定義:
1)單位:政府行政組織結構中的職能部門,組建架構並承擔對應責任;
2)主單位:“我”所在的單位;
3)其他單位:除了“我”所在的其他單位;
4)骨架:通訊錄骨架包含了所有的單位節點;
5)普通部門:不屬於任何單位的部門節點。下圖是組織架構樹的示意圖:
如上圖所示:藍色節點是優先載入的本單位,灰色節點是其他單位,紅色節點是骨架。不同的單位獨立載入。
8、300萬最佳化方案中的“按單位載入”技術思路
8.1載入策略
接下來我們看看載入策略。
第一:是對自己所在的主單位(藍色節點),每次喚醒時就會更新,跟舊組織架構的邏輯類似,但是會限制拉取節點的數量。
第二:對於其他單位(灰色節點),點選到該單位時才會拉取,2個小時後會淘汰刪除,避免資料表過大。
第三:對於骨架(紅色節點),會全量載入節點ID,再拉取節點詳情。拉取策略限制了能夠拉取的節點詳情數量,如果單位節點數量超過了限制,首先拉取全量ID,再按照優先規則,拉取配置的節點詳請數量。
8.2載入流程
載入的流程是先拉取自己的單位列表,然後拉取每個單位的全量通訊錄ID,再按照後臺策略,拉取所需的詳細節點,最後拉取骨架。
如果點選到主單位:
1)如果只有ID沒有節點,會立刻拉取節點詳情返回介面;
2)如果ID和節點詳情都有,可以直接返回UI展示,然後延遲重新整理節點。
如果是點選到其他單位:可能出現ID和詳情都沒有的情況,需要拉取其他單位的節點,介面loading等待。如果是骨架:就一定有節點和詳情,只需要延遲重新整理。
9、300萬最佳化方案的分層設計思路
接下來我們看看如何分層。
在300萬量級的大規模組織架構下,移動端和pc端都出現了組織架構卡頓、閃退的問題,所以我們希望能夠開發一套各端共用的邏輯,統一維護。
第一:是要抽取公共的基礎庫,包括boost庫、任務框架、執行緒管理框架等。
第二:是設計公共的資料結構。
第三:因為不同端的網路庫差異比較大,這裡不好完全共用,所以需要抽取網路任務介面,由各端獨立實現。
具體到框架圖,我們從下往上看:
1)底層是基礎庫;
2)接著是C++實現的跨平臺業務層;
3)Service層是移動端和pc端分開實現,主要是做介面呼叫和回撥的簡單封裝;
4)上層則各端介面實現。
上層介面為了相容新舊兩套組織架構,也做了介面抽象,可以透過開關自由切換。這樣優點就是有統一的業務邏輯程式碼、DB設計和執行緒管理。
關鍵點:
1)抽取公共基礎庫;
2)抽象公共的資料結構;
3)抽象網路層和資料庫層介面。
優點:統一的業務邏輯程式碼、DB設計、執行緒管理。
10、300萬最佳化方案的整體架構設計思路
在具體實現之前,我們來看看架構設計的一些概念。
10.1架構整潔之道
1)業務實體和用例:
關鍵業務邏輯和關鍵業務資料是緊密相關的,所以它們很適合被放在同一個物件中處理。我們將這種物件稱為“業務實體”。業務實體這個概念中應該只有業務邏輯,沒有別的,與資料庫、使用者介面、第三方框架等內容無關。用例所描述的是某種特定應用情景下的業務邏輯,可以理解為:輸入 + 業務實體 + 輸出 = 用例。
2)軟體架構:
軟體的系統架構應該為該系統的用例提供支援。一個良好的架構設計應該圍繞著用例來展開,這樣的架構設計可以在脫離框架、工具以及使用環境的情況下完整地描述用例。
3)整潔架構:
下圖的同心圓分別代表了軟體系統中的不同層次,越靠近中心,其所在的軟體層次就越高。基本上,外層圓代表的是機制,內層圓代表的是策略。這其中有一條貫穿整個架構設計的規則,即依賴關係規則:
10.2我們的架構
我們的類圖與架構設計概念的對應關係如下:
1)業務實體:ArchTask;
2)用例:ArchProto;
3)模型層:即最外層,各種第三方框架,如DbInterface(資料庫模組)、ArchLogicHandler(網路模組)等。
我們從一次具體的業務呼叫流程來看看這樣設計的意義。下面是從UI發起的一次架構更新流程,大家可以主要關注控制流是怎麼穿越各層的邊界:控制流從最外層的使用者介面開始,穿過用例(Arch),最後呼叫最外層的元件:網路模組和資料庫模組。但是我們原始碼中的依賴方向卻都是向內指向用例的。這裡,我們採用的是依賴反轉原則(DIP)來解決這種相反性。我們可以透過調整程式碼中的介面和繼承關係,利用原始碼中的依賴關係,限制控制流只能在正確的地方跨域架構邊界。
在上面的流程圖中,主要有兩個應用依賴反轉原則的地方:
1)CalcPreLoadArchIDs是從SyncUnitArchTask(業務實體)呼叫呼叫到ArchProto(用例)。業務實體這樣的高層概念,是無須瞭解像用例這樣的底層概念的。反之,底層業務用例卻需要了解高層的業務實體。所以在SyncUnitArchTask中,其實是透過呼叫ArchProto的介面來呼叫CalcPreLoadArchIDs。
SyncUnitArchTask中的呼叫程式碼如下:
arch_service_context_->CalcPreLoadArchIDs(unit_id_, arch_service_context_->GetCurrentVid(), other_unit_click_partyid_, vecHashNode, all_tmp_ids, arch_ids, ptr_map_);
ArchProto會在Task初始化時,把自己設定進Task中,給各型別的Task反向呼叫。classArchProto : publicArchServiceContext{...};
2)最外層的模型層一般是由工具、資料庫、網路框架等組成的。
框架與驅動程式層中包含了所有的實現細節。從系統架構的角度看,工具通常是無關緊要的,因為這只是一個底層的實現細節,一種達成目標的手段。當Task需要呼叫網路模組收發請求或者呼叫資料庫模組獲取資料時,為了避免內層策略依賴外層機制,Task只會呼叫外層工具的介面層,而不會依賴實現細節。這樣的架構設計給我們帶來的好處是,我們可以輕鬆替換框架,而不影響內層策略。比如在桌面端,我們會有另外一套完全不同的網路模組實現,只需要掛接不同的網路實現子類,我們就可以在桌面端複用新的大架構模組。良好的架構設計應該儘可能地允許使用者推遲和延後決定採用什麼框架、資料庫、網路框架以及其他與環境相關的工具。總之,良好的架構設計應該只關注用例,並能將它們與其他的周邊因素隔離。
10.3新舊組織架構模組的互動
大架構跨平臺層,跟原來的組織架構模組是怎麼互動的呢?
原來的組織架構的資料表主要分成三部分:
1)部門表;
2)人員資訊表;
3)部門人員關係表。
而出現效能問題的主要在於關係表上。所以資料設計上,人員資訊保留在原組織架構底層,部門人員關係表、部門表在大架構底層。
表結構設計:
1)主要組成:人員資訊表、部門表、部門人員關係表;
2)大架構底層儲存部門和部門人員關係表,人員資訊保留在原組織架構底層。
大架構底層與原組織架構底層的業務關聯:
1)人員展示的部門鏈路如何獲取?從大架構底層獲取,因為關係表存放在大架構底層;
2)搜尋如何做?部門名字儲存到原組織架構底層,複用原組織架構底層的索引建立邏輯。
11、300萬最佳化方案的雙DB切換模式
11.1舊的讀寫表切換方式
舊方案裡組織架構的全量更新流程:
當後臺告訴客戶端需要全量更新時,客戶端會將所有節點標為待刪除,然後同步後臺的節點,清除待刪除標記。同步完成後,將寫表的資料同步到讀表,更新版本號。最後UI就可以從讀表中讀取到最新的資料。而之前透過使用者日誌案例分析,最長的耗時主要是在將寫表的資料複製到讀表上面。在這個過程中,大架構下部分使用者的日誌裡有更新57w節點的資料用了2個半小時的情況,而且這個步驟是原子操作,如果不能夠一次完成,下次還得重新執行。原有流程裡,讀表和寫表是固定的,導致全量更新需要等讀表同步完資料,介面才能讀到新資料。
分析:寫表同步資料到讀表耗時很久,當全量更新時,如果有大量節點需要更新,會耗時很長。
缺點:寫表和讀表固定,全量更新需要等資料同步完成,介面才能讀取到新資料。
11.2新的雙DB切換方式
針對舊方案中讀寫表同步過久的問題,大架構方案裡我們換成了雙DB切換的模式。下面是我們的狀態機設計和業務程式碼獲取表名的邏輯。這樣修改之後,不需要等讀寫表同步完,UI就可以讀取到最新資料。而同步的過程可以在後臺慢慢完成,並且不會受原子性操作的限制。業務程式碼獲取讀表的邏輯,也收攏到了一個函式。
因為單位模式下,每個單位的節點數量都不會很多,而且大多數使用者只會載入日常有交流的幾個單位,所以讀寫表同步這裡,我們採用了把原表刪掉,全量複製的方式。
12、200萬級最佳化後的效果
對於耗時,最佳化前使用全量載入的方式使得耗時很長,而最佳化後採用的“本單位+骨架”的預載入邏輯使得載入耗時大幅度減小。最佳化後的記憶體佔用大小在各場景下均有減小,通訊錄頁面的流暢度也得到了一定的提升。
耗時:
CPU佔用率:
記憶體佔用大小:
卡頓:
13、相關資料
[1] 企業微信客戶端中組織架構資料的同步更新方案最佳化實戰
[2] 企業微信的IM架構設計揭秘:訊息模型、萬人群、已讀回執、訊息撤回等
[3] 釘釘——基於IM技術的新一代企業OA平臺的技術挑戰(影片+PPT) [附件下載]》
[4] 阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處》
[5] 深度解密釘釘即時訊息服務DTIM的技術設計
[6] 深度揭密RocketMQ在釘釘IM系統中的應用實踐
[7] IM開發乾貨分享:萬字長文,詳解IM“訊息“列表卡頓最佳化實踐
[8] 手Q客戶端針對2020年春節紅包的技術實踐
[9] 移動端IM實踐:Android版微信如何大幅提升互動效能(一)
[10] 移動端IM實踐:Android版微信如何大幅提升互動效能(二)
[11] 移動端IM實踐:iOS版微信的多裝置字型適配方案探討
[12] 愛奇藝技術分享:愛奇藝Android客戶端啟動速度最佳化實踐總結
[13] 微信團隊分享:微信支付程式碼重構帶來的移動端軟體架構上的思考
(本文已同步釋出於:http://www.52im.net/thread-4437-1-1.html)