🚀基于Netty高可用分布式即时通讯系统,支持长连接网关管理、单聊、群聊、离线消息、消息推送消息、消息已读未读、消息未读数、红包、消息漫游等功能,支持集群部署的分布式架构。
🚀基于Netty高可用分布式即时通讯系统,支持长连接网关管理、单聊、群聊、离线消息、消息推送消息、消息已读未读、消息未读数、红包、消息漫游等功能,支持集群部署的分布式架构。
基于可扩展性高可用原则,把网关层、逻辑层、数据层分离,并且支持分布式部署
问题背景:当某个实例重启后,该实例的连接断开后,客户端会发起重连,重连就大概率转移其他实例上,导致最近启动的实例连接数较少,最早启动的实例连接数较多 解决方法:
GATE层网关有以下特性:
logic按照分布式微服务的拆分思想进行拆分,拆分为多个模块,集群部署:
logic消息服务集成路由客户端SDK,SDK职责:
SDK和网关底层通信设计:
IM协议采用二进制定长包头和变长包体来实现客户端和服务端的通信,并且采用谷歌protobuf序列化协议,设计如下:
各个字段如下解释:
一个正常的消息流转需要如图所示的流程:
需要考虑的是,一个健壮的系统需要考虑各种异常情况,如丢消息,重复消息,消息时序问题
以上,如果保持绝对的实现,那么只能是一个发送方,一个接收方,一个线程阻塞式通讯来实现。那么性能会降低。
单聊:通过发送方的绝对时序seq,来作为接收方的展现时序seq。
群聊:因为发送方多点发送时序不一致,所以通过服务器的单点做序列化,也就是通过ID递增发号器服务来生成seq,接收方通过seq来进行展现时序。
群聊时序的优化:按照上面的群聊处理,业务上按照道理只需要保证单个群的时序,不需要保证所有群的绝对时序,所以解决思路就是同一个群的消息落到同一个发号service上面,消息seq通过service本地生成即可。
为什么要保证顺序?
如何保证顺序?
整体消息推送和拉取的时序图如下:
本项目是进行推拉结合来进行服务器端消息的推送和客户端的拉取,我们知道单pull和单push有以下缺点:
单pull:
单push:
推拉结合:
实际业务的情况【只做参考,实际可以根据公司业务线来调整】
根据以上业务情况,来设计分布式ID:
当并发度不高的时候,时间跨毫秒的消息,区分不出来消息的先后顺序。因为时间跨毫秒的消息生成的ID后面的最后一位都是0,后续如果按照消息ID维度进行分库分表,会导致数据倾斜
难点
优化
一个Notify包的数据经网关的线程模型图:
一个请求包的数据经网关的架构图:
难点
优化
背景:高峰期系统压力大,偶发的网络波动或者机器过载,都有可能导致大量的系统失败。加上IM系统要求实时性,不能用异步处理实时发过来的消息。所以有了柔性保护机制防止雪崩
柔性保护机制开启判断指标,当每个指标不在平均范围内的时候就开启
当开启了柔性保护机制,那么会返回失败,用户端体验不友好,如何优化
gate层重启升级或者意外down机有以下问题:
解决方案如下:
Redis作用背景
如果Redis宕机,会造成下面结果
Redis宕机兜底处理策略
核心设计点
字段 | 类型 | 描述 |
---|---|---|
id | int | 自增ID |
group_id | int | 群ID |
user_id | bigint | 用户ID |
last_ack_msg_id | bigint | 最后一次ack的消息ID |
user_device_type | tinyint | 用户设备类型 |
is_deleted | tinyint | 是否删除,根据这个字段后续可以做冷备归档 |
create_time | datetime | 创建时间 |
update_time | datetime | 更新时间 |
字段 | 类型 | 描述 |
---|---|---|
id | int | 自增ID |
msg_id | bigint | 消息ID |
group_id | int | 群ID |
sender_id | bigint | 发送方ID |
msg_type | int | 消息类型 |
msg_content | varchar | 消息内容 |
is_deleted | tinyint | 是否删除 |
create_time | datetime | 创建时间 |
update_time | datetime | 更新时间 |
Q:相比传统HTTP请求的业务系统,IM业务系统的有哪些不一样的设计难点?
Q:对于单聊和群聊的实时性消息,是否需要MQ来作为通信的中间件来代替rpc?
A:MQ作为解耦可以有以下好处:
但是缺点也有:
综上,是否考虑使用MQ需要架构师去考量,比如考虑业务是否允许、或者系统的流量、或者高可用设计等等影响因素。 本项目基于使用成本、耦合成本和运维成本考虑,采用Netty作为底层自定义通信方案来实现,也能同样实现层级调用。
Q:为什么接入层用LSB返回的IP来做接入呢?
A:可以有以下好处:1、灵活的负载均衡策略 可根据最少连接数来分配IP;2、做灰度策略来分配IP;3、AppId业务隔离策略 不同业务连接不同的gate,防止相互影响
Q:为什么应用层心跳对连接进行健康检查?
A:因为TCP Keepalive状态无法反应应用层状态问题,如进程阻塞、死锁、TCP缓冲区满等情况;并且要注意心跳的频率,频率小则可能及时感知不到应用情况,频率大可能有一定的性能开销。
Q:MQ的使用场景?
A:IM消息是非常庞大的,比如说群聊相关业务、推送,对于一些业务上可以忍受的场景,尽量使用MQ来解耦和通信,来降低同步通讯的服务器压力。
Q:群消息存一份还是多份,读扩散还是写扩散?
A:存1份,读扩散。存多份下同一条消息存储了很多次,对磁盘和带宽造成了很大的浪费。可以在架构上和业务上进行优化,来实现读扩散。
Q:消息ID为什么是趋势递增就可以,严格递增的不行吗?
A:严格递增会有单点性能瓶颈,比如MySQL auto increments;redis性能好但是没有业务语义,比如缺少时间因素,还可能会有数据丢失的风险,并且集群环境下写入ID也属于单点,属于集中式生成服务。小型IM可以根据业务场景需求直接使用redis的incr命令来实现IM消息唯一ID。本项目采用snowflake算法实现唯一趋势递增ID,即可实现IM消息中,时序性,重复性以及查找功能。
Q:gate层为什么需要开两个端口?
A:gate会接收客户端的连接请求(被动),需要外网监听端口;entry会主动给logic发请求(主动);entry会接收服务端给它的通知请求(被动),需要内网监听端口。一个端口对内,一个端口对外。
Q:用户的路由信息,是维护在中央存储的redis中,还是维护在每个msg层内存中?
Q:网关层和服务层以及msg层和网关层请求模型具体是怎样的?
Q:本地写数据成功,一定代表对端应用侧接收读取消息了吗? A:本地TCP写操作成功,但数据可能还在本地写缓冲区中、网络链路设备中、对端读缓冲区中,并不代表对端应用读取到了数据。
Q:为什么用netty做来做http网关, 而不用tomcat?
Q:为什么消息入库后,对于在线状态的用户,单聊直接推送,群聊通知客户端来拉取,而不是直接推送消息给客户端(推拉结合)? A:在保证消息实时性的前提下,对于单聊,直接推送。对于群聊,由于群聊人数多,推送的话一份群消息会对群内所有的用户都产生一份推送的消息,推送量巨大。解决办法是按需拉取,当群消息有新消息时候发送时候,服务端主动推送新的消息数量,然后客户端分页按需拉取数据。
Q:为什么除了单聊 群聊 推送 离线拉取等实时性业务,其他的业务都走http协议? A:IM协议简单最好,如果让其他的业务请求混进IM协议中,会让其IM变的更复杂,比如查找离线消息记录拉取走http通道避免tcp 通道压力过大,影响即时消息下发效率。在比如上传图片和大文件,可以利用HTTP的断点上传和分段上传特性
Q:机集群机器要考虑到哪些优化? A:网络宽带;最大文件句柄;每个tcp的内存占用;Linux系统内核tcp参数优化配置;网络IO模型;网络网络协议解析效率;心跳频率;会话数据一致性保证;服务集群动态扩容缩容