现代IM架构研究笔记:瓜子IM和OpenIM

您所在的位置:网站首页 开源OpenIM 现代IM架构研究笔记:瓜子IM和OpenIM

现代IM架构研究笔记:瓜子IM和OpenIM

2023-08-15 22:11| 来源: 网络整理| 查看: 265

传统架构

传统的架构(十万级用户量)还是基于多进程思想,这里以TeamTalk为例,TeamTalk是蘑菇街5年前(2015年)开源的内部企业通讯软件,当时还火爆了一下,很多人纷纷研究,各种分析文章满天飞。它的架构如图所示:

简单介绍一下工作原理:

login:客户端先通过http发到login(这里应该叫rebanlancer,负载均衡),获取一个低负载(登录用户数量,即tcp连接数)的msg IP地址

msg:然后通过tcp连接msg,进行登录认证、查询会话、收发消息等。msg在整个架构中就是一个网关的角色,主要负责管理客户端的tcp连接和与客户端通信,业务实现和消息存储都在dbproxy中。因为msg是无状态设计,所以是可以水平复制的。技术实现上主要使用epoll I/O复用,是单进程方式。这样一台支持3-5万连接没啥问题,部署2-3台就可以实现10万的用户在线了。

route:msg之间通过一个路由服务进行通信,以解决在不同的msg上用户消息传递的问题。

dbproxy:业务实现和消息存储,也是无状态设计,所以可以部署2个,一个用来处理登录业务,一个做负责消息业务。

还有其他一些模块,这里就不展开讲了,比如httpmsg提供http接口给外部进行调用,推送服务,文件服务等等。

这个架构,在10万用户级别是完全没问题的,适合企业内部通讯这种场景,但是如果在百万的用户级别下,就有一些问题:

单点故障。route,所有msg都需要连接到一台route上,进行路由分发。那么这个服务挂了,就完蛋了。当然,也可以在运维层面通过Haproxy和KeepAlive等解决。

msg直连dbproxy,如果某一段时间请求很密集,势必造成dbproxy压力陡增。虽然dbproxy使用了线程池和局部任务队列排队处理,但是因为是FCFS(先来先处理),请求积压了,会造成后面的请求失败、超时等问题。

dbproxy是1个大模块,改动代价很高。如果增加一个功能没测试好,发布上线后崩溃或者有BUG整个服务将无法使用。另外一点就是,各种业务的频率也是不一样的,比如登录因为用户不停打开APP,所以最频繁,应该拆分开。使用不同的mysql,降低压力。

所以,势必对架构进行升级,才能扛得住更大的量。route的问题先放一放,msg和dbproxy这一层,我们可以通过引入Kafka解决。

这也是我认为,现代IM架构都会使用MQ的原因之一,引入Kafka的好处:

流量削峰:Kafka QPS是百万级的,mysql QPS 不太好计算,但是差不多几千到几万,肯定比Kafka慢很多,所以消息在存储进mysql之前,让Kafka这个大肚子挡一下,以避免一下子把Mysql打死。

解耦合:服务的可用性毋庸置疑的重要,通过引入Kafka,我们把系统间的强依赖变成了基于数据的弱依赖,原来修改一个模块可能其他模块也会受影响。现在,由于Kafka不同消费者之间可以同时消费消息,所以我完全可以把大模块拆成一个个小的模块,处理自己感兴趣的消息。

上面2点是我觉得从十万跨越到百万最重要的点。

现代架构

我最近主要在研究瓜子IM和openIM,其中瓜子IM的作者封宇大神分享了一系列文章,里面有详尽的时序图、协议设计、分库分表、TimeLine模型同步等等,看完之后会有一个大概的认识。Open-IM-Server是前微信技术专家创业的开源项目,使用go语言开发,使用的收件箱模型,能很好的结合瓜子IM的设计文档,加深理解。

瓜子IM

(图片来源:公众号-普通程序员 作者-封宇)

作者分享的架构,一开始其实并没有Kafka这一层,具体可以参考:

一个海量在线用户即时通讯系统(IM)的完整设计Plus

看完之后,你可能和我一样有所疑惑:这里面Kafka到底要怎么用?

Opem-IM-Server

这个作者好像是微信团队出来的,所以架构的设计上天然就是基于收件箱+写扩散模式,目前还在完善中,已经实现了基本的单聊和群里功能。

我们以上图为例,你可能我和一样,第一眼以为发消息是这个流程:

gate网关生产2个消息到kafka(上图步骤2和3),然后回复发送者发送成功(步骤4)。

通过不同的consume name,实现多个服务同时消费这个消息。在config.yaml中可以看到针对Kafka的配置:2个topic,3个消费组

其中transfer服务通过2个不同的消费组,同时把消息持久化到mysql和mongodb中

因为pusher和transfer是不同的消费组,所以pusher并发的把这个消息通过rpc调用gate,再转发给对方(扩散写,from发件箱和to收件箱各写一份)。如果对方处于离线状态,则通过第三方推送,否则,在线的情况下直接走WebSocket长连接。

看完之后,我不禁有2个疑问:

消息序号(乱序)在哪个环节生成的,我怎么没看到?不然客户端如何做消息排序?

消息都没有入mysql就判定成功,给客户端返回了是不是有问题?如果Kafka丢消息了,可能会出现数据不一致?

第2个疑问,我已经在文章:Golang中如何正确的使用sarama包操作Kafka?中进行了说明,只要producer和comsumer使用正确,最终数据会一致。为什么这里不要求强一致性?我的理解是,对于在线的用户,其实已经通过push进行了推送,客户端自己会对数据进行对齐(本地存储)。离线的客户端上线拉取的时候再对齐,这中间是有充分的时间让我们服务处理的,当然出现了BUG(消费者挂了)另说。

所以,下面主要来分析一下第一个问题。

细究Open-IM-Server中使用Kafka的流程

我画了一个简化版的图:

经过梳理和翻源码,我更正了几个误区:

并不是msg-gateway把消息投递到Kafka,而是通过gRPC调用Chat服务,Chat中会生成2个消息发到Kafka(发件箱和收件箱)

chat本地只生成MsgID(字符串,去重)和发送时间,然后发给给客户端,客户端拿服务端生成的msgID去重和发送时间进行排序。

消息序号的生成是在transfer的消费者中。一开始我以为没有seq,后面才发现是在写mongodb之前分配了一个seq。这是不是有点问题?应该在chat中生成seq啊,不然客户端pull的时候拿到seq,发送的时候拿到sendTime以那个字段为主呢,莫不是自己构造一个seq?

在Kafka中,只要消费组名不一样,就可以多次消费,所以上面有3个消费组,分别进行mysql的持久化,离线消息存储mongodb以及推送任务。

transfer的mongo消费者,写入mongodb后(如果客户端设置了 isHistory 字段),会先尝试gRPC直接调用push给客户端推消息,否则通过Kafka再绕一圈。

chat服务send_msg.go的关键代码如下:

transfer的mongo消费者关键代码:

用时序图梳理一下整个过程:

客户端通过webSocket发送消息到msg_gateway

msg_gateway通过gRPC调用chat的 UserSendMsg() 发送消息

chat服务主要是本地生成唯一消息ID(去重)和发送时间

然后投递到Kafka,等待所有Kafka的Slave都收到消息后判断发送成功

gRPC返回

给客户端回复ACK,携带错误码和服务端生成的MsgID等

transfer中的消费组mysql消费到2条消息(发送者的发件箱、接收者的收件箱)

持久化到mysql中全量存储,主要是应对后台分析、审计等需求,客户端是从mongodb中拉取的(拉取后删除),这里和微信逻辑类似。微信号称不在服务器存储数据,所以你用微信登录PC端时,你会发现刚刚手机上的消息在PC上怎么看不到?要么是PC端没有pull的过程,要么是离线消息只针对APP端,PC端拉不到。

同理,transfer中消费组mongodb消费到2条消息

调用redis的incr,递增用户的消息序号,key格式为:"REDIS_USER_INCR_SEQ: " + UserID,所以是用户范围内递增,因为本身用户只有一个收件箱,没毛病。

插入mongodb中的chat collection

优先通过gRPC调用pusher进行推送,否则走Kafka,通过Pusher消费的方式推送

pusher也同样通过gRPC调用msg_gateway的MsgToUser推送消息

通过websocket推送

用户b上线的时候,通过pull从mongodb中拉取离线消息(成功后会从mongodb中删除)

...

尾语

至此,在IM中如何使用Kafka已经相对清晰了,更多的一些细节读者可以通过代码来进一步了解。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3