編者按:本文來自微信公眾號“InfoQ”(ID:infoqchina),作者胡騰;36氪經(jīng)授權發(fā)布。
作為企業(yè)級的微信,在業(yè)務快速發(fā)展的背景下,迭代優(yōu)化的要求也越發(fā)急迫。企業(yè)微信初版的全量同步方案在快速的業(yè)務增長面前已經(jīng)捉襟見肘,針對其遇到的問題,怎樣做好組織架構同步優(yōu)化?這是又一篇來自微信團隊的技術實戰(zhàn)。寫在前面
企業(yè)微信在快速發(fā)展過程中,陸續(xù)有大企業(yè)加入使用,企業(yè)微信初版采用全量同步方案,該方案在大企業(yè)下存在流量和性能兩方面的問題,每次同步消耗大量流量,且在 iPhone 5s 上拉取 10w+ 成員架構包解壓時會提示 memory warning 而應用崩潰。
全量同步方案難以支撐業(yè)務的快速發(fā)展,優(yōu)化同步方案越來越有必要。本文針對全量同步方案遇到的問題進行分析,提出組織架構增量同步方案,并對移動端實現(xiàn)增量同步方案的思路和重難點進行了講解。
企業(yè)微信業(yè)務背景
在企業(yè)微信中,組織架構是非常重要的模塊,用戶可以在首頁的 tab 上選擇"通訊錄"查看到本公司的組織架構,并且可以通過"通訊錄"找到本公司的所有成員,并與其發(fā)起會話或者視頻語音通話。
組織架構是非常重要且敏感的信息,企業(yè)微信作為企業(yè)級產(chǎn)品,始終把用戶隱私和安全放在重要位置。針對組織架構信息,企業(yè)管理員具有高粒度隱私保護操作權限,不僅支持個人信息隱藏,也支持通訊錄查看權限等操作。
在企業(yè)微信中,組織架構特征有:
1、多叉樹結(jié)構。葉子節(jié)點代表成員,非葉子節(jié)點代表部門。部門最多只有一個父部門,但成員可屬于多個部門。
2、架構隱藏操作。企業(yè)管理員可以在管理后臺設置白名單和黑名單,白名單可以查看完整的組織架構,其他成員在組織架構里看不到他們。黑名單的成員只能看到自己所在小組和其所有的父部門,其余人可以看到黑名單的成員。
3、組織架構操作。企業(yè)管理員可以在 web 端和 app 端添加 / 刪除部門,添加 / 刪除 / 移動 / 編輯成員等操作,并且操作結(jié)果會及時同步給本公司所有成員。
全量同步方案的問題
本節(jié)大致講解下全量同步方案實現(xiàn)以及遇到的問題。
全量同步方案原理企業(yè)微信在 1.0 時代,從穩(wěn)定性以及快速迭代的角度考慮,延用了企業(yè)郵通訊錄同步方案,采取了全量架構同步方案。
核心思想為服務端下發(fā)全量節(jié)點,客戶端對比本地數(shù)據(jù)找出變更節(jié)點。此處節(jié)點可以是用戶,也可以是部門,將組織架構視為二叉樹結(jié)構體,其下的用戶與部門均為節(jié)點,若同一個用戶存在多個部門下,被視為多個節(jié)點。
全量同步方案分為首次同步與非首次同步:
-
首次同步服務端會下發(fā)全量的節(jié)點信息的壓縮包,客戶端解壓后得到全量的架構樹并展示。
-
非首次同步分為兩步:
-
服務端下發(fā)全量節(jié)點的 hash 值??蛻舳藢Ρ缺镜財?shù)據(jù)找到刪除的節(jié)點保存在內(nèi)存中,對比找到新增的節(jié)點待請求具體信息。
-
客戶端請求新增節(jié)點的具體信息。請求具體信息成功后,再進行本地數(shù)據(jù)庫的插入 / 更新 / 刪除處理,保證同步流程的原子性。
用戶反饋
初版上線后,收到了大量的組織架構相關的 bug 投訴,主要集中在:
-
流量消耗過大。
-
客戶端架構與 web 端架構不一致。
-
組織架構同步不及時。
這些問題在大企業(yè)下更明顯。
問題剖析
深究全量同步方案難以支撐大企業(yè)同步的背后原因,皆是因為采取了服務端全量下發(fā) hash 值方案的原因,方案存在以下問題:
-
拉取大量冗余信息。即使只有一個成員信息的變化,服務端也會下發(fā)全量的 hash 節(jié)點。針對幾十萬人的大企業(yè),這樣的流量消耗是相當大的,因此在大企業(yè)要盡可能的減少更新的頻率,但是卻會導致架構數(shù)據(jù)更新不及時。
-
大企業(yè)拉取信息容易失敗。全量同步方案中首次同步架構會一次性拉取全量架構樹的壓縮包,而超大企業(yè)這個包的數(shù)據(jù)有幾十兆,解壓后幾百兆,對內(nèi)存不足的低端設備,首次加載架構可能會出現(xiàn)內(nèi)存不足而 crash。非首次同步在對比出新增的節(jié)點,請求具體信息時,可能遇到數(shù)據(jù)量過大而請求超時的情況。
-
客戶端無法過濾無效數(shù)據(jù)??蛻舳瞬焕斫?hash 值的具體含義,導致在本地對比 hash 值時不能過濾掉無效 hash 的情況,可能出現(xiàn)組織架構展示錯誤。
優(yōu)化組織架構同步方案越來越有必要。
尋找優(yōu)化思路
尋求同步方案優(yōu)化點,我們要找準原來方案的痛點以及不合理的地方,通過方案的調(diào)整來避免這個問題。
組織架構同步難點準確且耗費最少資源同步組織架構是一件很困難的事情,難點主要在:
-
組織架構架構數(shù)據(jù)量大。消息 / 聯(lián)系人同步一次的數(shù)據(jù)量一般情況不會過百,而企業(yè)微信活躍企業(yè)中有許多上萬甚至幾十萬節(jié)點的企業(yè),意味著架構一次同步的數(shù)據(jù)量很輕松就會上千上萬。移動端的流量消耗是用戶非常在乎的,且內(nèi)存有限,減少流量的消耗以及減少內(nèi)存使用并保證架構樹的完整同步是企業(yè)微信追求的目標。
-
架構規(guī)則復雜。組織架構必須同步到完整的架構樹才能展示,而且企業(yè)微信里的涉及到復雜的隱藏規(guī)則,為了安全考慮,客戶端不應該拿到隱藏的成員。
-
修改頻繁且改動大。組織架構的調(diào)整存在著新建部門且移動若干成員到新部門的情況,也存在解散某個部門的情況。而員工離職也會通過組織架構同步下來,意味著超大型企業(yè)基本上每天都會有改動。
技術選型-提出增量更新方案
上述提到的問題,在大型企業(yè)下會變得更明顯。在幾輪方案討論后,我們給原來的方案增加了兩個特性來實現(xiàn)增量更新:
-
增量。服務端記錄組織架構修改的歷史,客戶端通過版本號來增量同步架構。
-
分片。同步組織架構的接口支持傳閾值來分片拉取。
在新方案中,服務端針對某個節(jié)點的存儲結(jié)構可簡化為:
vid 是指節(jié)點用戶的唯一標識 id,departmentid 是指節(jié)點的部門 id,is_delete 表示該節(jié)點是否已被刪除。
-
若節(jié)點被刪除了,服務端不會真正的刪除該節(jié)點,而將 is_delete 標為 true。
-
若節(jié)點被更新了,服務端會增大記錄的 seq,下次客戶端來進行同步便能同步到。
其中,seq 是自增的值,可以理解成版本號。每次組織架構的節(jié)點有更新,服務端增加相應節(jié)點的 seq 值。客戶端通過一個舊的 seq 向服務器請求,服務端返回這個 seq 和 最新的 seq 之間所有的變更給客戶端,完成增量更新。
圖示為:
通過提出增量同步方案,我們從技術選型層面解決了問題,但是在實際操作中會遇到許多問題,下文中我們將針對方案原理以及實際操作中遇到的問題進行講解。
增量同步方案
本節(jié)主要講解客戶端中增量同步架構方案的原理與實現(xiàn),以及基礎概念講解。
增量同步方案原理企業(yè)微信中,增量同步方案核心思想為:
服務端下發(fā)增量節(jié)點,且支持傳閾值來分片拉取增量節(jié)點,若服務端計算不出客戶端的差量,下發(fā)全量節(jié)點由客戶端來對比差異。
增量同步方案可抽象為四步完成:
-
客戶端傳入本地版本號,拉取變更節(jié)點。
-
客戶端找到變更節(jié)點并拉取節(jié)點的具體信息。
-
客戶端處理數(shù)據(jù)并存儲版本號。
-
判斷完整架構同步是否完成,若尚未完成,重復步驟 1,若完成了完整組織架構同步,清除掉本地的同步狀態(tài)。
忽略掉各種邊界條件和異常狀況,增量同步方案的流程圖可以抽象為:
接下來我們再看看增量同步方案中的關鍵概念以及完整流程是怎樣的。
版本號同步的版本號是由多個版本號拼接成的字符串,版本號的具體含義對客戶端透明,但是對服務端非常重要。
版本號的組成部分為:
版本號回退
增量同步在實際操作過程中會遇到一些問題:
-
服務端不可能永久存儲刪除的記錄,刪除的記錄對服務端是毫無意義的而且永久存儲會占用大量的硬盤空間。而且無效數(shù)據(jù)過多也會影響架構讀取速度。當 is_delete 節(jié)點的數(shù)目超過一定的閾值后,服務端會物理刪除掉所有的 is_delete 為 true 的節(jié)點。此時客戶端會重新拉取全量的數(shù)據(jù)進行本地對比。
-
一旦架構隱藏規(guī)則變化后,服務端很難計算出增量節(jié)點,此時會下發(fā)全量節(jié)點由客戶端對比出差異。
理想狀況下,若服務端下發(fā)全量節(jié)點,客戶端鏟掉舊數(shù)據(jù),并且去拉全量節(jié)點的信息,并且用新數(shù)據(jù)覆蓋即可。但是移動端這樣做會消耗大量的用戶流量,這樣的做法是不可接受的。所以若服務端下發(fā)全量節(jié)點,客戶端需要本地對比出增刪改節(jié)點,再去拉變更節(jié)點的具體信息。
增量同步情況下,若服務端下發(fā)全量節(jié)點,我們在本文中稱這種情況為版本號回退,效果類似于客戶端用空版本號去同步架構。從統(tǒng)計結(jié)果來看,線上版本的同步中有 4% 的情況會出現(xiàn)版本號回退。
閾值分片拉取若客戶端的傳的 seq 過舊,增量數(shù)據(jù)可能很大。此時若一次性返回全部的更新數(shù)據(jù),客戶端請求的數(shù)據(jù)量會很大,時間會很長,成功率很低。針對這種場景,客戶端和服務端需要約定閾值,若請求的更新數(shù)據(jù)總數(shù)超過這個閾值,服務端每次最多返回不超過該閾值的數(shù)據(jù)。若客戶端發(fā)現(xiàn)服務端返回的數(shù)據(jù)數(shù)量等于閾值,則再次到服務端請求數(shù)據(jù),直到服務端下發(fā)的數(shù)據(jù)數(shù)量小于閾值。
節(jié)點結(jié)構體優(yōu)化在全量同步方案中,節(jié)點通過 hash 唯一標示。服務端下發(fā)的全量 hash 列表,客戶端對比本地存儲的全量 hash 列表,若有新的 hash 值則請求節(jié)點具體信息,若有刪除的 hash 值則客戶端刪除掉該節(jié)點信息。
在全量同步方案中,客戶端并不能理解 hash 值的具體含義,并且可能遇到 hash 碰撞這種極端情況導致客戶端無法正確處理下發(fā)的 hash 列表。
而增量同步方案中,使用 protobuf 結(jié)構體代替 hash 值,增量更新中節(jié)點的 proto 定義為:
在增量同步方案中,用 vid 和 partyid 來唯一標識節(jié)點,完全廢棄了 hash 值。這樣在增量同步的時候,客戶端完全理解了節(jié)點的具體含義,而且也從方案上避免了曾經(jīng)在全量同步方案遇到的 hash 值重復的異常情況。
并且在節(jié)點結(jié)構體里帶上了 seq 。節(jié)點上的 seq 來表示該節(jié)點的版本,每次節(jié)點的具體信息有更新,服務端會提高節(jié)點的 seq,客戶端發(fā)現(xiàn)服務端下發(fā)的節(jié)點 seq 比客戶端本地的 seq 大,則需要去請求節(jié)點的具體信息,避免無效的節(jié)點信息請求。
判斷完整架構同步完成因為 svr 接口支持傳閾值批量拉取變更節(jié)點,一次網(wǎng)絡操作并不意味著架構同步已經(jīng)完成。那么怎么判斷架構同步完成了呢?這里客戶端和服務端約定的方案是:
若服務端下發(fā)的(新增節(jié)點+刪除節(jié)點)小于客戶端傳的閾值,則認為架構同步結(jié)束。
當完整架構同步完成后,客戶端需要清除掉緩存,并進行一些額外的業(yè)務工作,譬如計算部門人數(shù),計算成員搜索熱度等。
增量同步方案 - 完整流程圖考慮到各種邊界條件和異常情況,增量同步方案的完整流程圖為:
增量同步方案難點
在加入增量和分片特性后,針對幾十萬人的超大企業(yè),在版本號回退的場景,怎樣保證架構同步的完整性和方案選擇成為了難點。
前文提到,隱藏規(guī)則變更以及后臺物理刪除無效節(jié)點后,客戶端若用很舊的版本同步,服務端算不出增量節(jié)點,此時服務端會下發(fā)全量節(jié)點,客戶端需要本地對比所有數(shù)據(jù)找出變更節(jié)點,該場景可以理解為版本號回退。在這種場景下,對于幾十萬節(jié)點的超大型企業(yè),若服務端下發(fā)的增量節(jié)點過多,客戶端請求的時間會很長,成功率會很低,因此需要分片拉取增量節(jié)點。而且拉取下來的全量節(jié)點,客戶端處理不能請求全量節(jié)點的具體信息覆蓋舊數(shù)據(jù),這樣的話每次版本號回退的場景流量消耗過大。
因此,針對幾十萬節(jié)點的超大型企業(yè)的增量同步,客戶端難點在于:
-
斷點續(xù)傳。增量同步過程中,若客戶端遇到網(wǎng)絡問題或應用中止了,在下次網(wǎng)絡或應用恢復時,能夠接著上次同步的進度繼續(xù)同步。
-
同步過程中不影響正常展示。超大型企業(yè)同步的耗時可能較長,同步的時候不應影響正常的組織架構展示。
-
控制同步耗時。超大型企業(yè)版本號回退的場景同步非常耗時,但是我們需要想辦法加快處理速度,減少同步的消耗時間。
思路
-
架構同步開始,將架構樹緩存在內(nèi)存中,加快處理速度。
-
若服務端端下發(fā)了需要版本號回退的 flag,本地將 db 中的節(jié)點信息做一次備份操作。
-
將服務端端下發(fā)的所有 update 節(jié)點,在架構樹中查詢,若找到了,則將備份數(shù)據(jù)轉(zhuǎn)為正式數(shù)據(jù)。若找不到,則為新增節(jié)點,需要拉取具體信息并保存在架構樹中。
-
當完整架構同步結(jié)束后,在 db 中找到并刪除掉所有備份節(jié)點,清除掉緩存和同步狀態(tài)。
若服務端下發(fā)了全量節(jié)點,客戶端的處理時序圖為:
服務端下發(fā)版本號回退標記
從時序圖中可以看出,服務端下發(fā)的版本號回退標記是很重要的信號。
而版本號回退這個標記,僅僅在同步的首次會隨著新的版本號而下發(fā)。在完整架構同步期間,客戶端需要將該標記緩存,并且跟著版本號一起存在數(shù)據(jù)庫中。在完整架構同步結(jié)束后,需要根據(jù)是否版本號回退來決定刪除掉數(shù)據(jù)庫中的待刪除節(jié)點。
備份架構樹方案架構樹備份最直接的方案是將 db 中數(shù)據(jù) copy 一份,并存在新表里。如果在數(shù)據(jù)量很小的情況下,這樣做是完全沒有問題的,但是架構樹的節(jié)點往往很多,采取這樣簡單粗暴的方案在移動端是完全不可取的,在幾十萬人的企業(yè)里,這樣做會造成極大的性能問題。
經(jīng)過考慮后,企業(yè)微信采取的方案是:
-
若同步架構時,后臺下發(fā)了需要版本號回退的 flag,客戶端將緩存和 db 中的所有節(jié)點標為待刪除(時序圖中 8,9 步)。
-
針對服務端下發(fā)的更新節(jié)點,在架構樹中清除掉節(jié)點的待刪除標記(時序圖中 10,11 步)。
-
在完整架構同步結(jié)束后,在 db 中找到并刪除掉所有標為待刪除的節(jié)點(時序圖中 13 步),并且清除掉所有緩存數(shù)據(jù)。
而且,在增量同步過程中,不應該影響正常的架構樹展示。所以在架構同步過程中,若有上層來請求 db 中的數(shù)據(jù),則需要過濾掉有待刪除標記的節(jié)點。
緩存架構樹方案決定客戶端避免不了全量節(jié)點對比,將重要的信息緩存到內(nèi)存中會大大加快處理速度。內(nèi)存中的架構樹節(jié)點體定義為:
此處我們用 std::map 來緩存架構樹,用 std::pair 作為 key。我們在比較節(jié)點的時候,會涉及到很多查詢操作,使用 map 查詢的時間復雜度僅為 O(logn)。
增量同步方案關鍵點
本節(jié)單獨將優(yōu)化同步方案中關鍵點拿出來寫,這些關鍵點不僅僅適用于本文架構同步,也適用于大多數(shù)同步邏輯。
保證數(shù)據(jù)處理完成后,再儲存版本號
在幾乎所有的同步中,版本號都是重中之重,一旦版本號亂掉,后果非常嚴重。
在架構同步中,最最重要的一點是:
保證數(shù)據(jù)處理完成后,再儲存版本號。
在組織架構同步的場景下,為什么不能先存版本號,再存數(shù)據(jù)呢?
這涉及到組織架構同步數(shù)據(jù)的一個重要特征:架構節(jié)點數(shù)據(jù)是可重復拉取并覆蓋的。
考慮下實際操作中遇到的真實場景:
-
若客戶端已經(jīng)向服務端請求了新增節(jié)點信息,客戶端此時剛剛插入了新增節(jié)點,還未儲存版本號,客戶端應用中止了。
-
此時客戶端重新啟動,又會用相同版本號拉下剛剛已經(jīng)處理過的節(jié)點,而這些節(jié)點跟本地數(shù)據(jù)對比后,會發(fā)現(xiàn)節(jié)點的 seq 并未更新而不會再去拉節(jié)點信息,也不會造成節(jié)點重復。
若一旦先存版本號再存具體數(shù)據(jù),一定會有概率丟失架構更新數(shù)據(jù)。
同步的原子性正常情況下,一次同步的邏輯可以簡化為:
在企業(yè)微信的組織架構同步中存在異步操作,若進行同步的過程不保證原子性,極大可能出現(xiàn)下圖所示的情況:
該圖中,同步的途中插入了另外一次同步,很容易造成問題:
-
輸出結(jié)果不穩(wěn)定。若兩次同步幾乎同時開始,但因為存在網(wǎng)絡波動等情況,返回結(jié)果可能不同,給調(diào)試造成極大的困擾。
-
中間狀態(tài)錯亂。若同步中處理服務端返回的結(jié)果會依賴于請求同步時的某個中間狀態(tài),而新的同步發(fā)起時又會重置這個狀態(tài),很可能會引起匪夷所思的異常。
-
時序錯亂。整個同步流程應該是原子的,若中間插入了其他同步的流程會造成整個同步流程時序混亂,引發(fā)異常。
怎樣保證同步的原子性呢?
我們可以在開始同步的時候記一個 flag 表示正在同步,在結(jié)束同步時,清除掉該 flag。若另外一次同步到來時,發(fā)現(xiàn)正在同步,則可以直接舍棄掉本次同步,或者等本次同步成功后再進行一次同步。
此外也可將同步串行化,保證同步的時序,多次同步的時序應該是 FIFO 的。
緩存數(shù)據(jù)一致性移動端同步過程中的緩存多分為兩種:
-
內(nèi)存緩存。加入內(nèi)存緩存的目的是減少文件 IO 操作,加快程序處理速度。
-
磁盤緩存。加入磁盤緩存是為了防止程序中止時丟失掉同步狀態(tài)。
內(nèi)存緩存多緩存同步時的數(shù)據(jù)以及同步的中間狀態(tài),磁盤緩存用于緩存同步的中間狀態(tài)防止緩存狀態(tài)丟失。
在整個同步過程中,我們都必須保證緩存中的數(shù)據(jù)和數(shù)據(jù)庫的數(shù)據(jù)的更改需要一一對應。在增量同步的情況中,我們每次需要更新 / 刪除數(shù)據(jù)庫中的節(jié)點,都需要更新相應的緩存信息,來保證數(shù)據(jù)的一致性。
優(yōu)化數(shù)據(jù)對比
內(nèi)存使用
測試方法:使用工具 Instrument,用同一賬號監(jiān)控全量同步和增量同步分別在首次加載架構時的 App 內(nèi)存峰值。
內(nèi)存峰值測試結(jié)果
分析
隨著架構的節(jié)點增多,全量同步方案的內(nèi)存峰值會一直攀升,在極限情況下,會出現(xiàn)內(nèi)存不足應用程序 crash 的情況(實際測試中,30w 節(jié)點下,iPhone 6 全量同步方案必 crash)。而增量同步方案中,總節(jié)點的多少并不會影響內(nèi)存峰值,僅僅會增加同步分片的次數(shù)。
優(yōu)化后,在騰訊域下,增量同步方案的 App 總內(nèi)存使用僅為全量同步方案的 53.1%,且企業(yè)越大優(yōu)化效果越明顯。并且不論架構的總節(jié)點數(shù)有多少,增量同步方案都能將完整架構同步下來,達到了預期的效果。
流量使用測試方法:在管理端對成員做增加操作五次,通過日志分析客戶端消耗流量,取其平均值。日志會打印出請求的 header 和 body 大小并估算出流量使用值。
測試結(jié)果
分析
增加成員操作,針對增量同步方案僅僅會新拉單個成員的信息,所以無論架構里有多少人,流量消耗都是相近的。同樣的操作針對全量同步方案,每次請求變更,服務端都會下發(fā)全量 hash 列表,企業(yè)越大消耗的流量越多??梢钥吹?,當企業(yè)的節(jié)點數(shù)達到 20w 級別時,全量同步方案的流量消耗是增量同步方案的近 500 倍。
優(yōu)化后,在騰訊域下,每次增量同步流量消耗僅為全量同步方案的 0.4%,且企業(yè)越大優(yōu)化效果越明顯。
寫在最后
增量同步方案從方案上避免了架構同步不及時以及流量消耗過大的問題。通過用戶反饋和數(shù)據(jù)分析,增量架構同步上線后運行穩(wěn)定,達到了理想的優(yōu)化效果。
作者介紹:胡騰,騰訊工程師,參與企業(yè)微信從無到有的整個過程,目前主要負責企業(yè)微信移動端組織架構和外部聯(lián)系人等模塊的開發(fā)工作。
編者按:本文來自微信公眾號“InfoQ”(ID:infoqchina),作者胡騰;36氪經(jīng)授權發(fā)布。
作為企業(yè)級的微信,在業(yè)務快速發(fā)展的背景下,迭代優(yōu)化的要求也越發(fā)急迫。企業(yè)微信初版的全量同步方案在快速的業(yè)務增長面前已經(jīng)捉襟見肘,針對其遇到的問題,怎樣做好組織架構同步優(yōu)化?這是又一篇來自微信團隊的技術實戰(zhàn)。
寫在前面
企業(yè)微信在快速發(fā)展過程中,陸續(xù)有大企業(yè)加入使用,企業(yè)微信初版采用全量同步方案,該方案在大企業(yè)下存在流量和性能兩方面的問題,每次同步消耗大量流量,且在 iPhone 5s 上拉取 10w+ 成員架構包解壓時會提示 memory warning 而應用崩潰。
全量同步方案難以支撐業(yè)務的快速發(fā)展,優(yōu)化同步方案越來越有必要。本文針對全量同步方案遇到的問題進行分析,提出組織架構增量同步方案,并對移動端實現(xiàn)增量同步方案的思路和重難點進行了講解。
企業(yè)微信業(yè)務背景
在企業(yè)微信中,組織架構是非常重要的模塊,用戶可以在首頁的 tab 上選擇"通訊錄"查看到本公司的組織架構,并且可以通過"通訊錄"找到本公司的所有成員,并與其發(fā)起會話或者視頻語音通話。
組織架構是非常重要且敏感的信息,企業(yè)微信作為企業(yè)級產(chǎn)品,始終把用戶隱私和安全放在重要位置。針對組織架構信息,企業(yè)管理員具有高粒度隱私保護操作權限,不僅支持個人信息隱藏,也支持通訊錄查看權限等操作。
在企業(yè)微信中,組織架構特征有:
1、多叉樹結(jié)構。葉子節(jié)點代表成員,非葉子節(jié)點代表部門。部門最多只有一個父部門,但成員可屬于多個部門。
2、架構隱藏操作。企業(yè)管理員可以在管理后臺設置白名單和黑名單,白名單可以查看完整的組織架構,其他成員在組織架構里看不到他們。黑名單的成員只能看到自己所在小組和其所有的父部門,其余人可以看到黑名單的成員。
3、組織架構操作。企業(yè)管理員可以在 web 端和 app 端添加 / 刪除部門,添加 / 刪除 / 移動 / 編輯成員等操作,并且操作結(jié)果會及時同步給本公司所有成員。
全量同步方案的問題
本節(jié)大致講解下全量同步方案實現(xiàn)以及遇到的問題。
全量同步方案原理
企業(yè)微信在 1.0 時代,從穩(wěn)定性以及快速迭代的角度考慮,延用了企業(yè)郵通訊錄同步方案,采取了全量架構同步方案。
核心思想為服務端下發(fā)全量節(jié)點,客戶端對比本地數(shù)據(jù)找出變更節(jié)點。此處節(jié)點可以是用戶,也可以是部門,將組織架構視為二叉樹結(jié)構體,其下的用戶與部門均為節(jié)點,若同一個用戶存在多個部門下,被視為多個節(jié)點。
全量同步方案分為首次同步與非首次同步:
首次同步服務端會下發(fā)全量的節(jié)點信息的壓縮包,客戶端解壓后得到全量的架構樹并展示。
非首次同步分為兩步:
服務端下發(fā)全量節(jié)點的 hash 值??蛻舳藢Ρ缺镜財?shù)據(jù)找到刪除的節(jié)點保存在內(nèi)存中,對比找到新增的節(jié)點待請求具體信息。
客戶端請求新增節(jié)點的具體信息。請求具體信息成功后,再進行本地數(shù)據(jù)庫的插入 / 更新 / 刪除處理,保證同步流程的原子性。
用戶反饋
初版上線后,收到了大量的組織架構相關的 bug 投訴,主要集中在:
流量消耗過大。
客戶端架構與 web 端架構不一致。
組織架構同步不及時。
這些問題在大企業(yè)下更明顯。
問題剖析
深究全量同步方案難以支撐大企業(yè)同步的背后原因,皆是因為采取了服務端全量下發(fā) hash 值方案的原因,方案存在以下問題:
拉取大量冗余信息。即使只有一個成員信息的變化,服務端也會下發(fā)全量的 hash 節(jié)點。針對幾十萬人的大企業(yè),這樣的流量消耗是相當大的,因此在大企業(yè)要盡可能的減少更新的頻率,但是卻會導致架構數(shù)據(jù)更新不及時。
大企業(yè)拉取信息容易失敗。全量同步方案中首次同步架構會一次性拉取全量架構樹的壓縮包,而超大企業(yè)這個包的數(shù)據(jù)有幾十兆,解壓后幾百兆,對內(nèi)存不足的低端設備,首次加載架構可能會出現(xiàn)內(nèi)存不足而 crash。非首次同步在對比出新增的節(jié)點,請求具體信息時,可能遇到數(shù)據(jù)量過大而請求超時的情況。
客戶端無法過濾無效數(shù)據(jù)??蛻舳瞬焕斫?hash 值的具體含義,導致在本地對比 hash 值時不能過濾掉無效 hash 的情況,可能出現(xiàn)組織架構展示錯誤。
優(yōu)化組織架構同步方案越來越有必要。
尋找優(yōu)化思路
尋求同步方案優(yōu)化點,我們要找準原來方案的痛點以及不合理的地方,通過方案的調(diào)整來避免這個問題。
組織架構同步難點
準確且耗費最少資源同步組織架構是一件很困難的事情,難點主要在:
組織架構架構數(shù)據(jù)量大。消息 / 聯(lián)系人同步一次的數(shù)據(jù)量一般情況不會過百,而企業(yè)微信活躍企業(yè)中有許多上萬甚至幾十萬節(jié)點的企業(yè),意味著架構一次同步的數(shù)據(jù)量很輕松就會上千上萬。移動端的流量消耗是用戶非常在乎的,且內(nèi)存有限,減少流量的消耗以及減少內(nèi)存使用并保證架構樹的完整同步是企業(yè)微信追求的目標。
架構規(guī)則復雜。組織架構必須同步到完整的架構樹才能展示,而且企業(yè)微信里的涉及到復雜的隱藏規(guī)則,為了安全考慮,客戶端不應該拿到隱藏的成員。
修改頻繁且改動大。組織架構的調(diào)整存在著新建部門且移動若干成員到新部門的情況,也存在解散某個部門的情況。而員工離職也會通過組織架構同步下來,意味著超大型企業(yè)基本上每天都會有改動。
技術選型-提出增量更新方案
上述提到的問題,在大型企業(yè)下會變得更明顯。在幾輪方案討論后,我們給原來的方案增加了兩個特性來實現(xiàn)增量更新:
增量。服務端記錄組織架構修改的歷史,客戶端通過版本號來增量同步架構。
分片。同步組織架構的接口支持傳閾值來分片拉取。
在新方案中,服務端針對某個節(jié)點的存儲結(jié)構可簡化為:
vid 是指節(jié)點用戶的唯一標識 id,departmentid 是指節(jié)點的部門 id,is_delete 表示該節(jié)點是否已被刪除。
若節(jié)點被刪除了,服務端不會真正的刪除該節(jié)點,而將 is_delete 標為 true。
若節(jié)點被更新了,服務端會增大記錄的 seq,下次客戶端來進行同步便能同步到。
其中,seq 是自增的值,可以理解成版本號。每次組織架構的節(jié)點有更新,服務端增加相應節(jié)點的 seq 值??蛻舳送ㄟ^一個舊的 seq 向服務器請求,服務端返回這個 seq 和 最新的 seq 之間所有的變更給客戶端,完成增量更新。
圖示為:
通過提出增量同步方案,我們從技術選型層面解決了問題,但是在實際操作中會遇到許多問題,下文中我們將針對方案原理以及實際操作中遇到的問題進行講解。
增量同步方案
本節(jié)主要講解客戶端中增量同步架構方案的原理與實現(xiàn),以及基礎概念講解。
增量同步方案原理
企業(yè)微信中,增量同步方案核心思想為:
服務端下發(fā)增量節(jié)點,且支持傳閾值來分片拉取增量節(jié)點,若服務端計算不出客戶端的差量,下發(fā)全量節(jié)點由客戶端來對比差異。
增量同步方案可抽象為四步完成:
客戶端傳入本地版本號,拉取變更節(jié)點。
客戶端找到變更節(jié)點并拉取節(jié)點的具體信息。
客戶端處理數(shù)據(jù)并存儲版本號。
判斷完整架構同步是否完成,若尚未完成,重復步驟 1,若完成了完整組織架構同步,清除掉本地的同步狀態(tài)。
忽略掉各種邊界條件和異常狀況,增量同步方案的流程圖可以抽象為:
接下來我們再看看增量同步方案中的關鍵概念以及完整流程是怎樣的。
版本號
同步的版本號是由多個版本號拼接成的字符串,版本號的具體含義對客戶端透明,但是對服務端非常重要。
版本號的組成部分為:
版本號回退
增量同步在實際操作過程中會遇到一些問題:
服務端不可能永久存儲刪除的記錄,刪除的記錄對服務端是毫無意義的而且永久存儲會占用大量的硬盤空間。而且無效數(shù)據(jù)過多也會影響架構讀取速度。當 is_delete 節(jié)點的數(shù)目超過一定的閾值后,服務端會物理刪除掉所有的 is_delete 為 true 的節(jié)點。此時客戶端會重新拉取全量的數(shù)據(jù)進行本地對比。
一旦架構隱藏規(guī)則變化后,服務端很難計算出增量節(jié)點,此時會下發(fā)全量節(jié)點由客戶端對比出差異。
理想狀況下,若服務端下發(fā)全量節(jié)點,客戶端鏟掉舊數(shù)據(jù),并且去拉全量節(jié)點的信息,并且用新數(shù)據(jù)覆蓋即可。但是移動端這樣做會消耗大量的用戶流量,這樣的做法是不可接受的。所以若服務端下發(fā)全量節(jié)點,客戶端需要本地對比出增刪改節(jié)點,再去拉變更節(jié)點的具體信息。
增量同步情況下,若服務端下發(fā)全量節(jié)點,我們在本文中稱這種情況為版本號回退,效果類似于客戶端用空版本號去同步架構。從統(tǒng)計結(jié)果來看,線上版本的同步中有 4% 的情況會出現(xiàn)版本號回退。
閾值分片拉取
若客戶端的傳的 seq 過舊,增量數(shù)據(jù)可能很大。此時若一次性返回全部的更新數(shù)據(jù),客戶端請求的數(shù)據(jù)量會很大,時間會很長,成功率很低。針對這種場景,客戶端和服務端需要約定閾值,若請求的更新數(shù)據(jù)總數(shù)超過這個閾值,服務端每次最多返回不超過該閾值的數(shù)據(jù)。若客戶端發(fā)現(xiàn)服務端返回的數(shù)據(jù)數(shù)量等于閾值,則再次到服務端請求數(shù)據(jù),直到服務端下發(fā)的數(shù)據(jù)數(shù)量小于閾值。
節(jié)點結(jié)構體優(yōu)化
在全量同步方案中,節(jié)點通過 hash 唯一標示。服務端下發(fā)的全量 hash 列表,客戶端對比本地存儲的全量 hash 列表,若有新的 hash 值則請求節(jié)點具體信息,若有刪除的 hash 值則客戶端刪除掉該節(jié)點信息。
在全量同步方案中,客戶端并不能理解 hash 值的具體含義,并且可能遇到 hash 碰撞這種極端情況導致客戶端無法正確處理下發(fā)的 hash 列表。
而增量同步方案中,使用 protobuf 結(jié)構體代替 hash 值,增量更新中節(jié)點的 proto 定義為:
在增量同步方案中,用 vid 和 partyid 來唯一標識節(jié)點,完全廢棄了 hash 值。這樣在增量同步的時候,客戶端完全理解了節(jié)點的具體含義,而且也從方案上避免了曾經(jīng)在全量同步方案遇到的 hash 值重復的異常情況。
并且在節(jié)點結(jié)構體里帶上了 seq 。節(jié)點上的 seq 來表示該節(jié)點的版本,每次節(jié)點的具體信息有更新,服務端會提高節(jié)點的 seq,客戶端發(fā)現(xiàn)服務端下發(fā)的節(jié)點 seq 比客戶端本地的 seq 大,則需要去請求節(jié)點的具體信息,避免無效的節(jié)點信息請求。
判斷完整架構同步完成
因為 svr 接口支持傳閾值批量拉取變更節(jié)點,一次網(wǎng)絡操作并不意味著架構同步已經(jīng)完成。那么怎么判斷架構同步完成了呢?這里客戶端和服務端約定的方案是:
若服務端下發(fā)的(新增節(jié)點+刪除節(jié)點)小于客戶端傳的閾值,則認為架構同步結(jié)束。
當完整架構同步完成后,客戶端需要清除掉緩存,并進行一些額外的業(yè)務工作,譬如計算部門人數(shù),計算成員搜索熱度等。
增量同步方案 - 完整流程圖
考慮到各種邊界條件和異常情況,增量同步方案的完整流程圖為:
增量同步方案難點
在加入增量和分片特性后,針對幾十萬人的超大企業(yè),在版本號回退的場景,怎樣保證架構同步的完整性和方案選擇成為了難點。
前文提到,隱藏規(guī)則變更以及后臺物理刪除無效節(jié)點后,客戶端若用很舊的版本同步,服務端算不出增量節(jié)點,此時服務端會下發(fā)全量節(jié)點,客戶端需要本地對比所有數(shù)據(jù)找出變更節(jié)點,該場景可以理解為版本號回退。在這種場景下,對于幾十萬節(jié)點的超大型企業(yè),若服務端下發(fā)的增量節(jié)點過多,客戶端請求的時間會很長,成功率會很低,因此需要分片拉取增量節(jié)點。而且拉取下來的全量節(jié)點,客戶端處理不能請求全量節(jié)點的具體信息覆蓋舊數(shù)據(jù),這樣的話每次版本號回退的場景流量消耗過大。
因此,針對幾十萬節(jié)點的超大型企業(yè)的增量同步,客戶端難點在于:
斷點續(xù)傳。增量同步過程中,若客戶端遇到網(wǎng)絡問題或應用中止了,在下次網(wǎng)絡或應用恢復時,能夠接著上次同步的進度繼續(xù)同步。
同步過程中不影響正常展示。超大型企業(yè)同步的耗時可能較長,同步的時候不應影響正常的組織架構展示。
控制同步耗時。超大型企業(yè)版本號回退的場景同步非常耗時,但是我們需要想辦法加快處理速度,減少同步的消耗時間。
思路
架構同步開始,將架構樹緩存在內(nèi)存中,加快處理速度。
若服務端端下發(fā)了需要版本號回退的 flag,本地將 db 中的節(jié)點信息做一次備份操作。
將服務端端下發(fā)的所有 update 節(jié)點,在架構樹中查詢,若找到了,則將備份數(shù)據(jù)轉(zhuǎn)為正式數(shù)據(jù)。若找不到,則為新增節(jié)點,需要拉取具體信息并保存在架構樹中。
當完整架構同步結(jié)束后,在 db 中找到并刪除掉所有備份節(jié)點,清除掉緩存和同步狀態(tài)。
若服務端下發(fā)了全量節(jié)點,客戶端的處理時序圖為:
服務端下發(fā)版本號回退標記
從時序圖中可以看出,服務端下發(fā)的版本號回退標記是很重要的信號。
而版本號回退這個標記,僅僅在同步的首次會隨著新的版本號而下發(fā)。在完整架構同步期間,客戶端需要將該標記緩存,并且跟著版本號一起存在數(shù)據(jù)庫中。在完整架構同步結(jié)束后,需要根據(jù)是否版本號回退來決定刪除掉數(shù)據(jù)庫中的待刪除節(jié)點。
備份架構樹方案
架構樹備份最直接的方案是將 db 中數(shù)據(jù) copy 一份,并存在新表里。如果在數(shù)據(jù)量很小的情況下,這樣做是完全沒有問題的,但是架構樹的節(jié)點往往很多,采取這樣簡單粗暴的方案在移動端是完全不可取的,在幾十萬人的企業(yè)里,這樣做會造成極大的性能問題。
經(jīng)過考慮后,企業(yè)微信采取的方案是:
若同步架構時,后臺下發(fā)了需要版本號回退的 flag,客戶端將緩存和 db 中的所有節(jié)點標為待刪除(時序圖中 8,9 步)。
針對服務端下發(fā)的更新節(jié)點,在架構樹中清除掉節(jié)點的待刪除標記(時序圖中 10,11 步)。
在完整架構同步結(jié)束后,在 db 中找到并刪除掉所有標為待刪除的節(jié)點(時序圖中 13 步),并且清除掉所有緩存數(shù)據(jù)。
而且,在增量同步過程中,不應該影響正常的架構樹展示。所以在架構同步過程中,若有上層來請求 db 中的數(shù)據(jù),則需要過濾掉有待刪除標記的節(jié)點。
緩存架構樹
方案決定客戶端避免不了全量節(jié)點對比,將重要的信息緩存到內(nèi)存中會大大加快處理速度。內(nèi)存中的架構樹節(jié)點體定義為:
此處我們用 std::map 來緩存架構樹,用 std::pair 作為 key。我們在比較節(jié)點的時候,會涉及到很多查詢操作,使用 map 查詢的時間復雜度僅為 O(logn)。
增量同步方案關鍵點
本節(jié)單獨將優(yōu)化同步方案中關鍵點拿出來寫,這些關鍵點不僅僅適用于本文架構同步,也適用于大多數(shù)同步邏輯。
保證數(shù)據(jù)處理完成后,再儲存版本號
在幾乎所有的同步中,版本號都是重中之重,一旦版本號亂掉,后果非常嚴重。
在架構同步中,最最重要的一點是:
保證數(shù)據(jù)處理完成后,再儲存版本號。
在組織架構同步的場景下,為什么不能先存版本號,再存數(shù)據(jù)呢?
這涉及到組織架構同步數(shù)據(jù)的一個重要特征:架構節(jié)點數(shù)據(jù)是可重復拉取并覆蓋的。
考慮下實際操作中遇到的真實場景:
若客戶端已經(jīng)向服務端請求了新增節(jié)點信息,客戶端此時剛剛插入了新增節(jié)點,還未儲存版本號,客戶端應用中止了。
此時客戶端重新啟動,又會用相同版本號拉下剛剛已經(jīng)處理過的節(jié)點,而這些節(jié)點跟本地數(shù)據(jù)對比后,會發(fā)現(xiàn)節(jié)點的 seq 并未更新而不會再去拉節(jié)點信息,也不會造成節(jié)點重復。
若一旦先存版本號再存具體數(shù)據(jù),一定會有概率丟失架構更新數(shù)據(jù)。
同步的原子性
正常情況下,一次同步的邏輯可以簡化為:
在企業(yè)微信的組織架構同步中存在異步操作,若進行同步的過程不保證原子性,極大可能出現(xiàn)下圖所示的情況:
該圖中,同步的途中插入了另外一次同步,很容易造成問題:
輸出結(jié)果不穩(wěn)定。若兩次同步幾乎同時開始,但因為存在網(wǎng)絡波動等情況,返回結(jié)果可能不同,給調(diào)試造成極大的困擾。
中間狀態(tài)錯亂。若同步中處理服務端返回的結(jié)果會依賴于請求同步時的某個中間狀態(tài),而新的同步發(fā)起時又會重置這個狀態(tài),很可能會引起匪夷所思的異常。
時序錯亂。整個同步流程應該是原子的,若中間插入了其他同步的流程會造成整個同步流程時序混亂,引發(fā)異常。
怎樣保證同步的原子性呢?
我們可以在開始同步的時候記一個 flag 表示正在同步,在結(jié)束同步時,清除掉該 flag。若另外一次同步到來時,發(fā)現(xiàn)正在同步,則可以直接舍棄掉本次同步,或者等本次同步成功后再進行一次同步。
此外也可將同步串行化,保證同步的時序,多次同步的時序應該是 FIFO 的。
緩存數(shù)據(jù)一致性
移動端同步過程中的緩存多分為兩種:
內(nèi)存緩存。加入內(nèi)存緩存的目的是減少文件 IO 操作,加快程序處理速度。
磁盤緩存。加入磁盤緩存是為了防止程序中止時丟失掉同步狀態(tài)。
內(nèi)存緩存多緩存同步時的數(shù)據(jù)以及同步的中間狀態(tài),磁盤緩存用于緩存同步的中間狀態(tài)防止緩存狀態(tài)丟失。
在整個同步過程中,我們都必須保證緩存中的數(shù)據(jù)和數(shù)據(jù)庫的數(shù)據(jù)的更改需要一一對應。在增量同步的情況中,我們每次需要更新 / 刪除數(shù)據(jù)庫中的節(jié)點,都需要更新相應的緩存信息,來保證數(shù)據(jù)的一致性。
優(yōu)化數(shù)據(jù)對比
內(nèi)存使用
測試方法:使用工具 Instrument,用同一賬號監(jiān)控全量同步和增量同步分別在首次加載架構時的 App 內(nèi)存峰值。
內(nèi)存峰值測試結(jié)果
分析
隨著架構的節(jié)點增多,全量同步方案的內(nèi)存峰值會一直攀升,在極限情況下,會出現(xiàn)內(nèi)存不足應用程序 crash 的情況(實際測試中,30w 節(jié)點下,iPhone 6 全量同步方案必 crash)。而增量同步方案中,總節(jié)點的多少并不會影響內(nèi)存峰值,僅僅會增加同步分片的次數(shù)。
優(yōu)化后,在騰訊域下,增量同步方案的 App 總內(nèi)存使用僅為全量同步方案的 53.1%,且企業(yè)越大優(yōu)化效果越明顯。并且不論架構的總節(jié)點數(shù)有多少,增量同步方案都能將完整架構同步下來,達到了預期的效果。
流量使用
測試方法:在管理端對成員做增加操作五次,通過日志分析客戶端消耗流量,取其平均值。日志會打印出請求的 header 和 body 大小并估算出流量使用值。
測試結(jié)果
分析
增加成員操作,針對增量同步方案僅僅會新拉單個成員的信息,所以無論架構里有多少人,流量消耗都是相近的。同樣的操作針對全量同步方案,每次請求變更,服務端都會下發(fā)全量 hash 列表,企業(yè)越大消耗的流量越多??梢钥吹?,當企業(yè)的節(jié)點數(shù)達到 20w 級別時,全量同步方案的流量消耗是增量同步方案的近 500 倍。
優(yōu)化后,在騰訊域下,每次增量同步流量消耗僅為全量同步方案的 0.4%,且企業(yè)越大優(yōu)化效果越明顯。
寫在最后
增量同步方案從方案上避免了架構同步不及時以及流量消耗過大的問題。通過用戶反饋和數(shù)據(jù)分析,增量架構同步上線后運行穩(wěn)定,達到了理想的優(yōu)化效果。
作者介紹:胡騰,騰訊工程師,參與企業(yè)微信從無到有的整個過程,目前主要負責企業(yè)微信移動端組織架構和外部聯(lián)系人等模塊的開發(fā)工作。