前言
你还在羡慕别人成熟的推送系统么?
你想定制自己的推送系统么?
你有内网推送的需求而不能使用外网推送产品的困扰么?
文章将向你介绍研究分布式推送的过程与心得(本人呕心沥血打造......),希望看完文章的你能有所收获。
- 通过这个标题点进来就默认你对websocket有了基本的了解,如果你不知道,这里有个很好的答案websocket是什么。
- 如果你不想听我bb,完整项目传送门(给个star支持一下....3q ^_^),里面有详细的项目介绍与搭建过程。
- websocket协议只是推送的一种实现,当然可以使用其他协议,希望构建的思路可以启发到你。
技术选型
在搜索了n多次websocket这个关键词以后...我选择用netty这个支持nio的高性能网络框架作为推送支持,它帮我们屏蔽了网络底层复杂通信逻辑,提供简单易用的api。(websocket的netty实现网上一搜一大把)
客户端的websocket握手请求如:ws://127.0.0.1:9003/websocket?channelId=123456
将客户端的唯一标识123456
与客户端在我们netty
中抽象出的连接对象channel
,维护至全局变量中
private static Map<String,Channel> channels = new ConcurrentHashMap(1000);
推送逻辑为:根据http
请求或者客户端发过来的websocket
消息格式,解析内容,通过客户端连接的标识channelId
找到对应客户端的连接channel
对象,调用channel.writeAndFlush(new TextWebSocketFrame("需要推送的内容"))
,完成推送。
消息体的两个主要参数为:
- 推送的目标客户端标识(who)
- 推送的内容(what)
到此,我们的推送功能,已基本实现,只是受限于单机各项性能参数,没有任何的拓展性。
初步拆分
我们希望根据系统的业务职能,做初步的拆分,以便未来根据不同业务的负载,做更细粒度的集群。
- portal—负责处理http请求
- websocket—负责处理websocket握手连接,接收和推送websocket消息
集群模式
注册中心与网关
当我们考虑把各个业务模块部署多份时,我们要面对这些问题:
1.需要对外暴露一个统一的入口来路由到我们不同的集群服务
2.对于集群中节点的上下线要做到动态感知
我们加入这两个组件
- gateway—网关,统一入口,动态路由(zuul并不支持websocket)
- eureka—注册中心,动态的感知服务上下线
在这个阶段中,我们需要解决一个推送业务中比较核心的问题:
- 推送请求的路由
我们知道:websocket
与http
瞬时无状态的请求响应不一样,客户端在发起websocket
握手成功后,不会立马断开,会维持住与服务器的tcp
连接,用于全双工通信。在这种情况下,我们的推送请求,就不能任由portal
服务端负载均衡,路由到各个websocket
服务节点上了。
举个栗子:
如上图所示client1
发起websocket
握手时,由网关将握手请求路由给websocket1
节点,client1
会通过网关与websocket1
节点维持一个tcp
通道,那么以http
请求来触发推送时,必须要把对client1
的推送请求指定路由给websocket1
节点来处理(对于以websocket
消息触发的推送请求也是如此,client4
发起对client1
的推送必须由接收到消息的websocket2
节点转发给websocket1
节点处理)。
分布式缓存
此时,我们需要引入redis
来记录各个websocket节点上所维护的客户端
:
-
websocket
服务节点处理完客户端的握手请求以后,将节点与客户端的路由关系保存进redis
-
http
触发推送的流程:portal
接收到推送请求时,根据需要推送的客户端的目的地,从redis
中找到客户端所在的服务器,转发http
请求至客户端所在服务器的websocket
节点,websocket
节点发起推送 -
websocket
消息触发推送的流程:websocket
节点接收到客户端websocket
消息推送请求时,判断需要推送的客户端是否在本节点上,如果是则直接推送,如果不是则转发给对应的其他websocket
节点发起推送
在我当前的项目中,一些地方使用到了redis
的管道特性,所以这里redis
不支持cluster
这种分片的集群部署方式,要想适用分片的集群方式来提高并发只能使用codis
做代理。。要么就单个master
。
消息中间件
上图可见:服务间的调用关系非常复杂,系统间耦合非常高,在线上对端口严格管控的服务器下部署这样一套系统是非常痛苦的,我们考虑引入MQ
解耦:当有需要转发给websocket
节点进行推送时,投递到MQ中对应节点所订阅的主题就行
简单描述mq的选型问题:目前常用的分布式高可用MQ中间件有:RabbitMQ
,kafka
,RocketMQ
RabbitMQ:需要安装Erlang环境。。个人希望系统部署尽量简单,首先就排除(对java有把握一点。。)
kafka:一般用于日志分析,大数据计算
RocketMQ: 相比于kafka,可靠,实时,易用等特性更适用与业务系统交互的场景中,注册中心nameSer也比kafka的zk要轻便(就他了)详细对比传送门
配置中心
到此,功能已基本完成,只是我们现在每部署一个服务节点都需要配置相同的注册中心地址、redis地址、mq地址,当某个中间件地址变动时,整个服务的所有节点都需要相适配,为避免这种繁琐而重复的配置,我们考虑引入配置中心:
- 配置中心存放公共的中间件地址
- 各服务节点从配置中心去获取所需的中间件地址
配置中心选型:
springcloud config:需要依赖git
apollo:需要依赖mysql
nacos:服务自带数据库,没有任何依赖,安装使用简单便捷。(就他了)
现在的完整配置如图:
补充
我们的分布式消息推送系统已经完成了O(∩_∩)O,不过这里存在一个运行上的小问题:
现在从gateway
网关路由websocket
握手请求到各个websocket
服务节点的权重都是相同的,也就是说理论上我们希望各个websocket
节点所维持的客户端连接数量是大致相同的,但是当我们服务部署运行一段时间后,发现各个websocket
节点的负载较高,我们希望增加(或者服务节点重启)websocket
服务节点的数量。但是增加(重启)完websocket
节点以后,gateway
路由到各个websocket
节点的权重依然相同,其实我们希望gateway
网关将websocket
握手请求能优先能路由给新部署上来的websocket
服务节点,即:
- 网关动态权重路由(将客户端websocket握手连接优先分配给较少连接数量的websocket节点)
具体实现在我完整项目传送门中的task
模块
第一次写文章。。希望大家多多支持。。。
认知有限。。如有描述错误的地方还请指出。。。
有问题提交至项目中的issue
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。