企业微信组织架构同步优化的思路与实操演练

  

做者|胡腾web

编辑|小智数据库

做为企业级的微信,在业务快速发展的背景下,迭代优化的要求也愈加急迫。企业微信第一版的全量同步方案在快速的业务增加面前已经捉襟见肘,针对其遇到的问题,怎样作好组织架构同步优化?这是又一篇来自微信团队的技术实战。缓存

写在前面安全

企业微信在快速发展过程当中,陆续有大企业加入使用,企业微信第一版采用全量同步方案,该方案在大企业下存在流量和性能两方面的问题,每次同步消耗大量流量,且在 iPhone 5s 上拉取 10w+ 成员架构包解压时会提示 memory warning 而应用崩溃。服务器

全量同步方案难以支撑业务的快速发展,优化同步方案愈来愈有必要。本文针对全量同步方案遇到的问题进行分析,提出组织架构增量同步方案,并对移动端实现增量同步方案的思路和重难点进行了讲解。微信

企业微信业务背景网络

在企业微信中,组织架构是很是重要的模块,用户能够在首页的 tab 上选择"通信录"查看到本公司的组织架构,而且能够经过"通信录"找到本公司的全部成员,并与其发起会话或者视频语音通话。架构

组织架构是很是重要且敏感的信息,企业微信做为企业级产品,始终把用户隐私和安全放在重要位置。针对组织架构信息,企业管理员具备高粒度隐私保护操做权限,不只支持我的信息隐藏,也支持通信录查看权限等操做。app

在企业微信中,组织架构特征有:异步

一、多叉树结构。叶子节点表明成员,非叶子节点表明部门。部门最多只有一个父部门,但成员可属于多个部门。

二、架构隐藏操做。企业管理员能够在管理后台设置白名单和黑名单,白名单能够查看完整的组织架构,其余成员在组织架构里看不到他们。黑名单的成员只能看到本身所在小组和其全部的父部门,其他人能够看到黑名单的成员。

三、组织架构操做。企业管理员能够在 web 端和 app 端添加 / 删除部门,添加 / 删除 / 移动 / 编辑成员等操做,而且操做结果会及时同步给本公司全部成员。

全量同步方案的问题

本节大体讲解下全量同步方案实现以及遇到的问题。

全量同步方案原理

企业微信在 1.0 时代,从稳定性以及快速迭代的角度考虑,延用了企业邮通信录同步方案,采起了全量架构同步方案。

核心思想为服务端下发全量节点,客户端对比本地数据找出变动节点。此处节点能够是用户,也能够是部门,将组织架构视为二叉树结构体,其下的用户与部门均为节点,若同一个用户存在多个部门下,被视为多个节点。

全量同步方案分为首次同步与非首次同步:

  • 首次同步服务端会下发全量的节点信息的压缩包,客户端解压后获得全量的架构树并展现。

  • 非首次同步分为两步:

    1. 服务端下发全量节点的 hash 值。客户端对比本地数据找到删除的节点保存在内存中,对比找到新增的节点待请求具体信息。

    2. 客户端请求新增节点的具体信息。请求具体信息成功后,再进行本地数据库的插入 / 更新 / 删除处理,保证同步流程的原子性。

用户反馈

第一版上线后,收到了大量的组织架构相关的 bug 投诉,主要集中在:

  • 流量消耗过大。

  • 客户端架构与 web 端架构不一致。

  • 组织架构同步不及时。

这些问题在大企业下更明显。

问题剖析

深究全量同步方案难以支撑大企业同步的背后缘由,皆是由于采起了服务端全量下发 hash 值方案的缘由,方案存在如下问题:

  1. 拉取大量冗余信息。即便只有一个成员信息的变化,服务端也会下发全量的 hash 节点。针对几十万人的大企业,这样的流量消耗是至关大的,所以在大企业要尽量的减小更新的频率,可是却会致使架构数据更新不及时。

  2. 大企业拉取信息容易失败。全量同步方案中首次同步架构会一次性拉取全量架构树的压缩包,而超大企业这个包的数据有几十兆,解压后几百兆,对内存不足的低端设备,首次加载架构可能会出现内存不足而 crash。非首次同步在对比出新增的节点,请求具体信息时,可能遇到数据量过大而请求超时的状况。

  3. 客户端没法过滤无效数据。客户端不理解 hash 值的具体含义,致使在本地对比 hash 值时不能过滤掉无效 hash 的状况,可能出现组织架构展现错误。

优化组织架构同步方案愈来愈有必要。

寻找优化思路

寻求同步方案优化点,咱们要找准原来方案的痛点以及不合理的地方,经过方案的调整来避免这个问题。

组织架构同步难点

准确且耗费最少资源同步组织架构是一件很困难的事情,难点主要在:

  • 组织架构架构数据量大。消息 / 联系人同步一次的数据量通常状况不会过百,而企业微信活跃企业中有许多上万甚至几十万节点的企业,意味着架构一次同步的数据量很轻松就会上千上万。移动端的流量消耗是用户很是在意的,且内存有限,减小流量的消耗以及减小内存使用并保证架构树的完整同步是企业微信追求的目标。

  • 架构规则复杂。组织架构必须同步到完整的架构树才能展现,并且企业微信里的涉及到复杂的隐藏规则,为了安全考虑,客户端不该该拿到隐藏的成员。

  • 修改频繁且改动大。组织架构的调整存在着新建部门且移动若干成员到新部门的状况,也存在解散某个部门的状况。而员工离职也会经过组织架构同步下来,意味着超大型企业基本上天天都会有改动。

技术选型-提出增量更新方案

上述提到的问题,在大型企业下会变得更明显。在几轮方案讨论后,咱们给原来的方案增长了两个特性来实现增量更新:

  1. 增量。服务端记录组织架构修改的历史,客户端经过版本号来增量同步架构。

  2. 分片。同步组织架构的接口支持传阈值来分片拉取。

在新方案中,服务端针对某个节点的存储结构可简化为:

vid 是指节点用户的惟一标识 id,departmentid 是指节点的部门 id,is_delete 表示该节点是否已被删除。

  • 若节点被删除了,服务端不会真正的删除该节点,而将 is_delete 标为 true。

  • 若节点被更新了,服务端会增大记录的 seq,下次客户端来进行同步便能同步到。

其中,seq 是自增的值,能够理解成版本号。每次组织架构的节点有更新,服务端增长相应节点的 seq 值。客户端经过一个旧的 seq 向服务器请求,服务端返回这个 seq 和 最新的 seq 之间全部的变动给客户端,完成增量更新。

图示为:

经过提出增量同步方案,咱们从技术选型层面解决了问题,可是在实际操做中会遇到许多问题,下文中咱们将针对方案原理以及实际操做中遇到的问题进行讲解。

增量同步方案

本节主要讲解客户端中增量同步架构方案的原理与实现,以及基础概念讲解。

增量同步方案原理

企业微信中,增量同步方案核心思想为:

服务端下发增量节点,且支持传阈值来分片拉取增量节点,若服务端计算不出客户端的差量,下发全量节点由客户端来对比差别。

增量同步方案可抽象为四步完成:

  1. 客户端传入本地版本号,拉取变动节点。

  2. 客户端找到变动节点并拉取节点的具体信息。

  3. 客户端处理数据并存储版本号。

  4. 判断完整架构同步是否完成,若还没有完成,重复步骤 1,若完成了完整组织架构同步,清除掉本地的同步状态。

忽略掉各类边界条件和异常情况,增量同步方案的流程图能够抽象为:

接下来咱们再看看增量同步方案中的关键概念以及完整流程是怎样的。

版本号

同步的版本号是由多个版本号拼接成的字符串,版本号的具体含义对客户端透明,可是对服务端很是重要。

版本号的组成部分为:

版本号回退

增量同步在实际操做过程当中会遇到一些问题:

  1. 服务端不可能永久存储删除的记录,删除的记录对服务端是毫无心义的并且永久存储会占用大量的硬盘空间。并且无效数据过多也会影响架构读取速度。当 is_delete 节点的数目超过必定的阈值后,服务端会物理删除掉全部的 is_delete 为 true 的节点。此时客户端会从新拉取全量的数据进行本地对比。

  2. 一旦架构隐藏规则变化后,服务端很难计算出增量节点,此时会下发全量节点由客户端对比出差别。

理想情况下,若服务端下发全量节点,客户端铲掉旧数据,而且去拉全量节点的信息,而且用新数据覆盖便可。可是移动端这样作会消耗大量的用户流量,这样的作法是不可接受的。因此若服务端下发全量节点,客户端须要本地对比出增删改节点,再去拉变动节点的具体信息。

增量同步状况下,若服务端下发全量节点,咱们在本文中称这种状况为版本号回退,效果相似于客户端用空版本号去同步架构。从统计结果来看,线上版本的同步中有 4% 的状况会出现版本号回退。

阈值分片拉取

若客户端的传的 seq 过旧,增量数据可能很大。此时若一次性返回所有的更新数据,客户端请求的数据量会很大,时间会很长,成功率很低。针对这种场景,客户端和服务端须要约定阈值,若请求的更新数据总数超过这个阈值,服务端每次最多返回不超过该阈值的数据。若客户端发现服务端返回的数据数量等于阈值,则再次到服务端请求数据,直到服务端下发的数据数量小于阈值。

节点结构体优化

在全量同步方案中,节点经过 hash 惟一标示。服务端下发的全量 hash 列表,客户端对比本地存储的全量 hash 列表,如有新的 hash 值则请求节点具体信息,如有删除的 hash 值则客户端删除掉该节点信息。

在全量同步方案中,客户端并不能理解 hash 值的具体含义,而且可能遇到 hash 碰撞这种极端状况致使客户端没法正确处理下发的 hash 列表。

而增量同步方案中,使用 protobuf 结构体代替 hash 值,增量更新中节点的 proto 定义为:

在增量同步方案中,用 vid 和 partyid 来惟一标识节点,彻底废弃了 hash 值。这样在增量同步的时候,客户端彻底理解了节点的具体含义,并且也从方案上避免了曾经在全量同步方案遇到的 hash 值重复的异常状况。

而且在节点结构体里带上了 seq 。节点上的 seq 来表示该节点的版本,每次节点的具体信息有更新,服务端会提升节点的 seq,客户端发现服务端下发的节点 seq 比客户端本地的 seq 大,则须要去请求节点的具体信息,避免无效的节点信息请求。

判断完整架构同步完成

由于 svr 接口支持传阈值批量拉取变动节点,一次网络操做并不意味着架构同步已经完成。那么怎么判断架构同步完成了呢?这里客户端和服务端约定的方案是:

若服务端下发的(新增节点+删除节点)小于客户端传的阈值,则认为架构同步结束。

当完整架构同步完成后,客户端须要清除掉缓存,并进行一些额外的业务工做,譬如计算部门人数,计算成员搜索热度等。

增量同步方案 - 完整流程图

考虑到各类边界条件和异常状况,增量同步方案的完整流程图为:

增量同步方案难点

在加入增量和分片特性后,针对几十万人的超大企业,在版本号回退的场景,怎样保证架构同步的完整性和方案选择成为了难点。

前文提到,隐藏规则变动以及后台物理删除无效节点后,客户端若用很旧的版本同步,服务端算不出增量节点,此时服务端会下发全量节点,客户端须要本地对比全部数据找出变动节点,该场景能够理解为版本号回退。在这种场景下,对于几十万节点的超大型企业,若服务端下发的增量节点过多,客户端请求的时间会很长,成功率会很低,所以须要分片拉取增量节点。并且拉取下来的全量节点,客户端处理不能请求全量节点的具体信息覆盖旧数据,这样的话每次版本号回退的场景流量消耗过大。

所以,针对几十万节点的超大型企业的增量同步,客户端难点在于:

  1. 断点续传。增量同步过程当中,若客户端遇到网络问题或应用停止了,在下次网络或应用恢复时,可以接着上次同步的进度继续同步。

  2. 同步过程当中不影响正常展现。超大型企业同步的耗时可能较长,同步的时候不该影响正常的组织架构展现。

  3. 控制同步耗时。超大型企业版本号回退的场景同步很是耗时,可是咱们须要想办法加快处理速度,减小同步的消耗时间。

思路

  1. 架构同步开始,将架构树缓存在内存中,加快处理速度。

  2. 若服务端端下发了须要版本号回退的 flag,本地将 db 中的节点信息作一次备份操做。

  3. 将服务端端下发的全部 update 节点,在架构树中查询,若找到了,则将备份数据转为正式数据。若找不到,则为新增节点,须要拉取具体信息并保存在架构树中。

  4. 当完整架构同步结束后,在 db 中找到并删除掉全部备份节点,清除掉缓存和同步状态。

若服务端下发了全量节点,客户端的处理时序图为:

服务端下发版本号回退标记

从时序图中能够看出,服务端下发的版本号回退标记是很重要的信号。

而版本号回退这个标记,仅仅在同步的首次会随着新的版本号而下发。在完整架构同步期间,客户端须要将该标记缓存,而且跟着版本号一块儿存在数据库中。在完整架构同步结束后,须要根据是否版本号回退来决定删除掉数据库中的待删除节点。

备份架构树方案

架构树备份最直接的方案是将 db 中数据 copy 一份,并存在新表里。若是在数据量很小的状况下,这样作是彻底没有问题的,可是架构树的节点每每不少,采起这样简单粗暴的方案在移动端是彻底不可取的,在几十万人的企业里,这样作会形成极大的性能问题。

通过考虑后,企业微信采起的方案是:

  1. 若同步架构时,后台下发了须要版本号回退的 flag,客户端将缓存和 db 中的全部节点标为待删除(时序图中 8,9 步)。

  2. 针对服务端下发的更新节点,在架构树中清除掉节点的待删除标记(时序图中 10,11 步)。

  3. 在完整架构同步结束后,在 db 中找到并删除掉全部标为待删除的节点(时序图中 13 步),而且清除掉全部缓存数据。

并且,在增量同步过程当中,不该该影响正常的架构树展现。因此在架构同步过程当中,如有上层来请求 db 中的数据,则须要过滤掉有待删除标记的节点。

缓存架构树

方案决定客户端避免不了全量节点对比,将重要的信息缓存到内存中会大大加快处理速度。内存中的架构树节点体定义为:

此处咱们用 std::map 来缓存架构树,用 std::pair 做为 key。咱们在比较节点的时候,会涉及到不少查询操做,使用 map 查询的时间复杂度仅为 O(logn)。

增量同步方案关键点

本节单独将优化同步方案中关键点拿出来写,这些关键点不只仅适用于本文架构同步,也适用于大多数同步逻辑。

保证数据处理完成后,再储存版本号

在几乎全部的同步中,版本号都是重中之重,一旦版本号乱掉,后果很是严重。

在架构同步中,最最重要的一点是:

保证数据处理完成后,再储存版本号。

在组织架构同步的场景下,为何不能先存版本号,再存数据呢?

这涉及到组织架构同步数据的一个重要特征:架构节点数据是可重复拉取并覆盖的。

考虑下实际操做中遇到的真实场景:

  1. 若客户端已经向服务端请求了新增节点信息,客户端此时刚刚插入了新增节点,还未储存版本号,客户端应用停止了。

  2. 此时客户端从新启动,又会用相同版本号拉下刚刚已经处理过的节点,而这些节点跟本地数据对比后,会发现节点的 seq 并未更新而不会再去拉节点信息,也不会形成节点重复。

若一旦先存版本号再存具体数据,必定会有几率丢失架构更新数据。

同步的原子性

正常状况下,一次同步的逻辑能够简化为:

在企业微信的组织架构同步中存在异步操做,若进行同步的过程不保证原子性,极大可能出现下图所示的状况:

该图中,同步的途中插入了另一次同步,很容易形成问题:

  1. 输出结果不稳定。若两次同步几乎同时开始,但由于存在网络波动等状况,返回结果可能不一样,给调试形成极大的困扰。

  2. 中间状态错乱。若同步中处理服务端返回的结果会依赖于请求同步时的某个中间状态,而新的同步发起时又会重置这个状态,极可能会引发匪夷所思的异常。

  3. 时序错乱。整个同步流程应该是原子的,若中间插入了其余同步的流程会形成整个同步流程时序混乱,引起异常。

怎样保证同步的原子性呢?

咱们能够在开始同步的时候记一个 flag 表示正在同步,在结束同步时,清除掉该 flag。若另一次同步到来时,发现正在同步,则能够直接舍弃掉本次同步,或者等本次同步成功后再进行一次同步。

此外也可将同步串行化,保证同步的时序,屡次同步的时序应该是 FIFO 的。

缓存数据一致性

移动端同步过程当中的缓存多分为两种:

  1. 内存缓存。加入内存缓存的目的是减小文件 IO 操做,加快程序处理速度。

  2. 磁盘缓存。加入磁盘缓存是为了防止程序停止时丢失掉同步状态。

内存缓存多缓存同步时的数据以及同步的中间状态,磁盘缓存用于缓存同步的中间状态防止缓存状态丢失。

在整个同步过程当中,咱们都必须保证缓存中的数据和数据库的数据的更改须要一一对应。在增量同步的状况中,咱们每次须要更新 / 删除数据库中的节点,都须要更新相应的缓存信息,来保证数据的一致性。

优化数据对比

内存使用

测试方法:使用工具 Instrument,用同一帐号监控全量同步和增量同步分别在首次加载架构时的 App 内存峰值。

内存峰值测试结果

分析

随着架构的节点增多,全量同步方案的内存峰值会一直攀升,在极限状况下,会出现内存不足应用程序 crash 的状况(实际测试中,30w 节点下,iPhone 6 全量同步方案必 crash)。而增量同步方案中,总节点的多少并不会影响内存峰值,仅仅会增长同步分片的次数。

优化后,在腾讯域下,增量同步方案的 App 总内存使用仅为全量同步方案的 53.1%,且企业越大优化效果越明显。而且不论架构的总节点数有多少,增量同步方案都能将完整架构同步下来,达到了预期的效果。

流量使用

测试方法:在管理端对成员作增长操做五次,经过日志分析客户端消耗流量,取其平均值。日志会打印出请求的 header 和 body 大小并估算出流量使用值。

测试结果

分析

增长成员操做,针对增量同步方案仅仅会新拉单个成员的信息,因此不管架构里有多少人,流量消耗都是相近的。一样的操做针对全量同步方案,每次请求变动,服务端都会下发全量 hash 列表,企业越大消耗的流量越多。能够看到,当企业的节点数达到 20w 级别时,全量同步方案的流量消耗是增量同步方案的近 500 倍。

优化后,在腾讯域下,每次增量同步流量消耗仅为全量同步方案的 0.4%,且企业越大优化效果越明显。

写在最后

增量同步方案从方案上避免了架构同步不及时以及流量消耗过大的问题。经过用户反馈和数据分析,增量架构同步上线后运行稳定,达到了理想的优化效果。