(由于工作繁忙,本文还处于持续更新中)
开源技术现在如此流行,程序员们纷纷拥抱开源,互联网公司纷纷拥抱开源。 拥护开源的年轻技术者们逐渐开始掌握企业的技术决策权,被证明稳定的开源的东西也逐渐被发布到生产环境去使用。
在如今开源互联网技术生态高度成熟的环境下,对于IM即时通讯这个集性能要求、稳定要求、安全要求、用户体验要求于一体的高频使用的软件,我们也有理由相信在接下来一到两年的时间里会迸发出至少5个生产级别的优秀的开源的IM项目出来。IM技术解决落地实践方案,已经越来越公开的互联网技术了,并没有任何神秘感。下文我们将会以非常接地气的方式介绍如何一步一步地开发出一个高可用、高性能、运维成本相对较低的IM服务端应用。
首先,本文围绕纯im架构进行探讨,并不围绕im生态展开,尽量做到专精。
技术需求分为分布式架构技术需求和IM技术需求
- 高可用:IM server花式宕机、分布式Job
- 高性能:单节点至少10万级长连接(c100k)和毫秒级响应速度
- 扩展性:可以自由扩展计算和存储能力
- 监控: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被证明单节点可以稳定hold住数十万连接。
- 充分利用非阻塞的线程威力
- 我们准备采用我们非常熟悉的rxjava2来作为开发框架
我们计划基于netty网络框架,采用http1.1无状态短连接。
- servlet http server(spring mvc、spring boot、单纯的servlet容器)
- netty http server 他们之间的性能差异,我这里就不描述了,懂的人自然懂。
- OAuth2.0 授权码模式(Authorization Code)、密码模式(password)、 Client模式(Client Credentials) (OAuth2.0需要redis集中存储状态数据)
- JWT 一次性授权票据、轻应用/小程序用完即走场景,服务端不需要集中存储session,是完全无状态的。
- dao层我们选型使用reactive postgresql
- 不再使用jdbc这种阻塞业务线程直到数据库返回结果的线程模型,什么mybatis、hibernate、jooq一律不在考虑范围内! 如果不基于jdbc,那么还需要提供一套自有的orm了,我们这边也有一定的技术积累可以复用。
- 字符串数据的序列化和反序列化,我们暂时使用熟悉的fastjson
- 即时通讯网络协议,如上文统一使用websocket应用层长连接
- 消息体payload的格式规范详见接口规范文档
- websocket长连接中的消息ack协议:
- 我们不是单纯地把消息payload丢到长连接上去就完了
- 我们还需要一套ack机制来保证消息是已经被终端收到。
- 我们还需要一套ack机制来保终端消息是已经被服务器持久化到数据库中了。
- 消息去重机制/协议:message id协议。
- 在确定的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
- 统一的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长连接需要断开并与新节点建立的长连接数量越少。
- 脑裂问题一直是一个经典问题,我只能说你们把机房基础网络做稳了,这是前提。
- 跨机房集群,机房间网络断了,我们又该怎么整呢?
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核心对应的各个功能模块必然对应各个管理接口,管理接口对应着不同的管理后台页面。我们的设计会将这些页面也进行微服务化分离管理。聪明的您理解得肯定没错,我们将会使用多站点方案:
- 单点登录,身份认证
- 集中权限管理机制:权限注册中心+权限单元自治。有兴趣的你可以参考阿里云RAM和腾讯云ACM。
有些老牌的后端开发工程师喜欢用模板框架,比如很有名的freemarker,Velocity。但是这些东东真的被证明是拖慢前后端开发者进行协作开发的“利器”。当然,你们团队都是后端工程师出身的“全栈”小伙子的情况除外。
- 前端技术选型呢,交给我们的前端小伙子去决定吧,后端模板技术,我们是不会选择的。
- 后端选型
- 既然是多站点,那么是需要提供单点登录能力的,这里我们将抽象一层单点登录客户端,去对接统一单点登录服务SSO/内置的简单的SSO服务
- 将来考虑支持移动B端管理能力,同样也是需要集成SSO的。
- 后端API服务能力,上文已经提到,我们使用netty实现API server,什么spring mvc、springboot都一边站吧。
- api doc:因为是前后端分离所以简洁清晰的API文档是必不可少的,不然你让你的前端基友读你的Java源码来理解API接口该如何调用嘛?而且API文档最好是“半自动化”的,“文档代码一致性”的。
发送方终端 --> 服务端 --> 数据库 --入库成功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才能让领导用着放心?
- 聊天消息在老板自己的机房里面存着,别人无权获取,也无权进行监控,更不谈对你的隐私进行窥探和大数据分析了
- 聊天的内容通过互联网传输时,别人难以截听,即使截听也难以破解
就是最近被美国制裁华为搞得大家纷纷开始优先选择国产技术,当然我们也要支持对国产技术的!
未完待续