本文由冰河分享,作者博客 binghe.gitcode.host,原题“这套分布式im即时通讯系统如何写到简历上?我给你整理好了!”,本文有修订和改动。
分布式im即时通讯系统本质上就是对线上聊天和用户的管理。
针对聊天本身来说,最核心的需求就是:发送文字、图片、文件、语音、视频、消息缓存、消息存储、消息未读、已读、撤回,离线消息、历史消息、单聊、群聊,多端同步,以及其他一些需求。
对用户管理来说,存在的需求包含:添加好友、查看好友列表、删除好友、查看好友信息、创建群聊、加入群聊、查看群成员信息、退出群聊、修改群昵称、拉人进群、踢人出群、解散群聊、填写群公告、修改群备注以及其他用户相关的需求等。
为了更好的理解分布式im即时通讯系统的设计,我站在架构师的角度,在充分了解系统需求、业务流程和技术流程后,从全局视角为系统设定方案目标,对技术方案进行选型,对系统进行总体架构设计和分层架构设计,并梳理清楚发送消息的交互链路、单聊和群聊的交互链路。希望对你有帮助。
技术交流:
- 移动端im开发入门文章:《》
- 开源im框架源码:()
(本文已同步发布于:)
在进行技术选型与总体架构设计之前,需要明确一个事项,就是系统无论采用哪种方案,采用哪种架构设计都需要明确这种方案的业务目标、技术目标和架构目标,并在研发过程中不断评估系统的总体性能表现,发现系统瓶颈并不断进行优化。
总体上,我们搭建和开发的分布式im即时通讯系统,需要满足如下方案目标。
具体是:
- 1)业务目标:满足需求设计篇章中的各类需求场景;
- 2)技术目标:支持无限扩容,百万用户同时在线聊天;
- 3)架构目标:高并发、高性能、高可用、可监控、可预警、可伸缩,支持无限扩展。
在技术选型上,除了采用springboot等基础框架外,也会采用容器化方案。
同时,考虑到为了尽量降低技术门槛,在整个分布式im即时通讯系统的技术选型中,主要采用市面上比较流行的技术框架和方案。
具体选型如下所示:
- 1)开发框架:springboot、springcloud、springcloud alibaba、dubbo;
- 2)缓存:redis分布式缓存 guava本地缓存;
- 3)数据库:mysql、tidb、hbase;
- 4)流量网关:openresty lua;
- 5)业务网关:springcloud gateway sentinel;
- 6)持久层框架:mybatis、mybatis-plus;
- 7)服务配置、服务注册与发现:nacos;
- 8)消息中间件:rocketmq;
- 9)网络通信:;
- 10)文件存储:minio;
- 11)日志可视化治理:elk;
- 12)容器化管理:swarm、portainer;
- 13)监控:prometheus、grafana;
- 14)前端:vue;
- 15)单元测试:junit;
- 16)基准测试:jmh;
- 17)压力测试:jmeter。
对于im即时通讯系统来说,涵盖了即时通讯后端服务、大后端平台、sdk接入服务、openai接入服务、大前端ui,我相信不少小伙伴多多少少能够画出im即时通讯系统的架构图,大致如下图所示。
其实,这种这种架构设计也比较常见,在这种架构设计中,kong/openresty/nginx只做负载均衡和反向代理,研发人员更多的是关注业务层和基础层的开发,流量比较小时,这种架构设计一般不会有什么问题。但是一旦流量比较大,用户调用后端平台的接口发送消息时,即时通讯sdk同步调用即时通讯服务的接口就会出现性能问题。
因为每个终端同时只能与一个im即时通讯服务实例建立连接,如果大量的用户终端恰好都与一个im即时通讯服务建立连接,那即时通讯sdk频繁同步调用同一个im即时通讯服务的接口就会出现性能瓶颈。此时,出现性能瓶颈时,不仅仅会影响到im即时通讯服务,也会对后端平台接收请求的业务造成一定的影响。
既然上节图中所示的架构设计存在性能瓶颈,那我们如何进行优化呢?
为此我们在前图基础上进行了优化,优化后的架构如下图所示。
对比两图可以看出,在屏蔽掉技术实现细节的前提下,我们将对业务的校验和流量管控进行前置化,放大kong/openresty/nginx的职责,使得这些软件不仅具备反向代理和负载均衡的功能,还能实现限流、黑白名单、流量管控、业务校验等功能。
也就是说,在这种架构模式下,我们充分发挥了整个分布式im即时通讯系统的入口职责,充分利用kong/openresty/nginx的高并发、高吞吐量的能力,尽量将大部分无效请求挡在整个系统之外。例如,用户在没登录系统的前提下,就尝试调用发送消息、添加好友、添加群组等等接口。这样会大大减轻后台平台的业务压力。
除了在kong/openresty/nginx中实现限流、黑白名单、流量管控、业务校验等功能外,我们还引入了业务网关集群,实现限流、降级、熔断、流控、校验、鉴权等功能,进一步保证下游系统的稳定性和安全。
为了解决大量用户终端恰好连接到同一个im即时通讯服务实例,im即时通讯sdk频繁调用同一个im即时通讯服务实例的接口造成的性能问题。我们在im即时通讯服务sdk与im即时通讯服务之间引入了rocketmq集群。
im即时通讯服务集群中的每一个im即时通讯服务实例在集群中都有一个唯一的id,并且每个im即时通讯服务实例在启动后,只会监听rocketmq中与自身id相关的topic。这样每个im即时通讯服务只会收到与自身id相关的topic中的消息,不会接收所有的消息。
当用户登录系统后,就会与im即时通讯服务建立长连接,并且会以用户id和终端为key,以im即时通讯服务的id为value,将其存储到分布式缓存中。同时,会以用户id和终端为key,以用户终端与im即时通讯服务建立的长连接为value,将其存储到im即时通讯服务本地内存中。
当用户调用后端平台的接口发消息时,会带上目标用户的id,并且在im即时通讯sdk中会指定用户登录的终端设备,最终会通过im即时通讯sdk向rocketmq发送消息。
此时im即时通讯sdk会根据目标用户id和终端从分布式缓存中获取目标用户连接的im即时通讯服务的id,并向此id相关的topic发送消息。此时与目标用户建立长连接的im即时通讯服务就会接收到rocketmq中的消息,随后根据用户id和终端从本地缓存中获取到与用户终端建立的长连接,并基于此长连接向用户推送消息。
另外,在实际实现中,为了避免大量用户同时只连接im即时通讯服务集群中的某一个服务实例,会对用户连接的ip、浏览器指纹、手机设备等做hash和取模运算,使其尽量均匀分布到集群中的每一个服务实例上。
那么问题来了,这种架构设计还有进一步优化的空间吗?
为进一步增强分布式im即时通讯系统的性能、可用性和弹性伸缩能力,我们可以对分布式im即时通讯系统进行容器化架构设计,如下图所示。
可以看到,我们对分布式im即时通讯系统的架构设计进行了进一步优化,采用了容器化架构设计。在原有架构的基础上,我们进行了如下改进和优化。
1)基础支撑服务:基础支撑服务会由各种基础中间件、数据存储服务、以及监控服务实现,包含:mysql数据库、tidb数据库、hbase、redis缓存、rocketmq消息队列、prometheus监控和portainer容器管理等基础中间件实现,基础支撑服务会对整个分布式im即时通讯系统提供最基础的数据、传输、监控和容器管理等服务。
2)容器化:在容器化层面,会通过docker、swarm和portainer实现,其中,会基于swarm和portainer对容器化进行管理。
3)其他基础性功能实现:除了上述分层架构外,对于建设分布式im即时通讯系统来说,还要考虑异常监控、服务注册与发现、可视化、服务降级与兜底数据、服务限流、服务容灾、容量规划与扩缩容和全链路压测等。
在分布式im即时通讯系统中,不管是大后端平台,还是im即时通讯服务,我们都会对业务层的代码采用分层业务架构。
这里,可以借鉴ddd的分层架构思想,将代码总体上分成展示层、应用层、领域层和基础设施层四个层次。
但是,考虑到分布式im即时通讯系统的特殊性,又不会严格按照ddd的原则来设计代码分层,具体按照如下图所示。
可以看到,分布式im即时通讯系统会借鉴ddd的设计思想,但是不会完全按照ddd的方式进行设计。
1)展示层:展示层,也叫做用户ui层,是ddd设计的最上层,对外提供api接口,接收客户端请求,解析参数,返回结果数据,并对异常进行处理。
2)应用层:应用层,也叫做application层,应用层主要处理容易变化的业务场景,可对相关的事件、调度和其他聚合操作进行相关的处理。
3)领域层:领域层,也叫做domain层,领域层可以说是ddd设计的精髓所在,它是将业务系统中相对不变的部分抽象出来封装成领域模型。在分布式im即时通讯系统的设计中,领域层基本不会依赖其他层,也不会依赖基础设施层,这里是与ddd设计存在区别的地方。
4)基础设施层:基础设施层,也叫做infrastructure层,基础设施层会对其他各层提供通用的基础能力,在分布式im即时通讯系统中,就包括了缓存、通用工具类、消息、系统的持久化机制等。
在分布式im即时通讯系统中,我们忽略掉其他一些细节信息,重点关注下发送消息的交互链路逻辑。不管是单聊还是群聊,最终都需要通过im即时通讯服务将消息推送给用户的终端。此时发送消息的流程如下图所示。
可以看到:用户在分布式im即时通讯系统发送消息时,不管是单聊还是群聊,最终的消息都会推送到用户登录的终端设备上。假设此时用户a给用户b发送消息,或者用户a和用户b在同一个群组,用户a向群组发送消息,用户b接收消息的主要流程如下。
具体是:
- 1)用户a调用后端平台的接口向用户b发送消息,并且发送的消息中会带有用户b的id以及终端信息;
- 2)后端平台将消息缓存起来,并且会将消息异步写入消息库;
- 3)后端平台从redis中获取用户b连接的im即时通讯服务的id;
- 4)后端平台获取到用户b连接的im即时通讯服务的id后,会向rocketmq中用户b连接的im即时通讯服务id对应的topic发送消息;
- 5)im即时通讯服务会监听自身服务id对应的rocketmq中topic的消息,此时,用户b连接的im即时通讯服务会接收到消息;
- 6)im即时通讯服务接收到消息后,会根据用户b的id以及终端信息从缓存中获取用户b与im即时通讯服务建立的连接,并且通过这个连接向用户b推送消息。
要实现如上发送消息的流程,前提是要满足如下条件:
- 1)后端平台满足分布式条件,可随时横向扩展;
- 2)im即时通讯服务满足分布式条件,可随时横向扩展;
- 3)每个启动的im即时通讯服务实例在集群中都有一个唯一的id;
- 4)每个im即时通讯服务,都只监听自身id对应的rocketmq中topic的消息;
- 5)用户登录分布式im即时通讯系统后,会与im即时通讯服务建立长连接,并且会根据用户id和所在的终端缓存长连接,同时会根据用户id和所在的终端将连接的im即时通讯服务的id缓存到redis;
- 6)用户发送消息时,会根据目标用户的id和终端从redis中获取im即时通讯服务的id,进而向当前im即时通讯服务的id对应的rocketmq的topic发送消息;
- 7)对应的im即时通讯服务监听并接收到rocketmq消息后,会根据目标用户的id和终端从缓存中获取到用户的连接信息,向目标用户推送消息。
单聊就是在分布式im即时通讯系统中,一个用户直接与另外一个用户聊天,也就是一对一的聊天。在这种场景下,很有可能单聊的两个用户中,出现用户不在线的情况。
例如:用户a给用户b发送消息时,用户b可能不在线。
此时,我们就需要将用户a向用户b发送的消息存储起来。
其实,在我们实现的分布式im即时通讯系统中,无论把用户b是否在线,都会存储消息记录。当用户b登录系统后,将消息同步给用户b,如下图所示。
可以看到,用户a向用户b发送消息时:
- 1)如果用户b在线,就可以按照发送消息的交互链路向用户b发送消息了;
- 2)如果用户b不在线,此时就无法向用户b正常推送消息。当用户b登录分布式im即时通讯系统后,就会调用后端平台的接口拉取所有未读消息,并通过用户b在线流程向用户b推送消息。
群聊就是在分布式im即时通讯系统中,多个用户在同一个群组中进行聊天。
此时在发送消息时,我们可以通过群组id找出群内所有在线的用户,将消息即时发送给在线的用户。
那些未在线的用户就按照单聊未在线的用户进行处理,如下图所示。
可以看到,群聊的交互链路流程如下所示:
- 1)用户调用后端平台的接口向群组发送消息;
- 2)后端平台将消息缓存并异步写入消息库;
- 3)由于是向群组发送消息,群里有多个用户,此时就会从redis中获取所有用户连接的im即时通讯服务id列表;
- 4)对用户按照服务id分组,将相同服务id下的用户分在同一个逻辑分组里,方便后续推送消息,并且会记录未在线的用户列表;
- 5)循环向每个服务id对应的rocketmq中的topic发送消息;
- 6)广播处理未在线用户的未读消息id;
- 7)im即时通讯服务会监听自身服务id对应的topic,会随时接收推送到自身服务的消息;
- 8)当im即时通讯服务接收到消息后,此时用户掉线,或者用户不在线,向用户推送消息就会失败,或者未查询到用户与im即时通讯服务建立的连接,就不会向用户推送消息;
- 9)当用户登录分布式im即时通讯系统后,会从后端平台拉取历史(离线)消息,并通过用户在线的流程,向用户推送消息;
好了,看到这里,你明白如何设计一个高度可扩展的分布式im即时通讯系统了吗?
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]
[11]
[12]
[13]
[14]
[15]
[16]
[17]
[18]
[19]
[20]
[21]
(本文已同步发布于:)