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