导航菜单
路很长,又很短
博主信息
昵   称:Cocodroid ->关于我
Q     Q:2531075716
博文数:356
阅读量:1744884
访问量:218072
至今:
×
云标签 标签球>>
云标签 - Su的技术博客
Tags : 亿级流量,架构,API网关发表时间: 2021-11-03 14:18:58


作者:张松然 松然聊技术

这不是一个讲概念的专栏,而且我也不擅长讲概念,每一篇文章都是一个故事,我希望你可以通过这些故事了解我当时在实际工作中遇到问题和背后的思考,架构设计是种经验,我有幸参与到多个亿级系统的架构设计中,有所收获的同时也希望把这些收获分享与大家。


承接上篇,客户端通过调用 API 网关获取数据,但实时数据的获取,如果通过轮询网关,大量空转不仅非常的低效且浪费服务器资源。基于此,实现了一种消息推送技术,提供一个实时的、可靠的、异步的双向数据交换通道,提升 API 网关性能。

消息平台既负责消息的接入,又负责消息的推送。为了支持多数据源的高性能消息推送,消息整体架构的改造包括以下几点:

一、解耦消息接入层和消息推送层,消息接入层只负责 Request-Response 和 Notice-Repley,而消息解析、适配、推送等逻辑处理都全部由消息推送层处理,而消息接入层和消息推送层之间则有消息队列异步进行通信。

二、消息存储由原来的半推半拉改为半推半查,即消息只推送通知,所以消息实体存储在云端。采用 ElasticSearch 作为消息搜索引擎,通过 Routing 实现性能查询。

三、优化推送方式,将 TCP 通知由异步改同步,计算消息送到率。

如下图所示:

图片

随着逐步对 NIO 的深入学习和对 Netty 框架的了解,采用 NIO 技术应用消息推送系统中,HTTP 长连接采用 Servlet 3 的特性,TCP 长连接采用了 Netty 4 框架,最终在2017年实现,并完全支撑业务化运行。


1。消息接入

首先,原来消息接入的痛点主要有接入方式不统一、推送不稳定、大促被降级、消息处理逻辑复杂、接入新的消息源困难、没有完善的消息追踪、消息统计等。

基于上述原因,重构优化实现了统一的消息接入;解决消息量过大对系统造成的积压问题;使用了管道分发的模式和模块化可插拔的处理方式,使消息源的接入变的极其简单,大大的缩短了开发的周期;实现全链路消息追踪和统计。

配置中心

另一个比较大的优化是呼起协议配置化,之前消息的呼起协议是写死在消息体里面,极其的不灵活,甚至很多系统消息无法对接呼起协议直接将链接暴露在消息体里,用户的体验是很不好的。为此,呼起协议对接统一协议管理中心,所有的呼起协议会根据消息里携带的 protocolID 从统一协议管理中心获取。呼起协议的中心化、配置化使得消息在系统流转的过程中不再需要关注具体的呼起协议,简化了消息在系统中的处理逻辑。而且协议中心化之后,协议的内容可以直接呈现给产品和运营,整个消息呼起的过程变得更加的清晰。


2。消息推送

其次,原来的消息推送都是通过客户端轮询拉取的方式,而且消息源也很多,格式不统一,处理复杂。且由于数据源较多、消息量大,推送性能成为瓶颈。基于此,构建了整个消息处理架构,通过优化线程池的参数和阻塞队列的大小、Redis 的管道、梳理关键路径等,推送性能提升由推送1万消息终端一次5~15分钟缩短到1分钟内。

上文提到,消息从消息源被发送出来,经过消息中心,消息路由解析定位 keep-alive 客户端,生成消息通知,交由 HTTP Push 或 TCP Push 推送到客户端,客户端收到通知后,再请求消息服务端,将消息实体拉取到客户端本地,存库进行展示。这里有两个比较大的改动,一个是从半推半拉改成半推半查

半推半拉模式中的“推”指的是由服务器推送消息通知到客户端,“拉”指的是客户端收到通知后再从服务器拉取消息实体到客户端本地存储。其中消息通知发送的仅是一个命令关键字,这样的设计是考虑消息推送可能存在丢失,通过拉取的方式,确保即使消息通知未送达,在下次消息通知触发下的拉取也能把上一次消息拉取到本地。采用的半推半拉,每次仅推送通知,推送量小,实时性高。

半推半查模式中“查”指的是消息通知依旧推送,但客户端收到消息通知后不再拉取消息实体,仅更新消息未读数和进行消息提醒等操作,而消息内容则是由服务端进行云端存储,采用轻客户端,重服务端的架构方案,只有用户点击查询消息时,才会按需进行数据查询,在客户端展示,但不存储。

这种推送模式的改动主要考虑了客户端拉取消息内容到本地存储,占用资源,重装之后客户端会丢失消息,以及多端存储的数据存在不一致等问题。

另一个改动就是从 Redis 队列到 ElasticSearch,这里其实也跟上一个改动点有密切的关联,原来的拉取模型里,消息中心需要为每个用户的消息存储一个 Redis 队列,当有通知下发后,客户端调用服务端从 Redis 队列拉下去,这里就有几个问题,一是如果客户端长期不拉取,服务端的资源就不会被释放;二是长时间后登录,会一下拉取大量数据造成阻塞;三是如果客户在多端登录,数据是不能被多端拉取的。

所以,改造之后数据在云端基于 ElasticSearch 进行消息存储,并根据业务类型区分索引,最开始的时候 ElasticSerach 查询存在性能问题,通过 Routing 优化查询性能,支持多维度进行查询,性能稳定。

图片

具体实现是 ElasticSerach 在进行 Query 查询时,会由命中节点分发给所有 Node,每个 Node 处理请求 Merge 后,返回给命中节点,命中节点在 Merge 所有 Node 返回的结果,最终返回。这样请求性能很低,通过对 ElasticSearch 自定义 Routing,实现 Index 和 Query 保证在一个 Shard,极大的提供的 Query 性能。


3。消息送达率

接下来,评估消息系统的一个核心指标是消息送达率。为保证每一条消息准确送达,为每条消息都会开启一个事务,从推送开始,到确认结束,如果超时未确认就会重发这条消息,这就是消息确认。

由于互联网环境复杂,消息超时时间不能设置太短,尤其在移动弱网络环境下。在本系统的中超时设置为 10 秒。

图片

那么如何确认消息送达呢?TCP 是一个异步请求,它不像 HTTP,TCP 只要把数据推送到网卡就会返回成功,那如果将下行通知与上行返回的应答进行关联呢?

由于消息系统基于 Netty 框架的 TCP 网关,我们通过实现 Future 自定义 NotifyFuture,为每个下行通知分配一个 seq,并定义 NotifyFuture 的 timeout。即每个下行通知分配一个 seq 存储缓存中,等待客户端回应这个应答,如果应答,则从缓存移出这个 seq,否则等待超时,自动从缓存中被移出。


4。消息监控

全链路消息追踪系统,是整合了从消息源到最终的消息推送,整个链路各个节点消息的流转状况,并且异步化存储。系统的处理方式是基于 MQ 实现将消息日志分别存储在 ES 和 HBase,存 ES 保证了我可以在消息管理后台对所有消息进行清晰透明化的追踪查询,存 HBase 是为了可以将数据长久的保存并且进一步的分析。


5。APNs

消息触达分为在线通知和离线通知。在线通知是通过服务端和客户端的 TCP 长连接来实现的。离线通知在最开始只有 iOS 的 APNs 推送,Android 系统无法很好的进行离线通知的推送一直是一大痛点。在这种情况下我们开发了 Android 推送的开源包,对接了华为、小米、魅族三大厂商,实现了 Android 离线通知的推送。

Android 在休眠时可以启动后台进程保持长连接,iOS 休眠不能启动后台。所以通过 APNS 向苹果服务器进行消息推送。iOS 在系统层面与苹果 APNs(Apple Push Notification Service)服务器建立连接,应用通过 Socket 向 APNs Server 推送消息,然后再由 APNs 进行推送。但是基于 Socket 的 APNs 协议是一种反人类的设计,在推送消息存在很多问题。

鉴于此,对 APNs 推送服务进行重构,基于 Netty 构建了 HTTP2 协议的推送服务,支持同步和异步的推送方式;解决 Channel 异常及 InActive 时重连等问题,保证 HTTP2 推送管道的问题;同时通过 IdleStateHandler 保持 HTTP2 长连接的心跳 。

IOS PUSH

图片


6。设计模式

最后,说一下消息中心管道的设计,管道是解决不同消息类型的业务编排问题,比如文本消息和图文消息的过滤、组装、存储和推送的具体实现是不一样,这就导致上层建筑的抽象是相似的,但是具体的实现是不同。所以,在重构消息推送的一段代码,发现之前的流程大概是如下图所示:

图片

这是一个发送消息的流程,长长的一段代码,通过依赖构成整个流程的架构。整个流程依赖三个环节:适配、发送、保存,其中发送又依赖消息体生成。由于消息类型不同,适配、发送、保存的具体实现不同,所以代码中大量的充斥着 if ... else ... 的判断语句。这不仅导致代码可读性很差,而且还产生了大量冗余代码。

针对如上问题,优化通过组合的方式重新封装,如下图所示:

图片

提取一个更高的抽象(Mesasge),这里组合了 Sender 和 Saver 两个行为,这里只组合行为,不组合实现。这样的好处,是外观代码变得更加清晰。这样的设计就好比,Sender 像一张弓,Message 像一支箭,getSender() 可以获取不同的弓,Message 也可以是不同的箭,通过不同的实现随意组合。通过组合的方式,就可以在不改变外观行为的框架下,构造针对不同特定的具体实现。


7。总结

言而总之,本篇文章重点讲述了架构演进重构消息PUSH系统的消息推送、消息送达率、APNs。下篇文章,我将开始介绍从焦油坑爬出来的交易系统。如果你觉得有收获,欢迎你把今天的内容分享给更多的朋友。


8。扩展阅读

故事1:从零构建亿级流量API网关
01 | API网关:统一接入、分层架构、高可用架构
02 | 流量调度:配置中心、泛化调用

故事2:架构演进构建TCP长连接网关
03 | TCP网关:Netty框架、Protobuf格式、业务线程池
04 | TCP长连接:心跳、Session管理、断线重连

故事3:架构演进重构消息PUSH系统
05 | 消息PUSH:消息推送、消息送达率、APNs

故事4:从焦油坑爬出来的交易系统
06 | 交易平台:订单管道、订单状态机、服务编排、任务引擎
07 | 微服务化:服务治理、领域设计

故事5:烦人的焦油开始到处都是
08 | 新老系统:业务整合、数据融合、系统迁移
09 | 高可用架构:隔离部署、系统监控与日志、可灰度、可降级

故事6:稳定性架构与大促保障
10 | 大道至简:系统复杂度、三明治架构
11 | 大促保障:自动化测试、故障演练、性能压测


...阅读原文
推荐文章