消息推送系统的设计
2016年12月09日


一、消息推送系统设计需求

1、高性价比,在有限的硬件资源下,尽可能的提高消息系统的性能和可用性。

2、提高数据的一致性。


二、分析

消息推送,按数据量划分,包括两类:

1)持续的大量数据(比如:持续的物联网GPS上报等)推送,单类数据量大于 10 kb 每秒 。

2)低频率、数据量小的偶发事件、通知类的数据推送。


消息重要性和实时性分级:( “四象限” 划分)

                            不重要                            |                          不重要

                            可延时                            |                          低延时

                            ——————————————————————

                            很重要                            |                          很重要

                            可延时                            |                          低延时

        备注:

                  很重要  =  非常重要,数据不丢、不乱。

                  不重要  =  可接受偶尔出现问题。

                  低延时  =  延时低(平均在3秒以内)。

                  可延时  =  有一定延时(3秒以上)。

 

大部分消息处于 (2) (3) (4) 象限。针对消息的特性,应采用不同性能和稳定级别的推送方案。




根据 CAP 定理:

    Consistency(一致性), 数据一致更新,所有数据变动都是同步的。

    Availability(可用性), 好的响应性能。

    Partition tolerance(分区容错性) 可靠性。

    定理:任何分布式系统只可同时满足二点,没法三者兼顾。

没有一个分布式系统是C、A、P同时都达到完美的,要么损失性能来保障一致性和可用性;要么损失一致性来提高性能。

理想模型如下:

A、牺牲 性能 来提高 可用性和一致性。

B、牺牲 一致性 来提高 性能和可用性。

C、牺牲 可用性 来提高 性能和一致性。

对于上面的 A 模型,用得非常广泛,比如消息的ACK机制,用法比较简单,不多说。

对于上面的 B 模型,经常使用 BASE (牺牲高一致性,保证最终一致性) 方案。

    Basically Available 基本可用。

    Soft state 软状态 状态可以有一段时间不同步,异步。

    Eventually consistent 最终一致,最终数据是一致的就可以了,而不是实时高一致。

对于上面的 C 模型,用得比较少,也不好理解。牺牲可用性,来保证一致性和性能,我的理解是,除非服务不可用,否则服务一定是高性能且高一致性的。

也就是说,当服务不能保证一致性和高性能的时候,就降低可用性,宁愿服务不可用,也不能让服务出现不一致或者性能低的情况。

C模型的关键点在于,C模型的系统,系统自身不保证高可用,出现异常(性能降低或者一致性出现问题)时,它有意降低可用性,来保障服务期间的高性能和数据的强一致性。

所以如果使用C模型,则需要考虑,如何通过外部手段来保证系统的可用性(加强监控,出现不可用时,人工介入恢复,或者启动临时方案)。

延伸知识:分布式系统的容错模式

电路熔断器模式(Circuit Breaker Patten), 该模式的原理类似于家里的电路熔断器,如果家里的电路发生短路,熔断器能够主动熔断电路,以避免灾难性损失。在分布式系统中应用电路熔断器模式后,当目标服务慢或者大量超时,调用方能够主动熔断,以防止服务被进一步拖垮或者导致其他系统故障(雪崩效应);如果情况又好转了,电路又能自动恢复,这就是所谓的弹性容错,系统有自恢复能力。

参考文章:http://www.infoq.com/cn/articles/basis-frameworkto-implement-micro-service/

 

上面的A、B、C模型对于推送系统来说,

对于上面处于(2) 象限的消息推送,可以考虑 能否采用上面 C模型,如果不能接受C模型的服务(暂时)不可用,则可能会降级为 A、B 模型,A模型牺牲性能,整个系统是否存在性能问题?B模型牺牲一致性,是否能采用软状态、最终一致性方案来弥补?

对于上面处于(3) 象限的消息推送,宜采用 A 模型。

对于上面处于(4) 象限的消息推送,宜采用 B 模型,A模型也可以,具体视情况而定。



A模型数据推送系统设计方案

 

在A模型中,性能不是关键。它的关键词:性能不是瓶颈  数据要准确可靠

 

考虑点:

  1、如何保证数据100%不丢失?

  2、如何保证数据100%不重复?

 

不丢失:数据来后,先以最稳当的方式持久化下来,再推送出去,推送成功后,才删掉持久化的数据,否则数据一直持久化直到超过保存容量或者时间上限。(如果使用数据库,可以考虑事务)

不重复:能够识别每条数据的唯一性(最简单的办法就是每条数据有一个唯一标识),在内存数据库中保存一段时间已推送的数据标识(ID)的列表,当新的数据来了之后,和数据标识列表中的ID进行比较,如果存在则说明之前已经推送过这条数据,直接pass,否则推送数据并加入数据标识列表。

 

技术选型和系统处理数据流程:(纯Java + Redis缓存ID + 本地磁盘缓存数据)

1)收到数据后,先将数据ID取出,去Redis里已推送的数据ID的列表中查找,如果找到这个ID,则说明数据已处理过,重复了,则丢弃该数据。否则继续下面的步骤。

2)将数据和ID用Producer线程尽快写入本地磁盘,以防丢失。

3)然后用Consumer线程将数据从磁盘读取出来,然后尝试推送出去。

4)如果推送成功,则标记该ID的数据为success并保存到磁盘,然后继续处理后面的数据。否则暂停或关闭接收 后续的数据(如果可以),如果不能暂停,则直接进入下面的第(5)步骤),

5)无限尝试推送当前数据,直到故障解除,当前数据推送成功,

7)故障解除时,如果之前暂停或关闭了数据接收,则将其恢复。

8)继续从磁盘取后面未处理的数据。

9)定期触发一个异步线程去清理磁盘上推送成功的数据,保持磁盘空间良好。

其他说明:

1)尽量将多种消息合并在一个线程处理,如果技术或者业务上不能在一个线程去处理,则另起线程(或进程),与其他线程(或进程)隔离、数据完全独立。

2)尽量控制在单机处理,如果一台机器不够,则多加几台机器,但每台机器处理的数据完全独立。

3)数据缓存在本地磁盘上,只是一种简单高效的策略,但使用不够方便,也不便于管理,如果有高可用、高性能的redis,则建议建将数据缓存在redis上。次之,可以换成mysql,如果网络环境稳定,可以连远程mysql,否则可以将mysql和应用安装在同一台服务器上。另外,也可以用sqlite文件数据库。

 

 

B模型数据推送系统设计方案

 

B模型的系统,不保证数据一致性。它的关键词:性能和可用性优先。

 

设想这种场景:

一天有1亿的数据要推送出去,但是在处理某条数据的时候报错了,且重试了3次仍然不成功,后面还有大量数据等着推送,线程不能阻塞,那么只能将推送失败的数据记录下来,继续处理后面来的数据。

 

可以在 数据推送失败或者接收方响应非常缓慢时,将数据记录下来(比如记日志,供人工排查补数据;或者转存kafka,由补偿程序处理),然后继续接收后面的数据继续推送。

 

首先,我们将容易产生故障的业务和数据分类,将容易发生故障的数据与其他数据隔离,以保证发生故障时,不影响其他数据的处理。

 

根据业务场景,按照数据分类,用单独的线程处理,做故障隔离(某个线程发生故障时,不影响其他线程)。

 

其次,在故障发生持续时间长,或者故障发生频繁的场景下,可能不适合用B模型,因为B模型不保证数据一致性,故障发生得越多,数据问题就会越严重,处理起来越麻烦。

 

在推送数据项目里,如果发生故障可能持续时间很长,数据堆积很多,有这种情况下,就不建议使用B方案。

在故障发生率低、故障持续时间短的前提下,可以如下操作:

  如果推送失败,不要阻塞后面的数据,采用将数据记录下来(放内存里面),等后面一批数据来后,同时处理,再次推送,如果这次推送仍然失败,则再重复这个过程,一共重试3~5次,如果都失败则进入C方案的流程(下面会讲)。

上面这个方案,可以在故障持续时间短的情况下,解决推送问题。

 

 

C模型数据推送系统设计方案

 

C模型的系统,不保证系统高可用,换句话说,它宁愿系统不可用,也要保证在系统可用的时间段内系统的高性能和数据的强一致性。它的关键词:牺牲可用性 换一致性和性能。

 

可以在 数据推送失败或者接收方响应非常缓慢时,且在C方案的补偿机制下,系统仍无好转,则关闭该数据的推送,同时触发监控程序,通知人工处理,或者由 故障处理程序 去处理。

 

根据具体的业务场景,如果当某类数据多次无法推送时,主动关闭该数据的推送,同时触发监控程序,通知维护人员,由维护人员决定,是否去处理故障,如果不需要处理,则该数据就一直关闭,如果需要处理,则当故障恢复时,重新启动该数据的推送任务。也可以由monitor主动去监控数据推送是否恢复正常,恢复正常后,自动恢复推送。