写点什么

设计一个百万级的消息推送系统,mybatis 技术原理

用户头像
极客good
关注
发布于: 刚刚

所以我们得进行区分,来做不同的处理;这就和客户端协商的协议有关了。


可以利用消息头中的某个字段进行区分。


更简单的就是一个 JSON 消息,拿出一个字段用于区分不同消息。


不管是哪种只有可以区分出来即可。


消息解析与业务解耦


消息可以解析之后便是处理业务,比如可以是写入数据库、调用其他接口等。


我们都知道在 Netty 中处理消息一般是在 channelRead() 方法中。



在这里可以解析消息,区分类型。


但如果我们的业务逻辑也写在里面,那这里的内容将是巨多无比。


甚至我们分为好几个开发来处理不同的业务,这样将会出现许多冲突、难以维护等问题。


所以非常有必要将消息解析与业务处理完全分离开来。


这时面向接口编程就发挥作用了。


这里的核心代码和 「造个轮子」——cicada(轻量级 WEB 框架) 是一致的。


都是先定义一个接口用于处理业务逻辑,然后在解析消息之后通过反射创建具体的对象执行其中的处理函数即可。


这样不同的业务、不同的开发人员只需要实现这个接口同时实现自己的业务逻辑即可。


伪代码如下:




想要了解 cicada 的具体实现请点击这里:


github.com/TogetherOS/…


上行还有一点需要注意;由于是基于长连接,所以客户端需要定期发送心跳包用于维护本次连接。同时服务端也会有相应的检查,N 个时间间隔没有收到消息之后将会主动断开连接节省资源。


这点使用一个 IdleStateHandler 就可实现,更多内容可以查看 Netty(一) SpringBoot 整合长连接心跳机制。


消息下行


有了上行自然也有下行。比如在聊天的场景中,有两个客户端连上了 push-server,他们直接需要点对点通信。


这时的流程是:


A 将消息发送给服务器。


服务器收到消息之后,得知消息是要发送给 B,需要在内存中找到 B 的 Channel。


通过 B 的 Channel 将 A 的消息转发下去。


这就是一个下行的流程。


甚至管理员需要给所有在线用户发送系统通知也是类似:


遍历保存通道关系的 Map,挨个发送消息即可。这也是之前需要存放到 Map 中的主要原因。


伪代码如下:



具体可以参考:


github.com/crossoverJi…


分布式方案


单机版的实现了,现在着重讲讲如何实现百万连接。


百万连接其实只是一个形容词,更多的是想表达如何来实现一个分布式的方案,可以灵活的水平拓展从而能支持更多的连接。


在做这个事前首先得搞清楚我们单机版的能支持多少连接。影响这个的因素就比较多了。


服务器自身配置。内存、CPU、网卡、Linux 支持的最大文件打开数等。


应用自身配置,因为 Netty 本身需要依赖于堆外内存,但是 JVM 本身也是需要占用一部分内存的,比如存放通道关系的大 Map。这点需要结合自身情况进行调整。


结合以上的情况可以测试出单个节点能支持的最大连接数。


单机无论怎么优化都是有上限的,这也是分布式主要解决的问题。


架构介绍


在将具体实现之前首先得讲讲上文贴出的整体架构图。


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码



先从左边开始。


上文提到的 注册鉴权 模块也是集群部署的,通过前置的 Nginx 进行负载。之前也提过了它主要的目的是来做鉴权并返回一个 token 给客户端。


但是 push-server 集群之后它又多了一个作用。那就是得返回一台可供当前客户端使用的 push-server。


右侧的 平台 一般指管理平台,它可以查看当前的实时在线数、给指定客户端推送消息等。


推送消息则需要经过一个推送路由(push-server)找到真正的推送节点。


其余的中间件如:Redis、Zookeeper、Kafka、MySQL 都是为了这些功能所准备的,具体看下面的实现。


注册发现


首先第一个问题则是 注册发现,push-server 变为多台之后如何给客户端选择一台可用的节点是第一个需要解决的。


这块的内容其实已经在 分布式(一) 搞定服务注册与发现 中详细讲过了。


所有的 push-server 在启动时候需要将自身的信息注册到 Zookeeper 中。


注册鉴权 模块会订阅 Zookeeper 中的节点,从而可以获取最新的服务列表。结构如下:



以下是一些伪代码:


应用启动注册 Zookeeper。




对于注册鉴权模块来说只需要订阅这个 Zookeeper 节点:



路由策略


既然能获取到所有的服务列表,那如何选择一台刚好合适的 push-server 给客户端使用呢?


这个过程重点要考虑以下几点:


尽量保证各个节点的连接均匀。


增删节点是否要做 Rebalance。


首先保证均衡有以下几种算法:


轮询。挨个将各个节点分配给客户端。但会出现新增节点分配不均匀的情况。


Hash 取模的方式。类似于 HashMap,但也会出现轮询的问题。当然也可以像 HashMap 那样做一次 Rebalance,让所有的客户端重新连接。不过这样会导致所有的连接出现中断重连,代价有点大。


由于 Hash 取模方式的问题带来了一致性 Hash 算法,但依然会有一部分的客户端需要 Rebalance。


权重。可以手动调整各个节点的负载情况,甚至可以做成自动的,基于监控当某些节点负载较高就自动调低权重,负载较低的可以提高权重。


还有一个问题是:


当我们在重启部分应用进行升级时,在该节点上的客户端怎么处理?


由于我们有心跳机制,当心跳不通之后就可以认为该节点出现问题了。那就得重新请求注册鉴权模块获取一个可用的节点。在弱网情况下同样适用。


如果这时客户端正在发送消息,则需要将消息保存到本地等待获取到新的节点之后再次发送。


有状态连接


在这样的场景中不像是 HTTP 那样是无状态的,我们得明确的知道各个客户端和连接的关系。


在上文的单机版中我们将这个关系保存到本地的缓存中,但在分布式环境中显然行不通了。


比如在平台向客户端推送消息的时候,它得首先知道这个客户端的通道保存在哪一节点上。


借助我们以前的经验,这样的问题自然得引入一个第三方中间件用来存放这个关系。


也就是架构图中的存放路由关系的 Redis,在客户端接入 push-server 时需要将当前客户端唯一标识和服务节点的 ip+port 存进 Redis。


同时在客户端下线时候得在 Redis 中删掉这个连接关系。


这样在理想情况下各个节点内存中的 map 关系加起来应该正好等于 Redis 中的数据。


伪代码如下:



这里存放路由关系的时候会有并发问题,最好是换为一个 lua 脚本。


推送路由


设想这样一个场景:管理员需要给最近注册的客户端推送一个系统消息会怎么做?


结合架构图


假设这批客户端有 10W 个,首先我们需要将这批号码通过平台下的 Nginx 下发到一个推送路由中。


为了提高效率甚至可以将这批号码再次分散到每个 push-route 中。


拿到具体号码之后再根据号码的数量启动多线程的方式去之前的路由 Redis 中获取客户端所对应的 push-server。


再通过 HTTP 的方式调用 push-server 进行真正的消息下发(Netty 也很好的支持 HTTP 协议)。


推送成功之后需要将结果更新到数据库中,不在线的客户端可以根据业务再次推送等。


消息流转


也许有些场景对于客户端上行的消息非常看重,需要做持久化,并且消息量非常大。


在 push-sever 做业务显然不合适,这时完全可以选择 Kafka 来解耦。


将所有上行的数据直接往 Kafka 里丢后就不管了。


再由消费程序将数据取出写入数据库中即可。


其实这块内容也很值得讨论,可以先看这篇了解下:强如 Disruptor 也发生内存溢出?

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
设计一个百万级的消息推送系统,mybatis技术原理