前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >揭秘企业微信如何优化满足ToB新挑战?

揭秘企业微信如何优化满足ToB新挑战?

作者头像
腾讯大讲堂
发布2021-07-06 15:40:17
1.3K0
发布2021-07-06 15:40:17
举报

作者:潘唐磊 腾讯WXG开发工程师

导语| 本文主要总结了企业微信消息系统的架构与设计,阐述了toB业务带来的一些难点,面临哪些挑战,以及设计方案的对比与分析。同时总结了后台开发的一些常用手段,实用于消息系统,解决相关问题。

名词解释

  • seq:自增长的序列号,每条消息对应一个
  • ImUnion:消息互通系统,用于企业微信与微信的消息打通
  • 控制消息:不可见消息,复用消息通道的一种可靠通知机制
  • 应用类消息:系统应用下发的消息
  • api消息:第三方应用下发的消息
  • appinfo:每条消息对应的唯一strid,全局唯一。同一条消息的appinfo在所有的接收方中是相同的

01

背景

企业微信作为一款办公协同的产品,消息收发是最基础的功能。消息系统的稳定性、可靠性、安全性尤其重要。消息系统的构建与设计的过程中,面临着较多的难点。toB场景的消息系统,需要支持更为复杂的业务场景:

  • 发消息的鉴权逻辑复杂:关系类型有群关系、同企业同事关系、好友关系、集团企业关系、圈子企业关系。收发消息双方需存在至少一种关系才允许发消息。
  • 回执消息:每条消息都需记录已读和未读人员列表,涉及频繁的状态读写操作
  • 撤回消息:支持24小时的有效期撤回动作
  • 消息云端存储:存储时间跨度长,最长可支持180天消息存储,数百TB用户消息需优化,减少机器成本
  • 万人群的群聊:群人数上限可支持10000人,一个群就像一个小型的ddos攻击
  • 与微信消息互通:两个异构的im系统直接打通,可靠性和一致性尤其重要

02

整体架构介绍

一、分层架构

接入层:统一入口,接收客户端的请求,根据类型转发到对应的CGI层。客户端可以通过长连或者短连连接wwproxy。活跃的客户端,优先用长连接发起请求,如果长连失败,则选用短连重试。

CGI层:http服务。接收wwproxy的数据包,校验用户的session状态,并用后台派发的秘钥去解包,如解密失败则拒绝请求。解密成功,则把明文包体转发到后端逻辑层对应的svr。

逻辑层:大量的微服务和异步处理服务,使用自研的hikit rpc框架,svr之间使用tcp短连进行通信。进行数据整合和逻辑处理。和外部系统的通信,通过http协议,包括微信互通、手机厂商的推送平台等。

存储层:消息存储是采用的是基于levelDB模型开发msgkv。SeqSvr是序列号生成器,保证派发的seq单调递增不回退,用于消息的收发协议。

二、消息收发模型

企业微信的消息收发模型采用了推拉方式,这种方式可靠性高,设计简单。以下是消息推拉的时序图。发送方请求后台,把消息写入到接收方的存储,然后push通知接收方。接受方收到push,主动上来后台收消息。

不重复,不丢失,及时触达,这三个是消息系统的核心指标:

  • 客户端通过与后台建立长连接,保证消息push的实时触达。
  • 如果客户端长连接不在,进程被kill了,利用手机厂商的推送平台,推送通知,或者直接拉起进程进行收消息。
  • 假如遇到消息洪峰,后台的push滞后,客户端有轮训机制进行兜底,保证消息可达。
  • 为了防止消息丢失,只要后台逻辑层接收到请求,保证消息写到接收方的存储,失败则重试。如果请求在CGI层就失败,则返回给客户端出消息红点
  • 消息排重。客户端在弱网络的场景下,有可能请求已经成功写入存储,回包超时,导致客户端重试发起相同的消息,那么就造成消息重复。为了避免这种情况发生,每条消息都会生成唯一的appinfo,后台通过建立索引进行排重,相同的消息直接返回成功,保证存储只有一条。

三、消息扩散写

IM通信工具的消息存储,一般有两种方式,扩散读扩散写

扩散读:每条消息只存一份,群聊成员都读取同一份数据

优点:节省存储容量

缺点:①每个用户需存储会话列表,通过会话id去拉取会话消息②收消息的协议复杂,每个会话都需要增量同步消息,则每个会话都需要维护一个序列号

扩散写:每条消息存多份,每个群聊成员在自己的存储都有一份

优点:①只需要通过一个序列号就可以增量同步所有消息,收消息协议简单②读取速度快,前端体验好③满足更多ToB的业务场景:回执消息、云端删除。同一条消息,在每个人的视角会有不同的表现。例如,回执消息,发送方能看到已读未读列表,接受方只能看到是否已读的状态。云端删除某条群消息,在自己的消息列表消失,其他人还是可见

缺点:存储容量的增加

企业微信采用了扩散写的方式,消息收发简单稳定;存储容量的增加,可以通过冷热分离的方案解决,冷数据存到廉价的SATA盘,扩散读体验稍差,协议设计也相对复杂些。

下图是扩散写的协议设计。

  • 每个用户只有一条独立的消息流。同一条消息多副本存在于每个用户的消息流中
  • 每条消息有一个seq,在同个用户的消息流中,seq是单调递增的
  • 客户端保存消息列表中最大seq,说明客户端已经拥有比该seq小的所有消息。若客户端被push有新消息到达,则用该seq向后台请求增量数据,后台把比此seq大的消息数据返回。

03

系统稳定性

一、柔性策略

背景:企业微信作为一款Tob场景的聊天im工具,用于工作场景的沟通,有着较为明显的高峰效应,如下图。工作时间上午9:00~12:00,下午14:00~18:00,是聊天的高峰,消息量剧增。工作日和节假日也会形成明显的对比。高峰期系统压力大,偶发的网络波动或者机器过载,都有可能导致大量的系统失败。im系统对及时性要求比较高,没办法进行削峰处理。那么引入一些柔性的策略,保证系统的稳定性和可用性非常有必要。

调用数:

启动过载保护策略。当svr已经达到最大处理能力的时候,说明处于一个过载的状态,服务能力会随着负载的增高而急剧下降。如果svr过载,则拒绝掉部分正常请求,防止机器被压垮,依然能对外服务。通过统计svr的被调耗时情况、worker使用情况等,判定是否处于过载状态。过载保护策略在请求高峰期间起到了保护系统的作用,防止雪崩效应。下图就是因过载被拒绝掉的请求。

问题点:系统过载返回失败,前端发消息显示失败,显示红点,严重影响产品体验。发消息是im系统的最基础的功能,可用性要求达到几乎100%。

解决方案:尽管失败,也返回前端成功,后台保证最终成功

为了保证消息系统的可用性,规避高峰期系统出现过载失败导致前端出红点,策略如下:

  • 逻辑层hold住失败请求,返回前端成功,不出红点,后端异步重试,直至成功
  • 为了防止在系统出现大面积故障的时候,重试请求压满队列,只hold住半小时的失败请求,半小时后新来的请求则直接返回前端失败。
  • 为了避免重试加剧系统过载,指数时间延迟重试
  • 复杂的消息鉴权(好友关系,企业关系,集团关系,圈子关系),耗时严重,后台波动容易造成失败。如果并非明确鉴权不通过,则幂等重试。
  • 为了防止作恶请求,限制单个用户和单个企业的请求并发数。例如,单个用户的消耗worker数超过20%,则直接丢弃该用户的请求,不重试

优化后,后台的波动,前端基本没有感知。

(流程图)

二、系统解耦

背景:由于产品形态的原因,企业微信的消息系统,会依赖很多外部模块,甚至外部系统。例如,与微信消息互通,发送消息的权限需要放到ImUnion去做判定,ImUnion是一个外部系统,调用耗时较长。再如,金融版的消息审计功能,需要把消息同步到审计模块,增加rpc调用。再如,客户服务的单聊群聊消息,需要把消息同步到crm模块,增加rpc调用。为了避免外部系统或者外部模块出现故障,拖累消息系统,导致耗时增加,则需要系统解耦。

方案:与外部系统的交互,全设计成异步化。

思考点:需要同步返回结果的请求,如何设计成异步化。例如,群聊互通消息需经过ImUnion鉴权返回结果,前端用于展示消息是否成功发送。先让客户端成功,异步失败,则回调客户端使得出红点。

如果是非主流程,则异步重试保证成功,主流程不受影响,如消息审计同步功能。那么,只需要保证内部系统的稳定,发消息的主流程就可以不受影响。

(解耦效果图)

三、业务隔离

企业微信的消息类型有多种

  • 单聊和群聊:基础聊天,优先级高
  • api消息:企业通过api接口下发的消息,有频率限制,优先级中
  • 应用消息:系统应用下发的消息,例如公告,有频率限制,优先级中
  • 控制消息:不可见的消息。例如群信息变更,会下发控制消息通知群成员。优先级低

群聊按群人数,分成3类:

  • 普通群:小于100人的群,优先级高
  • 大群:小于2000人的群,优先级中
  • 万人群:优先级低

业务繁多,如果不加以隔离,那么其中一个业务的波动有可能引起整个消息系统的瘫痪。重中之重,需要保证核心链路的稳定,就是企业内部的单聊和100人以下群聊,因为这个业务是最基础的,也是最敏感的,稍有问题,投诉量巨大。其余的业务,互相隔离,减少牵连。按照优先级和重要程度进行隔离,对应的并发度也做了调整,尽量保证核心链路的稳定性。

(解耦和隔离的效果图)

04

toB业务功能的设计与优化

一、万人群的设计与优化

背景:企业微信的群人数上限是10000,只要群内每个人都发一条消息,那么扩散量就是10000*10000=1亿次调用,非常巨大。10000人投递完成需要的耗时长,影响了消息的及时性。

分析:既然超大群扩散写量大,耗时长,那么自然会想到,超大群是否可以单独拎出来做成扩散读呢。下面分析一下超大群设计成单副本面临的难点。

①一个超大群,一条消息流,群成员都同步这条流的消息

②假如用户拥有多个超大群,则需要同步多条流,客户端需维护每条流的seq

③客户端卸载重装,并不知道拥有哪些消息流,后台需存储并告知

④某个超大群来了新消息,需通知所有群成员,假如push没触达,客户端没办法感知有新消息,不可能去轮训所有的消息流

综上所述,单副本的方案代价太大。以下介绍,针对扩散写的方案,做的一些优化方案。

1. 并发限制

万人群的扩散量大,为了是消息尽可能及时到达,使用了多协程去分发消息。但是并不是无限制地加大并发度。

为了避免某个万人群的高频发消息,造成对整个消息系统的压力,消息分发以群id为维度,限制了单个群的分发并发度。消息分发给一个人的耗时是8ms,那么万人的总体耗时是80s,并发上限是5,那么消息分发完成需要16s。16s的耗时,在产品角度来看还、是可以接受的,大群对及时性不敏感。同时,并发度控制在合理范围内。

除了限制单个群id的并发度,还限制了万人群的总体并发度。单台机,小群的worker数为250个,万人群的worker数为30。

效果图:图一,万人群的频繁发消息,worker数用满,导致队列出现积压。图二,由于并发限制,调用数被压平,没有请求无限上涨,系统稳定。

图一

图二

2. 合并插入

工作场景的聊天,多数是在小群完成,大群用于管理员发通知或者老板发红包。

大群消息有一个常见的规律,平时消息少,会突然活跃。例如,老板在群里发个大红包,群成员起哄,此时,就会产生大量的消息。

消息量上涨,并发度被限制,任务处理不过来,那么队列自然就会积压。积压的任务中可能存在多条消息需要分发给同一个群的群成员。此时,可以将这些消息,合并成一个请求,写入到消息存储,消息系统的吞吐量就可以成倍增加。

在日常的监控中,可以捕获到这种场景,高峰可以同时插入20条消息,对整个系统很友善。

3. 业务降级

控制消息:群人员变更,群名称变动,群设置变更,都会在群内扩散一条不可见的控制消息。群成员收到此控制消息,则向后台请求同步新数据。

举个例子,一个万人群,由于消息过于频繁,对群成员造成骚扰,部分群成员选择退群来拒绝消息,假设有1000人选择退群。那么扩散的控制消息量就是1000w,用户收到控制消息就向后台请求数据,则额外带来1000w次的数据请求,造成系统的巨大压力。

控制消息在小群是很有必要的,能让群成员实时感知群信息的变更。但是在大群,群信息的变更其实不那么实时,用户也感觉不到。所以,结合业务场景,实施降级服务,控制消息在大群可以直接丢弃,不分发,减少对系统的调用。

二、回执消息的设计与优化

回执消息是办公场景经常用到的一个功能,能看到消息接受方的阅读状态。一条消息的阅读状态会被频繁修改,群消息被修改的次数和群成员人数成正比。每天上亿条消息,读写频繁,请求量巨大,怎么保证每条消息在接受双方的状态是一致的是一个难点。

1. 回执消息的实现方案

消息的阅读状态的存储方式两个方案。

方案一:利用消息存储,插入一条新消息指向旧消息,此新消息有最新的阅读状态。客户端收到新消息,则用新消息的内容替换旧消息的内容展示,以达到展示阅读状态的效果。

优点:复用消息通道,增量同步消息就可以获取到回执状态,复用通知机制和收发协议,前后端改造小

缺点:①存储冗余,状态变更多次,则需插入多条消息②收发双方都需要修改阅读状态(接收方需标志消息为已读状态),存在收发双方数据一致性问题

方案二:独立存储每条消息的阅读状态,消息发送者通过消息id去拉取数据

优点:状态一致

缺点:①构建可靠的通知机制,通知客户端某条消息属性发生变更。②同步协议复杂。客户端需要准确知道哪条消息的状态已变更。③消息过期删除,阅读状态数据也要自动过期删除

企业微信采用了方案一去实现,简单可靠,改动较小,存储冗余的问题可以通过LevelDB落盘的时候merge数据,只保留最终状态那条消息即可;一致性问题下面会介绍如何解决。下图是协议流程。referid:被指向的消息id,senderid:消息发送方的msgid。

  • 每条消息都有一个唯一的msgid,只在单个用户内唯一,kv存储自动生成的
  • 接收方b已读消息,客户端带上msgid=b1请求到后台
  • 在接受方b新增一条消息,msgid=b2,referid=b1,指向msgid=b1的消息。并把msgid=b2的消息内容设置为消息已读。msgid=b1的消息体,存有发送方的msgid,即senderid=a1
  • 发送方a,读出msgid=a1的消息体,把b加入到已读列表,把新的已读列表保存到消息体中,生成新消息msgid=a2,referid=a1,追加写入到a的消息流。
  • 接收方c已读同一条消息,在c的消息流走同样的逻辑
  • 发送方a,读出msgid=a1的消息体,把c加入到已读列表,把新的已读列表保存到消息体中,生成新消息msgid=a3,referid=a1,追加写入到a的消息流。a3>a2,以msgid大的a3为最终状态。

2. 异步化

接受方已读消息,让客户端同步感知成功,但是发送方的状态没必要同步修改。因为发送方的状态修改情况,接受方没有感知不到。那么,可以采用异步化的策略,降低同步调用耗时。

  • 接受方的数据同步写入,让客户端马上感知消息已读成功
  • 发送方的数据异步写入,减少同步请求
  • 异步写入通过重试来保证成功,达到状态最终一致的目的

3. 合并处理

客户端收到大量消息,并不是一条一条消息已读确认,而是多条消息一起已读确认。为了提高回执消息的处理效率,可以对多条消息合并处理。

  • X>>A,表示X发了一条消息给A
  • 如下图,A合并确认3条消息,B合并确认3条消息。那么只需要处理2次,就能标志6条消息已读。
  • 经过mq分发,相同的发送方也可以合并处理。在发送方,X合并处理2条消息,Y合并处理2条消息,Z合并处理2条消息,则合并处理3次就能标志6条消息

经过合并处理,处理效率大大提高。下图是采集了线上高峰时期的调用数据。可以看得出来,优化后的效果一共节省了44%的写入量。

4. 读写覆盖解决

发送方的消息处理方式是先把数据读起来,修改后重新覆盖写入存储。接收方有多个,那么就会并发写发送方数据,避免不了出现覆盖写的问题。流程如下

  • 发送方某条消息的已读状态是X
  • 接收方a确认已读,已读状态修改为X+a
  • 接收方b确认已读,已读状态修改为X+b
  • 接收方a的状态先写入,接受方b的状态后写入。这最终状态为X+b
  • 其实正确的状态是X+a+b

处理这类问题,无非就一下几种办法:

方案一:因为并发操作是分布式,那么可以采用分布式锁的方式保证一致。操作存储之前,先申请分布式锁。这种方案太重,不适合这种高频多账号的场景。

方案二:带版本号读写。一个账号的消息流只有一个版本锁,高频写入的场景,很容易产生版本冲突,导致写入效率低下。

方案三:mq串行化处理。能避免覆盖写问题,关键是在合并场景起到很好的作用。同一个账号的请求串行化,就算出现队列积压,合并的策略也能提高处理效率。

企业微信采用了方案三,相同id的用户请求串行化处理,简单易行,逻辑改动较少。

三、 撤回消息的设计

1. 难点

撤回消息,相当于更新原消息的状态,是不是也可以通过referid的方式去指向呢?回执消息分析过,通过referid指向,必须要知道原消息的msgid。区别于回执消息,撤回消息需要修改所有接收方的消息状态,而不仅仅是发送方和单个接收方的。消息扩散写到每个接收方的消息流,各自的消息流对应的msgid是不相同的,如果沿用referid的方式,那就需要记录所有接收方的msgid。

2. 解决方案

分析:撤回消息比回执消息简单的是,撤回消息只需要更新消息的状态,而不需要知道原消息的内容。接收方的消息的appinfo都是相同的,可以通过appinfo去做指向。

协议流程:

  • 用户a、b、c,都存在同一条消息,appinfo=s,sendtime=t
  • a撤回该消息,则在a的消息流插入一条撤回的控制消息,消息体包含{appinfo=s,sendtime=t}
  • 客户端sync到撤回的控制消息,获取到消息体的appinfo与sendtime,把本地appinfo=s且sendtime=t的原消息显示为撤回状态,并删除原消息数据。之所以引入sendtime字段,是为了防止appinfo碰撞,加的双重校验
  • 接收方撤回流程和发送方一致,也是通过插入撤回的控制消息

该方案的优点明显,可靠性高,协议简单。

(撤回的示意图)

05

思考与总结

企业微信的消息架构与微信类似,但是在ToB业务场景面临了一些新的挑战。结合产品形态,分析策略,通过优化方案,来确保消息系统的可靠性、稳定性、安全性。

ToB业务繁杂,有很多定制化的需求。消息系统的设计需要考虑通用性和扩展性,以便支持各种需求。例如,撤回消息的方案,可以适用于消息任何属性的更新,满足更多场景。

近期热文推荐

     你“在看”我吗?

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-07-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯大讲堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01
  • 02
  • 03
  • 04
  • 05
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档