《企业微信组织架构同步优化的思路与实操演练》要点:
本文介绍了企业微信组织架构同步优化的思路与实操演练,希望对您有用。如果有疑问,可以联系我们。
作者:胡腾
编辑:小智
作为企业级的微信,在业务快速发展的背景下,迭代优化的要求也越发急迫.企业微信初版的全量同步方案在快速的业务增长面前已经捉襟见肘,针对其遇到的问题,怎样做好组织架构同步优化?这是又一篇来自微信团队的技术实战.
企业微信在快速发展过程中,陆续有大企业加入使用,企业微信初版采用全量同步方案,该方案在大企业下存在流量和性能两方面的问题,每次同步消耗大量流量,且在 iPhone 5s 上拉取 10w+ 成员架构包解压时会提示 memory warning 而应用崩溃.
全量同步方案难以支撑业务的快速发展,优化同步方案越来越有必要.本文针对全量同步方案遇到的问题进行分析,提出组织架构增量同步方案,并对移动端实现增量同步方案的思路和重难点进行了讲解.
在企业微信中,组织架构是非常重要的模块,用户可以在首页的 tab 上选择”通讯录”查看到本公司的组织架构,并且可以通过”通讯录”找到本公司的所有成员,并与其发起会话或者视频语音通话.
组织架构是非常重要且敏感的信息,企业微信作为企业级产品,始终把用户隐私和安全放在重要位置.针对组织架构信息,企业管理员具有高粒度隐私保护操作权限,不仅支持个人信息隐藏,也支持通讯录查看权限等操作.
在企业微信中,组织架构特征有:
1、多叉树结构.叶子节点代表成员,非叶子节点代表部门.部门最多只有一个父部门,但成员可属于多个部门.
2、架构隐藏操作.企业管理员可以在管理后台设置白名单和黑名单,白名单可以查看完整的组织架构,其他成员在组织架构里看不到他们.黑名单的成员只能看到自己所在小组和其所有的父部门,其余人可以看到黑名单的成员.
3、组织架构操作.企业管理员可以在 web 端和 app 端添加 / 删除部门,添加 / 删除 / 移动 / 编辑成员等操作,并且操作结果会及时同步给本公司所有成员.
本节大致讲解下全量同步方案实现以及遇到的问题.
企业微信在 1.0 时代,从稳定性以及快速迭代的角度考虑,延用了企业邮通讯录同步方案,采取了全量架构同步方案.
核心思想为服务端下发全量节点,客户端对比本地数据找出变更节点.此处节点可以是用户,也可以是部门,将组织架构视为二叉树结构体,其下的用户与部门均为节点,若同一个用户存在多个部门下,被视为多个节点.
全量同步方案分为首次同步与非首次同步:
初版上线后,收到了大量的组织架构相关的 bug 投诉,主要集中在:
这些问题在大企业下更明显.
深究全量同步方案难以支撑大企业同步的背后原因,皆是因为采取了服务端全量下发 hash 值方案的原因,方案存在以下问题:
优化组织架构同步方案越来越有必要.
寻求同步方案优化点,我们要找准原来方案的痛点以及不合理的地方,通过方案的调整来避免这个问题.
准确且耗费最少资源同步组织架构是一件很困难的事情,难点主要在:
上述提到的问题,在大型企业下会变得更明显.在几轮方案讨论后,我们给原来的方案增加了两个特性来实现增量更新:
在新方案中,服务端针对某个节点的存储结构可简化为:
vid 是指节点用户的唯一标识 id,departmentid 是指节点的部门 id,is_delete 表示该节点是否已被删除.
其中,seq 是自增的值,可以理解成版本号.每次组织架构的节点有更新,服务端增加相应节点的 seq 值.客户端通过一个旧的 seq 向服务器请求,服务端返回这个 seq 和 最新的 seq 之间所有的变更给客户端,完成增量更新.
图示为:
通过提出增量同步方案,我们从技术选型层面解决了问题,但是在实际操作中会遇到许多问题,下文中我们将针对方案原理以及实际操作中遇到的问题进行讲解.
本节主要讲解客户端中增量同步架构方案的原理与实现,以及基础概念讲解.
企业微信中,增量同步方案核心思想为:
服务端下发增量节点,且支持传阈值来分片拉取增量节点,若服务端计算不出客户端的差量,下发全量节点由客户端来对比差异.
增量同步方案可抽象为四步完成:
忽略掉各种边界条件和异常状况,增量同步方案的流程图可以抽象为:
接下来我们再看看增量同步方案中的关键概念以及完整流程是怎样的.
同步的版本号是由多个版本号拼接成的字符串,版本号的具体含义对客户端透明,但是对服务端非常重要.
版本号的组成部分为:
增量同步在实际操作过程中会遇到一些问题:
理想状况下,若服务端下发全量节点,客户端铲掉旧数据,并且去拉全量节点的信息,并且用新数据覆盖即可.但是移动端这样做会消耗大量的用户流量,这样的做法是不可接受的.所以若服务端下发全量节点,客户端需要本地对比出增删改节点,再去拉变更节点的具体信息.
增量同步情况下,若服务端下发全量节点,我们在本文中称这种情况为版本号回退,效果类似于客户端用空版本号去同步架构.从统计结果来看,线上版本的同步中有 4% 的情况会出现版本号回退.
若客户端的传的 seq 过旧,增量数据可能很大.此时若一次性返回全部的更新数据,客户端请求的数据量会很大,时间会很长,成功率很低.针对这种场景,客户端和服务端需要约定阈值,若请求的更新数据总数超过这个阈值,服务端每次最多返回不超过该阈值的数据.若客户端发现服务端返回的数据数量等于阈值,则再次到服务端请求数据,直到服务端下发的数据数量小于阈值.
在全量同步方案中,节点通过 hash 唯一标示.服务端下发的全量 hash 列表,客户端对比本地存储的全量 hash 列表,若有新的 hash 值则请求节点具体信息,若有删除的 hash 值则客户端删除掉该节点信息.
在全量同步方案中,客户端并不能理解 hash 值的具体含义,并且可能遇到 hash 碰撞这种极端情况导致客户端无法正确处理下发的 hash 列表.
而增量同步方案中,使用 protobuf 结构体代替 hash 值,增量更新中节点的 proto 定义为:
在增量同步方案中,用 vid 和 partyid 来唯一标识节点,完全废弃了 hash 值.这样在增量同步的时候,客户端完全理解了节点的具体含义,而且也从方案上避免了曾经在全量同步方案遇到的 hash 值重复的异常情况.
并且在节点结构体里带上了 seq .节点上的 seq 来表示该节点的版本,每次节点的具体信息有更新,服务端会提高节点的 seq,客户端发现服务端下发的节点 seq 比客户端本地的 seq 大,则需要去请求节点的具体信息,避免无效的节点信息请求.
因为 svr 接口支持传阈值批量拉取变更节点,一次网络操作并不意味着架构同步已经完成.那么怎么判断架构同步完成了呢?这里客户端和服务端约定的方案是:
若服务端下发的(新增节点+删除节点)小于客户端传的阈值,则认为架构同步结束.
当完整架构同步完成后,客户端需要清除掉缓存,并进行一些额外的业务工作,譬如计算部门人数,计算成员搜索热度等.
增量同步方案 – 完整流程图
考虑到各种边界条件和异常情况,增量同步方案的完整流程图为:
在加入增量和分片特性后,针对几十万人的超大企业,在版本号回退的场景,怎样保证架构同步的完整性和方案选择成为了难点.
前文提到,隐藏规则变更以及后台物理删除无效节点后,客户端若用很旧的版本同步,服务端算不出增量节点,此时服务端会下发全量节点,客户端需要本地对比所有数据找出变更节点,该场景可以理解为版本号回退.在这种场景下,对于几十万节点的超大型企业,若服务端下发的增量节点过多,客户端请求的时间会很长,成功率会很低,因此需要分片拉取增量节点.而且拉取下来的全量节点,客户端处理不能请求全量节点的具体信息覆盖旧数据,这样的话每次版本号回退的场景流量消耗过大.
因此,针对几十万节点的超大型企业的增量同步,客户端难点在于:
若服务端下发了全量节点,客户端的处理时序图为:
从时序图中可以看出,服务端下发的版本号回退标记是很重要的信号.
而版本号回退这个标记,仅仅在同步的首次会随着新的版本号而下发.在完整架构同步期间,客户端需要将该标记缓存,并且跟着版本号一起存在数据库中.在完整架构同步结束后,需要根据是否版本号回退来决定删除掉数据库中的待删除节点.
架构树备份最直接的方案是将 db 中数据 copy 一份,并存在新表里.如果在数据量很小的情况下,这样做是完全没有问题的,但是架构树的节点往往很多,采取这样简单粗暴的方案在移动端是完全不可取的,在几十万人的企业里,这样做会造成极大的性能问题.
经过考虑后,企业微信采取的方案是:
而且,在增量同步过程中,不应该影响正常的架构树展示.所以在架构同步过程中,若有上层来请求 db 中的数据,则需要过滤掉有待删除标记的节点.
方案决定客户端避免不了全量节点对比,将重要的信息缓存到内存中会大大加快处理速度.内存中的架构树节点体定义为:
此处我们用 std::map 来缓存架构树,用 std::pair 作为 key.我们在比较节点的时候,会涉及到很多查询操作,使用 map 查询的时间复杂度仅为 O(logn).
本节单独将优化同步方案中关键点拿出来写,这些关键点不仅仅适用于本文架构同步,也适用于大多数同步逻辑.
保证数据处理完成后,再储存版本号
在几乎所有的同步中,版本号都是重中之重,一旦版本号乱掉,后果非常严重.
在架构同步中,最最重要的一点是:
保证数据处理完成后,再储存版本号
在组织架构同步的场景下,为什么不能先存版本号,再存数据呢?
这涉及到组织架构同步数据的一个重要特征:架构节点数据是可重复拉取并覆盖的.
考虑下实际操作中遇到的真实场景:
若一旦先存版本号再存具体数据,一定会有概率丢失架构更新数据.
正常情况下,一次同步的逻辑可以简化为:
在企业微信的组织架构同步中存在异步操作,若进行同步的过程不保证原子性,极大可能出现下图所示的情况:
该图中,同步的途中插入了另外一次同步,很容易造成问题:
怎样保证同步的原子性呢?
我们可以在开始同步的时候记一个 flag 表示正在同步,在结束同步时,清除掉该 flag.若另外一次同步到来时,发现正在同步,则可以直接舍弃掉本次同步,或者等本次同步成功后再进行一次同步.
此外也可将同步串行化,保证同步的时序,多次同步的时序应该是 FIFO 的.
移动端同步过程中的缓存多分为两种:
内存缓存多缓存同步时的数据以及同步的中间状态,磁盘缓存用于缓存同步的中间状态防止缓存状态丢失.
在整个同步过程中,我们都必须保证缓存中的数据和数据库的数据的更改需要一一对应.在增量同步的情况中,我们每次需要更新 / 删除数据库中的节点,都需要更新相应的缓存信息,来保证数据的一致性.
测试方法:使用工具 Instrument,用同一账号监控全量同步和增量同步分别在首次加载架构时的 App 内存峰值.
内存峰值测试结果
分析
随着架构的节点增多,全量同步方案的内存峰值会一直攀升,在极限情况下,会出现内存不足应用程序 crash 的情况(实际测试中,30w 节点下,iPhone 6 全量同步方案必 crash).而增量同步方案中,总节点的多少并不会影响内存峰值,仅仅会增加同步分片的次数.
优化后,在腾讯域下,增量同步方案的 App 总内存使用仅为全量同步方案的 53.1%,且企业越大优化效果越明显.并且不论架构的总节点数有多少,增量同步方案都能将完整架构同步下来,达到了预期的效果.
测试方法:在管理端对成员做增加操作五次,通过日志分析客户端消耗流量,取其平均值.日志会打印出请求的 header 和 body 大小并估算出流量使用值.
测试结果
分析
增加成员操作,针对增量同步方案仅仅会新拉单个成员的信息,所以无论架构里有多少人,流量消耗都是相近的.同样的操作针对全量同步方案,每次请求变更,服务端都会下发全量 hash 列表,企业越大消耗的流量越多.可以看到,当企业的节点数达到 20w 级别时,全量同步方案的流量消耗是增量同步方案的近 500 倍.
优化后,在腾讯域下,每次增量同步流量消耗仅为全量同步方案的 0.4%,且企业越大优化效果越明显.
增量同步方案从方案上避免了架构同步不及时以及流量消耗过大的问题.通过用户反馈和数据分析,增量架构同步上线后运行稳定,达到了理想的优化效果.
胡腾,腾讯工程师,参与企业微信从无到有的整个过程,目前主要负责企业微信移动端组织架构和外部联系人等模块的开发工作.
文章来自微信公众号:InfoQ
转载请注明本页网址:
http://www.vephp.com/jiaocheng/3742.html