如何构建IM系统(摘选)

2020-11-20  本文已影响0人  阿斯蒂芬2

需求分析

技术需求

技术需求分为分布式架构技术需求和IM技术需求

分布式架构技术需求

高可用:IM server花式宕机、分布式Job

高性能:单节点至少10万级长连接(c100k)和毫秒级响应速度

扩展性:可以自由扩展计算和存储能力

监控:IM服务具备自监控能力,降低对外监控服务的依赖

易运维

IM技术需求

统一的websocket技术选型:兼容局限于7层协议web终端,7层相对4层的性能损耗也不是很大。

在线用户状态:在线终端与IM server节点的websocket长连接对应关系的状态维护

图片压缩技术

消息持久化存储

消息序列化和反序列化

im管理后台

功能需求

消息可靠性、即时性:不丢消息、毫秒触达。

通信加密

消息存储加密

文件发送和存储

“正在输入”状态

“消息撤回”

“忙碌”等状态

群组

用户关系(可见性)

消息类型(文字、音频、视频、图片、地理位置、自定义类型)

消息评论(类似飞书)

消息待办

历史消息

聊天室

多终端同时登陆

音视频通话

企业im组织架构管理

集团im多组织隔离

互联网im多租户

多级管理员(系统管理员、租户管理员、组织管理员、部门管理员)

本地账密认证和第三方账密认证

架构分析

技术选型

语言方面依然是Java8

花式宕机

vip虚拟ip+dns轮询,实现负载器集群的高可用

多NGINX/负载均衡器(我个人认为根本没必要花钱去使用商业版的负载均衡器,NGINX集群真的完全完全充足够用!人傻钱多才去买F5等昂贵的负载器)

去注册中心的raft+gossip,这样可以降低运维成本,因为不再需要维护一个注册中心集群(如zookeeper、etcd等)

分布式job(我们计划参考Raft+Gossip算法来实现一套分布式调度任务,摒弃使用任何第三方的job调度器)

chaos monkey(这个名字很有名的,搞应用架构的肯定都知道,有了它,程序想不健壮都不行了)

高性能

基于Netty的websocket server

事实上netty被证明单节点可以稳定hold住数十万连接。

业务层使用非阻塞的reactive框架来开发

充分利用非阻塞的线程威力

我们准备采用我们非常熟悉的rxjava2来作为开发框架

高性能netty网络框架实现http API server

我们计划基于netty网络框架,采用http1.1无状态短连接。

 servlet http server(spring mvc、spring boot、单纯的servlet容器)

 netty http server 他们之间的性能差异,我这里就不描述了,懂的人自然懂。

API server安全接入与授权

 OAuth2.0 授权码模式(Authorization Code)密码模式(password)、 Client模式(Client Credentials)(OAuth2.0需要redis集中存储状态数据)

 JWT 一次性授权票据、轻应用/小程序用完即走场景,服务端不需要集中存储session,是完全无状态的。

dao层不再使用基于jdbc的框架

dao层我们选型使用reactive postgresql

不再使用jdbc这种阻塞业务线程直到数据库返回结果的线程模型,什么mybatis、hibernate、jooq一律不在考虑范围内! 如果不基于jdbc,那么还需要提供一套自有的orm了,我们这边也有一定的技术积累可以复用。

高性能IM通信协议

字符串数据的序列化和反序列化,我们暂时使用熟悉的fastjson

即时通讯网络协议,如上文统一使用websocket应用层长连接

消息体payload的格式规范详见接口规范文档

websocket长连接中的消息ack协议:

我们不是单纯地把消息payload丢到长连接上去就完了

我们还需要一套ack机制来保证消息是已经被终端收到。

我们还需要一套ack机制来保终端消息是已经被服务器持久化到数据库中了。

消息去重机制/协议:message id协议。

在确定的IM通信协议前提下,我们要提供一套完善的全方位的压测客户端,以确定各种规模的服务端配置能给出多高的极限并发量。

扩展性

无状态的im应用实例

上文提到我们基于raft+gossip去中心化的服务治理方案,我们的服务端im-server应用扩展直接通过增加服务器增加应用实例数量来实现计算能力扩展。

如何克服关系型数据库存储的瓶颈问题

使用数据库中件(这个方案由于维护成本太高而放弃)

使用云厂商的数据库云服务postgresql(这个视情况而定)

使用分布式关系型数据库

基于上文提到的reactive-dao的选型,我们选型的是postgresql型数据库

决定采用开源的cockroachdb,是由于它是完整的postgresql数据库协议的实现,而且支持完美的写扩展能力,开源和高可用的支持,基于postgresql生态,云原生的支持,不依赖某个特定的云厂商。

自监控

这里为什么要提自监控呢?主要是因为我们想要实现一款解耦对特定监控系统的依赖关系。 设计思路是:

实现一套通用监控数据的metric接口,将IM服务器的状态全部提供出来

任何外部的监控系统如果要与之对接,则再实现一个适配器对接二方数据接口即可。

易维护性

如果要降低im集群的维护难度,原则就是:

尽量避免或少使用外部中间件和多重数据库种类。

我们见过很多系统,为了性能和为了功能丰富,MySQL主从集群、redis集群、MongoDB集群、NGINX轮询、zookeeper集群、fastdfs集群、kafka/rabbitMQ集群,等等,一堆一堆地用。这运维的酸爽感,你们不搞运维的体会不了。

傻瓜式部署

你的软件,它如果是可以一键傻瓜运行、可实施性特别简单傻瓜是不是接受度会更高?特别是是在小型企业内部,以及学术技术研究方面。

多环境支持

Linux、windows、Mac OS

docker或者更进一步kubernetes

IM相关技术实现

session长连接

统一的websocket技术选型:兼容局限于7层协议web终端,7层相对4层的性能损耗也不是很大。

技术选型如上文,我们使用netty来实现websocket server

在线用户状态

海量在线用户终端是与多个IM server建立着长连接的,我们向终端推送消息必须定位到是哪个im server的哪个websocket连接。如果有一个简单而搞笑的定位算法那就是极好的。一般我们首先想到的是使用redis等缓存技术去集中存储这些状态。但是,我们是否有更优秀的方案呢?

hash slot算法

raft一致性算法 步骤如下:

我们通过raft一致性算法得到IM server节点列表

我们通过hash slot算法可以快速高效地得到用户对应的IM server节点,由于一致性哈希算法的一致性保证,我们可以保证每次得到的都是一致的结果,即我们可以保证每次定位到100%相同的长连接所在的节点。

hash slot结合raft一致性算法,支持花式宕机后session长连接动态调整到一致性哈希对应的落点,节点越多需要新建的session长连接越少;同理,我们扩展im计算能力,新增session节点后,部分存量session长连接需要断开并与新节点建立的长连接数量越少。

问题来了:raft脑裂问题

脑裂问题一直是一个经典问题,我只能说你们把机房基础网络做稳了,这是前提。

跨机房集群,机房间网络断了,我们又该怎么整呢? 0. 首先国内就没有几家企业有机会让他们的IM达到跨机房部署应用集群的条件。如果达到了,我们继续往下看。

不知道大家有没有玩过elasticsearch?

elasticsearch有一个minimum_master_nodes参数,用来规避脑裂时被隔离的小集群内某个节点被提升为leader。所以你们的懂的,这里不再多说了。

图片压缩技术

先空着

消息持久化存储

上文已经提到了,我们基于最单纯数据库和中间件选型原则,选用最少的数量类型的数据库技术: 目前市面上最合适的产品经过实践评估即是cockroachdb,即小强db,云原生的异地多活分布式关系型数据库解决方案。 之所以是cockroachdb:

上文已经讲到过postgresql的reactive驱动的生态。

跟tidb一样,参考Google f1论文理论基础,对标Google spanner数据库的产品,但是比tidb更简洁的运维+云原生;(我凭什么说比tidb运维更简单呢?你们自己玩一下这两个数据库就知道了。)

我心目中的明星协议:一致性协议raft

完全开源

消息序列化和反序列化

有人说Protobuf通信协议是最快的序列化和传输方案。但是protobuf不是最通用的序列化方案。考虑到我们对json的熟悉程度更高于protobuf,而且基于字符串的序列化性能,json不比protobuf慢。而IM传输的payload不就是文本嘛?有兴趣可以参考这篇关于protobuf和json性能对比的文章

所以,我们固执地选择fastjson作为序列化和反序列化方案

im管理后台

管理功能微服务化

即后台界面与管理接口配套的微服务化方案。说白了就是复用,就是将来谁想要复用我们的局部服务能力,我可以把它抠出去给他复用。

考虑到微服务化,我们需要将im核心进行细粒度的拆分实现。im核心对应的各个功能模块必然对应各个管理接口,管理接口对应着不同的管理后台页面。我们的设计会将这些页面也进行微服务化分离管理。聪明的您理解得肯定没错,我们将会使用多站点方案:

单点登录,身份认证

集中权限管理机制:权限注册中心+权限单元自治。有兴趣的你可以参考阿里云RAM和腾讯云ACM。

一沉不变的前后端分离方案

有些老牌的后端开发工程师喜欢用模板框架,比如很有名的freemarker,Velocity。但是这些东东真的被证明是拖慢前后端开发者进行协作开发的“利器”。当然,你们团队都是后端工程师出身的“全栈”小伙子的情况除外。

前端技术选型呢,交给我们的前端小伙子去决定吧,后端模板技术,我们是不会选择的。

后端选型

既然是多站点,那么是需要提供单点登录能力的,这里我们将抽象一层单点登录客户端,去对接统一单点登录服务SSO/内置的简单的SSO服务

将来考虑支持移动B端管理能力,同样也是需要集成SSO的。

后端API服务能力,上文已经提到,我们使用netty实现API server,什么spring mvc、springboot都一边站吧。

api doc:因为是前后端分离所以简洁清晰的API文档是必不可少的,不然你让你的前端基友读你的Java源码来理解API接口该如何调用嘛?而且API文档最好是“半自动化”的,“文档代码一致性”的。

IM功能性设计

消息发送简要流程

发送方终端 --> 服务端 --> 数据库 --入库成功ack--> 发送方终端 

--> 收方终端 --成功接收ack--> 服务端 --更新状态已发送--> 数据库 

(是不是感觉这个不清晰,我们可以)

消息可靠性,不丢消息

我们采用经典的ack和message id去重机制

ack保证消息不丢失,关于ack我们在上文中已经简单地提到过,它属于websocket session会话通信协议的一部分:

发送方终端-->服务端-->持久化成功--ack-->发送方终端

服务端--推送-->接收端终端--接收并ack-->服务端

message id去重机制保证

服务端消息仅入库存储唯一一条,我们将message id在数据库设置唯一性约束。

终端接收消息后推送,都会根据id去重,保证UI只呈现那唯一一条消息。

message id由im-sdk生成(消息推送restful接口除外)

messageId结构规范定义:版本号 + 发送方id(比如userId)+ 终端id + 时间戳 + 随机数

消息即时性

消息即时性,其实就是IM服务性能可靠保证前提和网络通畅的前提

性能保证:上文已经提到,监控和弹性是保证IM服务可靠性的关键。

网络可靠性:网络可靠性还是相对依赖网络基础设施了,应用层会辅助对应用流量进行监控,以降低对网络监控的强依赖。

消息发送流程图

消息发送

消息推送

终端消息拉取

通信加密

什么样的IM才能让领导用着放心?

聊天消息在老板自己的机房里面存着,别人无权获取,也无权进行监控,更不谈对你的隐私进行窥探和大数据分析了

聊天的内容通过互联网传输时,别人难以截听,即使截听也难以破解

上一篇下一篇

猜你喜欢

热点阅读