一篇文章教你如何设计一个百万级的消息推送系统

2020年05月

一篇文章教你如何设计一个百万级的消息推送系统

一篇文章教你如何设计一个百万级的消息推送系统

前语

先简略说下本次的主题,因为我最近做的是物联网相关的开发作业,其间就难免会遇到和设备的交互。

最首要的作业便是要有一个体系来支撑设备的接入、向设备推送音讯;一起还得满意很多设备接入的需求。

所以本次共享的内容不光能够满意物联网范畴一起还支撑以下场景:

  • 依据 WEB 的谈天体系(点对点、群聊)。
  • WEB 运用中需求服务端推送的场景。
  • 依据 SDK 的音讯推送渠道。

技能选型

要满意很多的衔接数、一起支撑双全工通讯,而且功用也得有确保。

在 Java 技能栈中进行选型首要天然是排除去一篇文章教你怎样规划一个百万级的音讯推送体系了传统 IO。

那就只要选 NIO 了,在这个层面其实挑选也不多,考虑到社区、材料保护等方面终究挑选了 Netty。

终究的架构图如下:

现在看着蒙不要紧,下文逐个介绍。

协议解析

已然是一个音讯系标签8统,那天然得和客户端界说好两边的协议格局。

常见和简略的是 HTTP 协议,但咱们的需求中有一项需求是双全工的交互办法,一起 HTTP 更多的是服务于浏览器。咱们需求的是一个愈加精简的协议,削减许多不必要的数据传输。

因而我觉得最好是在满意事务需求的状况下定制自己的私有协议,在我这个场景下其实有规范的物联网协议。

假如是其他场景能够学习现在盛行的 RPC 结构定制私有协议,使得两边通讯愈加高效。

不过依据这段时刻的经历来看,标签22不管是哪种办法都得在协议中预留安全相关的方位。

协议相关的内容就不过评论了,更多介绍具体的运用。

简略完成

首要考虑怎样完成功用,再来考虑百万衔接的状况。

注册鉴权

在做真实的音讯上、下行之前首要要考虑的便是鉴权问题。

就像你运用微信相同,第一步怎样也得是登录吧,不能不管是谁都能够直接衔接到渠道。

所以第一步得是注册才行。

如上面架构图中的 注册/鉴权 模块。一般来说都需求客户端经过标签10 HTTP 恳求传递一个仅有标识,后台鉴权经过之后会呼应一个 token,并将这个 token 和客户端的联系保护到 Redis 或者是 DB 中。

客户端将这个 token 也保存到本地,往后的每一次恳求都得带上这个 token。一旦这个 token 过期,客户端需求再次请标签15求获取 token。

鉴权经过之后客户端会直接经过 TCP长衔接到图中的 push-server 模块。

这个模块便是真实处理音讯的上、下行。

保存通道联系

在衔接接入之后,真实处理事务之前需求将当时的客户端和 Channel 的联系保护起来。

假定客户端的仅有标识是手机号码,那就需求把手机号码和当时的 Channel 保护到一个 Map 中。标签10

这点和之前 SpringBoot 整合长衔接心跳机制 相似。

一起为了能够经过 Channel 获取到客户端仅有标识(手机号码),还需求在 Channel 中设置对应的特点:

获取时手机号码时:

这样当咱们客户端下线的时便能够记载相关日志:

这儿有一点需求留意:寄存客户端与 Channel 联系的 Map 最好是预设好大小一篇文章教你怎样规划一个百万级的音讯推送体系(防止常常扩容),因为它将是运用最为频频一起也是占用内存最大的一个目标。

音讯上行

接下来则是真实的事务数据上传,一般来说第一步是需求判别上传音讯输入什么事务类型。

在谈天场景中,有或许上传的是文本、图片、视频等内容。

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

  • 能够运用音讯头中的某个字段进行区别。
  • 更简略的便是一个 JSON 音讯,拿出一个字段用于区别不同音讯。

不管是哪标签2种只要能够区别出来即可。

音讯解析与事务解耦

音讯能够解析之后便是处理事务,比方能够是写入数据库、调用其他接口等。

咱们都知道在 Netty 中处理音讯一般是在 channelRead() 办法中。

在这儿能够解析音讯,区别类型。

但假如咱们的事务逻辑也写在里边,那这儿的内容将是巨多无比。

乃至咱们分为好几个开发来处理不同的事务,这样将会呈现许多抵触、难以保护等问题。

所以十分有必要将音讯解析与事务处理彻底别离开来。

这时面向接口编程就发挥效果了。

这儿的中心代码和 「造个轮子」——cicada(轻量级 WEB 结构) 是共同的。

都是先界说一个接口用于处理事务逻辑,然后在解析音讯之后经过反射创立具体的目标履行其间的 处理函数即可。

这样不同的事务、不同的开发人员只需求完成这个接口一起完成自己的事务逻辑即可。

伪代码如下:

上行还有一点需求留意;由所以依据长衔接,所以客户端需求定时发送心跳包用于保护本次衔接。一起服务端也会有相应的检查,N 个时刻距离没有收到音讯之后将会主动断开衔接节约资源。

这点运用一个 IdleStateHand一篇文章教你怎样规划一个百万级的音讯推送体系ler 就可完成,更多内容能够检查 Netty(一) SpringBoot 整合长衔接心跳机制。

音讯下行

有了上行天然也有下行。比方在谈天的场景中,有两个客户端连上了 push-server,他们直接需求点对点通讯。

这时的流程是:

  • A 将音讯发送给服务器。
  • 服务器收到音讯之后,得知音讯是要发送给 B,需求在内存中找到 B 的 Channel。
  • 经过 B 的 Channel 将 A 的音讯转发下去。

这便是一个下行的流程。

乃至办理员需求给一切在线用户发送体系告诉也是相似:

遍历保存通标签10道联系的 Map,挨个发送音讯即可。这也是之前需求寄存到 Map 中的首要原因。

伪代码如下:

分布式计划

单机版的完成了,现在侧重讲讲怎样完成百万衔接。

百万衔接其实仅仅一个形容词,更多的是想表达怎样来完成一个分布式的计划,能够灵敏的水平拓宽然后能支撑更多的衔接。

再做这个事前首要得搞清楚咱们单机版的能支标签11持多少衔接。影响这个的要素就比较多了。

  • 服务器自身装备。内存、CPU、网卡、Linux 支撑的最大文件翻开数等。
  • 运用自身装备,因为 Netty 自身需求依赖于堆外内存,可是 JVM 自身也是需求占用一部分内存的,比方寄存通道联系的大 Map。这点需求结合自身状况进行调整。

结合以上的状况能够测试出单个节点能支撑的最大衔接数。

单机不管怎样优化都是有上限的,这也是分布式首要处理的问题。

架构介绍

在将具体完成之前首要得讲讲上文贴出的全体架构图。

先从左面开端。

上文说到的 注册鉴权 模块也是集群布置的,经过前置的 Nginx 进行负载。之前也提过了一篇文章教你怎样规划一个百万级的音讯推送体系它首要的意图是来做鉴权并回来一个 token 给客户端。

可是 push-server 集群之后它又多了一个效果。那便是得回来一台可供当时客户端运用的 push-server。

右侧的 渠道 一般指办理渠道,它能够检查当时的实时在线数、给指定客户端推送音讯等。

推送音讯则需求经过一个推送路由( push-server)找到真实的推送节点。

其他的中间件如:Redis、Zookeeper、Kafka、MySQL 都是为了这些功用所预备的,具体看下面的完成。

注册发现

首要第一个问题则是 注册发现, push-server 变为多台之后怎样给客户端挑选一台可用的节点是第一个需求处理的。

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

一切的 push-server 在发动时分需求将自身的信息注册到 Zookeeper 中。

注册鉴权 模块会订阅 Zookeeper 中的节点,然后能够获取最新的服务列表。结构如下:

以下是一些伪代码:

运用发动注册 Zookeeper。

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

路由战略

已然能获取到一切的服务列表,那怎样挑选一台刚好适宜的 push-server 给标签2客户端运用呢?

这个进程要点要考虑以下几点:标签7

  • 尽量确保各个节点的衔接均匀。
  • 增删节点是否要做 Rebalance。

首要确保均衡有以下几种算法:

  • 轮询。挨个将各个节点分配给客户端。但会呈现新增节点分配不均匀的状况。
  • Hash 取模的办法。相似于 HashMap,但也会呈现轮询的问题。当然也能够像 HashMap 那样做一次 Rebalance,让一切的客户端重一篇文章教你怎样规划一个百万级的音讯推送体系新衔接。不过这样会导致一切的衔接呈现中止重标签8连,价值有点大。
  • 因为 Hash 取模办法的问题带来了 共同性Hash算法,但仍然会有一部分的客户端需求 Rebalance。
  • 权重。能够手动调整各个节点的负载状况,乃至能够做成主动的,依据监控当某些节点负载较高就主动调低权重,负载较低的能够进步权重。

还有一篇文章教你怎样规划一个百万级的音讯推送体系一个问题是:

当咱们在重启部分运用进行晋级时,在该节点上的客户端怎样处理?

因为咱们有心跳机制,当心跳不通之后就能够以为该节点呈现问题了。那就得从头恳求 注册鉴权模块获取一个可用的节点。在弱网状况下相同适用。

假如这时客户端正在发送音讯,则需求将音讯保存到标签12本地等候获取到新的节点之后再次发送。

有状况衔接

在这样的场景中不像是 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 标签19做事务明显不适宜,这时彻底能够挑选 Kafka 来解耦。

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

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

其实这块内容也很值得评论,能够先看这篇了解下:强如 Disruptor 也发作内存溢出?

后续谈到 Kafka 再做具体介绍。

分布式问题

分布式处理了功用问题但却带来了其他费事。

运用监控

比方怎样知道线上几十个 push-server 节点的健康状况?

这时就得监控体系发挥效果了,咱们需求知道各个节点当时的内存运用状况、GC。

以及操作体系自身的内存运用,究竟 Netty 很多运用了堆外内存。

一起需求监控各个节点当时的在线数,以及 Redis 中的在线数。理论上这两个数应该是持平的。

这样也能够知道体系的运用状况,能够灵敏的保护这些节点数量。

日志处理

日志记载也变得反常重要了,比方哪天反应有个客户端一向连不上,你得知道问题出在哪里。

最好是给每次恳求都加上一个 traceID 记载日志,这样就能够经过这个日志在各个节点中检查到底是卡在了哪里。

以及 ELK 这些东西都得用起来才行。

总结

本次是结合我日常经历得出的,有些坑或许在作业中并没有踩到,一切还会有一些遗失的当地。

就现在来看想做一个安稳的推送体系其实是比较费事的,其间涉及到的点十分多,只要真实做过之后才会知道。

发表评论

电子邮件地址不会被公开。 必填项已用*标注