Skip to content

Latest commit

 

History

History
270 lines (114 loc) · 15.2 KB

1、开发一个IM需要什么以及大体套路.md

File metadata and controls

270 lines (114 loc) · 15.2 KB

1、开发一个IM需要什么以及大体套路

    即时通讯工具发展时值今日,可以说家喻户晓,娱乐性即时通讯的发展,无非在乎类似QQ、微信、人人、微博私信一类的通讯产品,放眼至国外,无非有twitter私信、Google+、Facebook聊天等等。开放性的聊天产品,比如Jabber XMPP等,都是非常常见的即时通讯产品。当互联网以及第三产业发展至联网铺开市场的时候,那么商业性和辅助商业的IM产品就应运而生。比如游戏中的聊天室、电商网站的客服系统、以及一些社区类似IRC的互动系统等等,因此我们在设计产品时,或多或少会考虑到使用一个开源的产品、接入商业IM产品、或者开发一款即时通讯工具的需求。

  当然,如果说,我们一开始决定了使用开源的产品进行集成和接入,或者使用商业的IM产品,那么就有本笔记一下内容了。所以我们现在要考虑,我们自己要开发一套即时通讯软件产品,我们需要使用哪些技术,以及如何选型、建立如何的选型条件。

  以下几点是开发一个IM系统所需要的考虑因素以及选型时需要的技术要点和指导,会举例说明可能的选型,并不做强制性要求或者推荐。

1、用户系统

对于不同的主营业务系统,所使用到的用户系统均存在着或多或少的差异,因此在设计用户系统时,会事实上建立在现实已经存在的用户系统以及模型上。对于资讯、游戏类应用,用户系统当分为两个部分:正式用户以及游客用户。而对于“登录”有必要的应用,只需要关心正式用户的设计即可,无需关心游客用户。

用户连接的认证机制

对于登录功能的设计,是一个设计上非常重要的一环。它关乎一个客户端到服务器长连接的合法性、用户令牌过期等问题的一个重要依赖业务。这个可能会用到jwt、OpenID、CAS、OAuth等框架协议,并且为用户获取Token,尽量少的可能性去使用uid作为用户令牌,以免造成潜在的安全问题。

2、业务系统

业务系统同用户系统,是以主要业务为基础的有关即时通讯的业务。比如最基本的聊天、群聊、聊天图片、聊天发送定位、聊天表情、语音聊天、消息通知的推送机制、加好友、通讯录、检索用户、应允好友请求和加群请求、拉人入群、踢人、删除好友、临时聊天、黑名单等等,有的系统可能是“关注”、“粉丝”等,这都需要需要设计合理的数据库表结构,以达到业务准确表达的同时,也要保证不差的最大化的查询性能(请参考SQL优化策略、时间复杂度和NoSQL性能优化)。

存储选型

使用MySQL、PerconaDB、MariaDB 等数据库均可存储用户以及业务信息,Cassandra、CouchDB、MongoDB可以用户用户时间序列信息的存储,使用redis作为非业务的必要信息的缓存和存储。

设计原则

设计业务时,要先“通用”,后“定制”的思维去设计业务,以保证能够在业务变更时及时响应变化,建议在设计业务组件时,以用户系统为底层基础,业务系统为上层模块,保证模块之间有较小的耦合性,以保证能够以组成件的方式组合业务。

后台功能设计指导

要针对用户查看用户在线状态、登录IP、登录时间、在线时长、用户关系、强制下线、删除用户、禁言、广播、拥有的群聊天室等等开放控制。

3、通讯协议

关于通讯协议问题,无论是传输层还是应用层定义协议,争议最大。

传输层协议以及心跳策略

TCP、UDP

TCP有保障

UDP传输性能强

强网络环境时,TCP可以提供更高的实时性

弱网络环境时,UDP可以节省带宽和握手过程,提高并发

传输层和NAT

TCP之上,必然少不了心跳,心跳包的存在,可以保障TCP链路维持通路,保持连接状态,保障消息可以收发。UDP之上,也需要心跳,用于记录最新的来源IP和端口。

心跳技术的应用,这都归结于NAT技术对于每一跳的网关端口都有有限的生存时间,使得TCP连接、UDP端口通道成为不可靠的临时地址和端口,因此,TCP需要通过心跳维持NAT配发的IP和端口,UDP更要通过心跳更新服务器持有的用户的外网IP和端口。

NAT和不同的网络

不同的网络使用不同的时长的NAT,有的长至半小时不等,有的短至几分钟不等,尤其对于不同的运营商,不同的网络如光纤网、2G、3G、4G、WiFi路由器,多一层路由跳点就多了一个不能确定NAT连接时长。因此,原则上是心跳越短越好,但是由于要减少耗电、减少流量和并发服务器的压力,还是需要适当延长心跳时长。3到15分钟均是合理区间,15分钟以上,则可能导致过早断开服务器的连接,导致消息大幅度延迟。

应用层协议

我们常见的协议无外乎这几种:XMPP、MQTT、Protobuf、JSON、自定义协议

XMPP:重,耗电,流量大,复杂,但是业务成熟

MQTT:中,需要自己扩展业务

Protobuf:轻、易用、技术成熟,只是需要自己编写业务,有一定的工作量

JSON: 轻、易用、技术成熟、协议需要自己定义,工作量稍多

完全自定义协议:轻重可调,易用性可调,技术看开发者,工作量天大

根据现实情况遴选,不过很多情况下,选用Protobuf或者JSON。

当然, 还有很多情况不排除使用thrift、gRPC等高性能的远程方法调用的协议去弯道超车。

消息协议路由

对于不同的消息协议,自然对应了不同的业务数据包,每个业务数据包都要有用于标识自己是何种业务的路由标识,可以仿照REST来设计业务路由,也可以仿照消息码方式设计业务路由。

消息中转

对于消息服务器来说,消息中转需要更短的时间存取数据、解析和业务转发,以获得单机更高并发量。

4、缓存和存储

未读消息缓存

每一条聊天消息都要进入缓存队列中,无论是否正确从TCP/UDP连接发送至客户端上,只有在客户端返回该条信息id的接收回执,才能够允许从未读消息缓存中删除。

历史消息缓存

如果借鉴微信的做法,这个做法是非必要的。也就是我们没有必要存储用户的信息在服务器上。这就意味着,聊天过的消息仅仅存储在客户端机的数据库中,一旦删除,则永久失去。

如果保存,可以考虑使用列数据库存储历史消息,存储为时间序列格式数据,以保证读写性能耗时最短,以及大数据库的扩展性。

用户在线信息缓存

用户在线信息需要缓存,用于记录当前用户在线状态,并且为消息中转的地址策略。

以上信息除历史消息外,都可以考虑存储在redis当中。未读消息相对更加中性和短时性,所以可以存储于redis数据库中。

5、高性能并发组件

高并发网络组件

在一般的Web系统当中,高性能不一定意味着高并发,但是高并发一定意味着高性能。

但是在相对Web软件系统更加实时的IM系统当中,高性能和高并发就成为了几乎同时存在的对称要求。这就意味着,高性能要伴随高并发,高并发意味着高性能。

如何实现这一点?

传统的一个连接由单独一个线程负责的循环阻塞式的线程模型,仍然存在着相当不小的处理延迟和性能瓶颈。而对于更加先进的NIO非阻塞线程模型,配合线程池和消息队列,可以提供更高的性能和异步消息处理。近年在Java 1.7中出现的比NIO还要先进的更高性能的异步IO——AIO,能够提供比NIO还要好的并发性能。基于NIO的组件Netty性能十分强悍,单机吞吐量可以达到5w/s以上,实际的服务器吞吐量可能可以达到10w+/s的超优秀并发性能。

另外,Golang的go关键字提供的原生CSP线程模型,也能达到近似Netty不相上下的高性能吞吐水准。此外,Node.js、Undertow、Vert.X都能提供很好的WebSocket高吞吐性能。

性能的保障——线程池

异步执行的做法有两种,一种是线程池并发执行,另一种是消息队列执行

线程池并发性能不差,但是对于业务解耦性不是很好,而且极容易造成引用的内存泄漏(memory leak),所以,这种情况一般用于单机IM系统的并发,不适用于多机集群或分布式IM系统的并发

分布式性能的保障——异步消息队列

有的读写查询操作时长比较长,业务比较复杂,高数量的连接并发以后,复杂的业务会导致并发量成倍数或指数下降,因此,我们可以通过消息队列的方式将操作异步分发进入到上游服务器中,排队操作,而不必等待执行结束继续后面的响应操作,快速释放连接资源。

推荐的消息队列:ZeroMQ、RabbitMQ,Kafka,ActiveMQ,NSQ等

6、高可用性组件

对于Web软件系统来说,高可用,可以通过使用反向代理后端集群的方式实现,而对于以长连接为基础的高可用可能会更加麻烦。

集群,连接反向代理和负载均衡

当我们讨论可用性时,是在讨论什么?大多数情况下,我们要减少停机时间,或者0停机,我们必要需要有一个网关,或者暴露多个连接服务器。早期的QQ是多个连接服务器,每台服务器保持连接后,后端服务器获取到用户所在服务器进行分发业务和消息中转,这就保障了任意一台服务器停机还有其它的服务器可以用的好处,麻烦是对于客户端的开发来说,需要获取一个服务器列表,然后自己来根据负载选择服务器连接。对于Web业务来说,已经有成熟的组件可以这么做了,但是对于IM来说,编写一个连接网关,这种做法仍然不太容易。

因此、如果对于能力比较强的开发者,可以开发一个总的连接网关,为后端提供反向代理和负载均衡。

分布式策略

有了连接集群,那就一定少不了后端分布式业务服务器,为了保证快速响应,保证吞吐量,保证分发,那么一定要在后端增加业务服务器数量,至于对于业务的拆分,可能需要根据业务本身是否会造成服务器压力来进行拆分,其次考虑业务关联程度拆分。

这牵扯到一个分布式架构的设计。但是我们设计的是一个IM系统,我们实在没必要像大型Web软件系统那样拆分特别细的粒度。因为IM系统的设计一切都是为了并发性能。

状态数据保持

很多时候,我们会把数据作为最后端为我们持久化数据存储。并且很多时候,也需要为一些持久化的缓存保存数据,这时候就不可避免会用到redis多机配置,zookeeper共享数据,或者Hazelcast作为分布式一致性存储后端,保证停机恢复后的状态还原。

7、可扩展性组件

业务扩展性。

Java的Spring提供了最好的扩展性,通过依赖倒置IoC的方式提供对一些接口的实现。这种方式提供了最大的扩展性。对于IM系统来说,扩展性体现在以下几点:

协议的扩展性

协议整体可以增加业务以及扩展数据,我们可以通过v1、v2方式来保证接口业务和客户端升级,可能会导致停机

插件扩展性

设计插件机制提供扩展性,提供一致的组件通讯接口来实现非停机增加插件、减少插件和升级插件的方式来升级业务。

组件通讯和远程方法调用

如果要设计时加入组件化机制,那么由于组件化、动态化,组件之间时不允许有直接引用进行操作,而是通过消息队列、远程方法调用等方式实现通讯,以便于让组件可以分开进程甚至分布到多机上独立执行。

选型建议

插件扩展性此种方式,更加趋向于动态化语言的使用、动态化逻辑的实现。Java的jar文件可以通过类加载器实现动态加载,C/C++可以通过动态dll或者so文件来实现动态加载插件,Python和Js均可以实现动态执行脚本来实现。唯一可惜的是Golang暂时只能通过扩展Lua等胶水语言的方式实现动态扩展。

8、安全性

加密策略

如果有条件,可以加入SSL或者TLS的方式加密客户端和服务器的通讯连接,对于协议中的聊天内容,可以通过动态对称式共享密钥进行加密和解密,对于用户令牌(token),可以用上文提到的用户认证系统来提供有时限的Token,以确保防止被冒用用户,和中间人抓包读取。

9、部署和运维

部署和运维是一个技术活,也是个艺术活。

容器技术的应用

我们至少要知道容器化技术的好处,例如Docker或者Kubernetes(K8s)。

对于我们每个服务器,都要包装成镜像,提交到Docker或者K8s上启动为容器来运行,对于Docker的编排和K8s,好处大家应该是有目共睹的,部署、更新、转移等,容器化是最轻量最方便的技术,比VPS方便的多。提供一次安装,处处部署的高度便利。

10、运行状态监控

技术后台设计指导:针对用户在线状态、登录IP、线程池状态、连接池状态、CPU状态、内存状态、IO、Network等,可以提供远程重启等一些必要操作,还要提供一些动态配置属性作为非停机的后台配置,这十分有用,对于这些,OpenFire是一个出色的参考。

11、项目模块化和集成

上述很多功能离开不了工程上模块化工具对于模块的依赖树管理,以及集成打包和部署工具。对于Java工程来说,maven和gradle是当仁不让的模块管理和集成工具。对于Go语言来说,Bazel是个不错的选择。

对于IM系统工程模块化的工作流程:

1、设计和切分功能模块
2、设计模块之间依赖关系
3、动态化和环境化属性配置和设计
4、分布式模块的构建和部署脚本
5、CI、持续部署系统的建立
6、虚拟客户端的协议和通讯测试
7、模拟生产环境测试

12、对于不同语言使用者的建议

对于Java使用者:

设计IM系统之前,有两个建议:

1、尽量早、或者一开始就引入Spring有关框架,比如使用Spring JPA加快对数据库部分的开发效率,并且将有关Spring的可用的组件及早引入使用,以确保分布式程序或者单机程序健壮性

2、尽量晚、或者不使用一切与Spring有关的框架,对于数据库,可以使用MyBatis或者Hibernate加快对数据库部分的开发效率,让IM整个系统中间件化,微服务化。如果需要Web或者REST,可以考虑引入Jersey

对于Golang使用者:

自由度高。接口可以考虑使用iris、beego、gin、echo等框架来提供。

天生有rpc,天生的分布式和集群支持。支持docker方便。

缺少Spring这一类框架,大量业务对象的实例管理更麻烦。