关于推送系统设计的一些总结与思考(二)

三、 消息推送的工作模式

** 
常见的消息推送系统的工作模式有:推模式、拉模式以及推拉混合模式三种,在很多推送系统中,采用在线消息直接推送下去,离线消息让客户端拉取,这种方式很容易造成漏消息的问题。本节将介绍几种“特殊定义“的推送模式的特点和应用场景,它们的含义与通常理解的略微有些差异。 
在线用户:个人认为在线用户是指网络正常、弱网、网络异常等情况下的用户,这些用户实际上正在使用系统,只是由于一些客观原因造成网络的暂时断开。从业务角度,我们将这部分用户划归为在线用户。 
离线用户:客户端真实的不在线,例如没有登录。 
在线与离线之间的界限有时候不太明显,尤其是在弱网、网络异常等情况下,我们以前的互联网时代可能认为在线就是在线、离线就是离线,这是非黑即白的看法,但是在移动互联网场景下,这种看法可能要稍微改变一下了。 
对于在线与离线的问题,从服务端的认知角度可能是连续非离线的状态,即存在一个模糊的过渡地带,无论从业务和技术角度服务端都无法准确区分一个连接的状态到底是在线和离线(在本文的TCP传输可靠性相关小节中会有详细描述)。我们试图将这种连续的状态进行离散化,以便我们后续分析和处理,于是我们就将处于过渡地带的网络异常情况下连接的客户端统一划归为在线状态,划分的角度就是用户、业务角度。 
**

3.1 直推模式

** 
在该模式中,发给推送系统的消息被直接封装到通知里,直接通过长连接服务推送下去, 在服务端不会对消息缓存。在该模式中,消息将只发送给离线、弱网和网络异常情况下的在线用户,离线用户将不会收的这些消息。这种模式的消息推送方式有如下优点:

  • 送到速度更快;
  • 更省流量、省电;
  • 服务端实现简单;

该模式的缺陷就是离线用户将不会收到直推的消息。 
直推模式有它的应用场景:消息对于在线用户有意义,对于离线用户没有意义,离线用户不需要接收它们,例如给用户发送好友上线通知等。 
**

3.2 拉模式

** 
在该模式中,服务端会给每个有消息要接收的用户分配一个队里,队里顺序存放所有要发送给该用户的消息。消息发送方在发送消息时,先将待发送消息发送到推送系统服务端,推送服务端将该消息缓存到接收方的消息队列中,然后通过长连接服务器给接收方发送一条有新消息到来的通知,通知的长度一般都比较短,客户端在收到通知之后,将自己队里中的全部消息拉下来,并进行处理、交付给使用方,然后再给服务端发送消息,删除自己已接收的那些消息。 
在拉模式中有两种触发消息拉取的时机:

  • -客户端收到新消息到来的通知时;
  • 客户端第一次上线时要无条件拉取一次;

拉模式下,可通过正序和倒序两种方式拉取服务端缓存的消息,其中正序拉取方式简单、易于实现,不易出错,适用于服务端缓存的消息量较小的情况下,可在推送系统中优先采用,倒序方式实现较为复杂,它更适合缓存消息量较大的情况,例如IM,客户端不在线时有可能会收到大量离线消息,这时客户端再次上线拉取消息时应采用倒序方式,优先拉取最近的消息,如果用户不再继续向前翻阅,更久的消息可能就不用拉取了,对于推送系统的拉模式,可以根据自己的实际情况采取倒序或正序拉取。拉模式下要特别注意以下事项: 
- 采用倒序拉取消息时,要保证消息最终的顺序性; 
- 采用倒序拉取消息时,要按轮、成批次拉取消息,不要收到一个通知只拉取一条,以降低消息拉取频度,例如可以从最近一条消息开始拉取指定数量的消息;每轮拉取消息时都要把消息全部拉取下来。 
- 成批次删除消息,即删除从最近ID以前的所有消息,以降低删除请求的发送频度,正序反序都要注意此项; 
- 新消息到来的通知中最好带上新消息的ID; 
- 推送客户端内要限制消息拉取频次,例如每秒最多拉取3次,防止消息量大时造成的频繁、无效的拉取操作。 
消息倒序拉取方式更易出错,现将其流程梳理如下: 
客户端拉取消息时将按轮进行拉取,每轮拉取可能分若干次拉取才能算完成,客户端将在以下两种情况下发起一轮拉取: 
(1) 接到新消息到来的通知; 
(2) 上线之后,包括新登录和断线重连; 
每轮的每次拉取消息时将按照如下方式进行: 
(1) 第一次拉取时从消息队列的最近一条消息开始; 
(2) 第二次以及以后发送拉取消息的请求需带上次拉取消息中最后一条消息的ID; 
如下图中,服务端缓存了16条消息,客户端如果在线,则它会收到对应的16个通知消息,现在客户端过滤了前15个通知,只有最后1个通知激发了拉取操作,如果客户端是离线后的第一次上线,那么还会无条件激发一次拉取消息操作,按照上述的流程,假设客户端根据当前网络条件判断最大拉取消息量为8条,客户端的拉取过程为: 
消息缓存示意图

(1) 第一次拉取时,其传入的参数为:起始ID为空,最大拉取条数为8,倒序拉取时从最近的一条消息开始拉取,返回的结果中将包含:9~16共8条消息;如下图所示: 
消息第一次拉取示意图

(2) 假设此时又有一条新消息(ID为17)进来,则此时服务器端和客户端的缓存消息状态如下图所示(蓝色斜线表示这部分消息已经被客户端拉取走): 
拉取消息期间又有新的消息进来 
(3) 第二次拉取时,其传入的参数为:起始ID为9,最大拉取条数为8,返回的结果中将包含1~8共8条消息,参数中传递起始ID将会被从返回结果中去掉;如下图所示: 
消息第二次拉取示意图

(4) 一轮拉取完成,则整理本次拉取的所有消息到本地缓存,如下图所示: 
完成一轮消息拉取 
(5) 每轮拉取之后,需回复确认消息,只需确认本轮拉取的最新一条ID为16的消息即可,确认之后,服务器端可能会根据情况将已经确认的消息之前的全部消息从缓存中删除,此时服务端和客户端的消息状态为: 
确认删除消息 
(6) 客户端拉取期间新到来的消息(例如上图中ID为17的消息)将会由下一轮拉取来送达客户端。 
拉模式的特点: 
拉模式不仅可以推送消息给在线用户还可以推送给离线用户,它相对于直推模式更复杂、对于在线用户的消息送达效率略慢。 
**

3.3 混合模式

** 
混合模式结合了直推模式和拉模式的的优点:对于在线用户将消息直接封装到通知里发送给推送客户端,对于离线的消息依然帮用户缓存直至其上线拉取走。 
在该模式中:发送到推送系统的消息,依然先被推送系统的服务端进行缓存,然后将消息封装到通知中交给长连接服务,对于在线用户,长连接服务将把这个包含了消息的通知直接通过tcp发送给客户端,对于离线用户,长连接服务将丢弃这个通知。当然,这只是一种实现方式,还有的推送系统中,要先判断客户端是否在线,如果在线,再封装通知发送给客户端,如果不在线则不会做这些动作,这种方式有风险,因为,在线状态的判断不一定有效。 
混合模式的特点:

  • 在线用户发送消息快,这一点与直推模式一致;
  • 在流量和省电方面,介于直推模式和混合模式之间;
  • 实现最为复杂,尤其客户端的去重操作会特别麻烦;
  • 在客户端收到消息之后(无论是在线时直接接收还是离线时被动拉取),都要主动删除服务端缓存的消息,这点与拉模式保持一致; 
    混合模式与xmpp等推送方式的区别在于: 
    混合模式依然全量缓存消息,客户端无论离线还是在线,收到消息之后都要发送删除请求的操作。 
    Xmpp在线时直接推送消息下去,服务端不缓存,离线时服务端缓存,客户端上线时拉取。 
    问题的关键在于:服务端无法有效判断客户端的在线状态,一旦服务端判断失误,将会造成消息丢失的问题,因此混合模式和拉模式相比于XMPP的这种消息推送方式,消息送达率更高,也可更高概率保证消息到达的有序性。 
    **

3.4 消息送达的可靠性保障

** 
个人认为,消息送达的可靠性保障是指推送系统将消息及时、可靠送达至接收客户端的能力,它包括推送系统保证消息发送过程中不会出现的重复、丢失、乱序的问题和及时送达消息等几个方面所能达到的服务质量。 
推送能保证消息到达的绝对可靠性吗? 
推送系统是尽最大可能保障消息送达至接收方,但是仅靠推送系统本身很难保证消息送达的绝对可靠性性,有一些极端异常场景很难处理,如果要专门针对这些异常进行处理,将会对整个系统的复杂性、以及系统的性能造成影响,对这部分的处理投入和最终取得的效果严重不成比例,因此绝大多数推送系统都采取不处理的方式,也都无法提送消息送达的绝对可靠性。 
推送系统在以下两个地方难以保证消息送达的不重不漏:

  • 发送方将消息交付给推送系统时,最常见的就是超时影响;例如:发送方将请求发送到访问端时,该请求因超时而返回,此时发送方不知道它的请求是否已经被服务端接收并处理了。这时: 
    如果发送方重发该请求,则面临两种情况:(1)上一个请求已经被服务端接收并处理了,那么它的第二个发送请求将被服务端作为一条新的消息请求,在这种情况下,接收方将会收到两条消息,对于推送系统的使用方来说,就出现了消息重复的问题;(2)上一个请求未被服务端接收或者未被成功处理,则系统运转正常,接收客户端会正常地接收到一条消息。 
    如果发送方在这种超时情况下不重复发送消息,它也面临两种情况:(1)上一个请求已经被服务端接收并处理,此时,系统运转正常,接收方正常地只接收一条消息;(2)上一个请求未被接收或成功处理,那么对于推送系统的使用方来说,就出现了消息丢失的问题。 
    因此,无论消息发送方采用哪种处理方式,都难以保证消息的重复或遗漏。
  • 推送系统将消息交付给接收方时,这种情况主要出现在有主动拉取和删除的拉模式和混合模式中,它的最主要问题是消息的交付与删除时,推送的客户端SDK要与不同的系统或应用交互,难以保证二者间的原子性。在有主动拉取和删除的模式中,推送客户端需要先到推送系统后台拉取到消息内容,然后完成“将消息交付给接收方”和“向推送后台发送删除消息的请求”两个动作,这两个动作间的先后顺序将对消息发送的结果直接造成不同的影响: 
    如果推送客户端SDK先向推送后台发送删除请求,后交付消息给接送方客户端应用,在两个动作之间推送客户端SDK发生异常时,就会出现消息已经被删掉了,但是推送客户端还没有将消息交付给接收方应用,从使用推送的应用方来看就造成了消息丢失的问题。 
    如果推送客户端SDK先将消息交付给接收方客户端应用,然后再向推送系统后台发送消息删除请求,在两个动作之间推送客户端SDK出现异常(例如推送客户端SDK异常挂掉)时,推送客户端SDK在恢复之后将会再到推送后台拉取这些本易交付的消息,并再次将它们交付接收方应用,这时,对于使用推送的应用而言,就出现了接收重复消息的问题。 
    **

3.5 TCP连接的可靠性

** 
使用TCP通信的数据需要经历三个阶段才能从发送方的应用程序到达接收方的应用程序中: 
(1) 发送方应用程序将消息交付到操作系统所分配的socket发送缓存中; 
(2) 发送方操作系统将其socket缓存中的消息发送到接收方操作系统的socket缓存中; 
(3) 接收方操作系统将socket缓存中的内容交付给接收方的应用程序。 
应用程序通过TCP发送消息 
如上图所示,发送方在进行第1步系统调用时,一旦消息被操作系统写入到本机的socket缓存中,就会立即返回,而不会等到2、3步执行完毕,此时,消息还未被发送出去,至于消息什么时候能成功发送出去,则由操作系统来决定,应用程序无法感知和控制,如下图所示: 
通过TCP发送消息的异步情况 
如果在上述消息发送时在2、3步出现问题,例如socket突然坏掉或者收发有一方的主机突然宕机,此时,对于消息发送方来说,它认为消息已经成功发送了,对于接受方来说,它却没有收到消息,那么对于整个应用系统而言,就出现了消息丢失的问题,就会出现消息传输的不可靠的问题。 
我们通常所说TCP是可靠性传输,主要是指收发双方在操作系统层面的可靠,即在通讯双方都正常工作的情况下,能保证发送方的socket缓存中的内容按序、可靠的传输到接收方的socket缓存中,另外,操作系统和应用程序之间的数据传递也是可靠的系统调用:系统调用成功返回,就意味着待发送的数据已经成功交付给了操作系统,主要问题在于上述两种操作之间的异步性上。 
如何保证消息传输的可靠性? 
首先,我们得看到保证消息的绝对可靠是非常困难的,通常付出的代价会非常大而获得的成效却并不成比例。大多数情况下,我们会选择一些妥协措施,用尽量小的投入产生比较大的可靠性效益。 
这种思想体现在推送系统的设计中,就是对外提供最大努力的推送功能,而非绝对可靠的推送。如果应用层对可靠性要求高于推送的能力,那么它应该自己来保证其可靠性,例如在应用层增加确认机制等措施,而不是依赖于推送系统。 
**

3.6消息送达的顺序性

** 
(1)直推模式 
在该模式下,消息被封装在通知里,直接由长连接服务(例如mosquitto)通过TCP连接直接发送到“推送客户端SDK”中,“推送客户端SDK”从通知中解析出消息内容,然后交给使用推送系统的应用接收客户端。因此,无论是开源还是自研,所采用的长连接服务必须能够将交付给它的消息按序发送到客户端。以mosquito为例,其内部是单线程工作模式,保证了所有的并发请求到它那里都变成串行执行,另外,它内部为每个连接都分配一个结构体,用于保存发往该连接的全部相关数据,例如连接的ID、socket描述符等等,在该结构体成员中有一个通知(消息)队列链表,所有要发送给该连接对应客户端的通知(消息)都被按照先后顺序放入到该通知(消息)队列链表中,在发送通知(消息)时也是按照先后顺序将通知(消息)队列中的通知写入到socket缓存中,这样就能保证所有交付到长连接服务的通知都能够按序到达接收端,如下图所示: 
长连接服务顺序推送通知 
在上图中,有4个通知发送方R1、R2、R3、R4,它们并发工作,各自先后发送了两条通知到长连接服务,长连接服务内部顺序处理这些接收到的通知,并将通知放到对应接收方的通知接收队列中,然后依次将这些通知从TCP连接中发送给推送客户端。 
Mosquitto这种实现方式中,有三处实现细节保证了通知传输的顺序性: 
(1) 单线程工作模式,无论发送到它那里的请求并发量有多大,它对请求的处理方式都是一个个的顺序进行; 
(2) 内部采用队列方式缓存待发送的通知,保证通知按照队列方式依次被插入和取走; 
(3) 消息发送时,按照在队列中的缓存顺序,依次写入到socket缓存中,socket缓存保证了网络传输过程中的顺序性。 
mosquitto就像对通知进行了排序,无论外部以怎样的并发量到达它那里,它都会把所有的请求进行排序,然后将这些通知顺序下发到客户端。 
(1)拉模式 
拉模式是指消息是通过客户端主动拉取的方式送达客户端,在该模式下,所有待推送的消息都会被推送的服务端缓存,在推送后台中,有专门的缓存集群(本文的缓存集群中有相关描述)用于存储每个接收端的消息。在缓存集群中,为每个用户分配一个消息队列,将要发送给某个客户端的消息都会先存入到其消息队列中,这里相当于对并发的发送消息的请求进行串行化处理,为消息进行排序;客户端在接到新消息到来的通知时或者客户端初次上线时按序从该消息队列中取出消息(详细拉取过程可见本文对拉模式的详细介绍),如下图所示: 
拉模式下的消息顺序性 
在拉模式下,是通过实际缓存节点的串行化存储和顺序拉取消息等措施保证消息送达的顺序性。 
无论到达推送后台的“发送消息”请求的并发量多复杂(包括量大而且无序),缓存集群首先会将消息路由到正确的缓存节点(这一步并未进行任何串行化操作),实际的缓存节点将转交过来的并发请求进行串行化。缓存节点以Redis为例(详见本文关于如何构建缓存集群的介绍),它内部也是单线程方式处理请求,因此到达实际缓存节点的并发请求,都将被其顺序处理,也即将所有的并发而来的消息进行排序。缓存节点的串行化操作保障消息在服务缓存的顺序性,从而为整个系统的消息有序性提供了前提条件。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值