构架分布式队列编程
分布式队列基础的需求
- 需求来源
- 抽象模型
- 编程模型
详情见:
http://tech.meituan.com/distributed_queue_based_programming.html
需求来源
通信是人们最基本的需求,同样也是计算机最基本的需求。对于工程师而言,在编程和技术选型的时候,更容易进入大脑的概念是RPC、Ajax、Kafka。在这些具体的概念后面,最本质的东西是“通讯”。
当确定系统之间有通讯需求的时候,工程师们需要做很多的决策和平衡,这直接影响工程师们是否会选择分布式队列编程模型作为架构。
从这个角度出发,影响建模的因素有四个:When、Who、Where、How。
When:同步VS异步
通信的一个基本问题是:发出去的消息什么时候需要被接收到?这个问题引出了两个基础概念:“同步通讯”和“异步通讯”。
这里给一些启发式的建议:
-
发出去的消息是否需要确认,如果不需要确认,更像是异步通信,这种通信有时候也称为单向通信(One-Way Communication)。
-
如果需要确认,可以根据需要确认的时间长短进行判断。时间长的更像是异步通信,时间短的更像是同步通信。当然时间长短的概念是纯粹的主观概念,不是客观标准。
-
发出去的消息是否阻塞下一个指令的执行,如果阻塞,更像是同步,否则,更像是异步。
当分析一个通信需求或者进行通信构架的时候,工程师们被迫作出“同步”还是“异步”的决定。当决策的结论是“异步通信”的时候,分布式队列编程模型就是一个备选项。
Who:发送者接收者解耦
在进行通信需求分析的时候,需要回答的另外一个基本问题是:消息的发送方是否关心谁来接收消息,或者反过来,消息接收方是否关心谁来发送消息。
如果工程师的结论是:消息的发送方和接收方不关心对方是谁、以及在哪里,分布式队列编程模型就是一个备选项。
因为在这种场景下,分布式队列架构所带来的解耦能给系统架构带来这些好处:
-
无论是发送方还是接收方,只需要跟消息中间件通信,接口统一。统一意味着降低开发成本。
-
在不影响性能的前提下,同一套消息中间件部署,可以被不同业务共享。共享意味着降低运维成本。
-
发送方或者接收方单方面的部署拓扑的变化不影响对应的另一方。解藕意味着灵活和可扩展。
Where:消息暂存机制
在进行通信发送方设计的时候,令工程师们苦恼的问题是:如果消息无法被迅速处理掉而产生堆积怎么办、能否被直接抛弃?
如果根据需求分析,确认存在消息积存,并且消息不应该被抛弃,就应该考虑分布式队列编程模型构架,因为队列可以暂存消息。
How:如何传递
对通信需求进行架构,一系列的基础挑战会迎面而来,这包括:
-
可用性,如何保障通信的高可用。
-
可靠性,如何保证消息被可靠地传递。
-
持久化,如何保证消息不会丢失。
-
吞吐量和响应时间。
-
跨平台兼容性。
-
除非工程师对造轮子有足够的兴趣,并且有充足的时间,采用一个满足各项指标的分布式队列编程模型就是一个简单的选择。
分布式队列抽象模型
分布式队列编程模型包含三类角色:发送者(Sender)、分布式队列(Queue)、接收者(Receiver)。发送者和接收者分别指的是生产消息和接收消息的应用程序或服务。
需要重点明确的概念是分布式队列,它是提供以下功能的应用程序或服务:
-
接收“发送者”产生的消息实体;
-
传输、暂存该实体;
-
为“接收者”提供读取该消息实体的功能。
特定的场景下,它当然可以是Kafka、RabbitMQ等消息中间件。但它的展现形式并不限于此,例如:
-
队列可以是一张数据库的表,发送者将消息写入表,接收者从数据表里读消息。
-
如果一个程序把数据写入Redis等内存Cache里面,另一个程序从Cache里面读取,缓存在这里就是一种分布式队列。
-
流式编程里面的的数据流传输也是一种队列。
-
典型的MVC(Model–view–controller)设计模式里面,如果Model的变化需要导致View的变化,也可以通过队列进行传输。这里的分布式队列可以是数据库,也可以是某台服务器上的一块内存。
最基础的分布式队列编程抽象模型是点对点模型,其他抽象构架模型居于改基本模型上各角色的数量和交互变化所导致的不同拓扑图。
具体而言,不同数量的发送者、分布式队列以及接收者组合形成了不同的分布式队列编程模型。
点对点模型(Point-to-point)
基础模型中,只有一个发送者、一个接收者和一个分布式队列。如下图所示:
生产者消费者模型(Producer–consumer)
如果发送者和接收者都可以有多个部署实例,甚至不同的类型;但是共用同一个队列,这就变成了标准的生产者消费者模型。在该模型,三个角色一般称为生产者(Producer)、分布式队列(Queue)、消费者(Consumer)。
发布订阅模型(PubSub)
如果只有一类发送者,发送者将产生的消息实体按照不同的主题(Topic)分发到不同的逻辑队列。每种主题队列对应于一类接收者。
这就变成了典型的发布订阅模型。在该模型,三个角色一般称为发布者(Publisher),分布式队列(Queue),订阅者(Subscriber)。
MVC模型
如果发送者和接收者存在于同一个实体中,但是共享一个分布式队列。这就很像经典的MVC模型。
分布式队列编程模型
分布式队列模型编程和异步编程
分布式队列编程模型的通讯机制一般是采用异步机制,但是它并不等同于异步编程。
首先,并非所有的异步编程都需要引入队列的概念,例如:大部分的操作系统异步I/O操作都是通过硬件中断( Hardware Interrupts)来实现的。
其次,异步编程并不一定需要跨进程,所以其应用场景并不一定是分布式环境。
最后,分布式队列编程模型强调发送者、接收者和分布式队列这三个角色共同组成的架构。这三种角色与异步编程没有太多关联。
分布式队列模式编程和流式编程
随着Spark Streaming,Apache Storm等流式框架的广泛应用,流式编程成了当前非常流行的编程模式。但是分布式队列编程模型和流式编程并非同一概念。
首先,队列编程模式不依赖于任何框架,而流式编程是在具体的流式框架内的编程。
其次,分布式队列编程模型是一个需求解决方案,关注如何根据实际需求进行分布式队列编程建模。流式框架里的数据流一般都通过队列传递,不过,流式编程的关注点比较聚焦,它关注如何从流式框架里获取消息流,进行map、reduce、 join等转型(Transformation)操作、生成新的数据流,最终进行汇总、统计。
队列式编程在分布式环境下的一些具体应用
信息采集处理
信息采集处理应用广泛,例如:广告计费、用户行为收集等。作者碰到的具体项目是为广告系统设计一套高可用的采集计费系统。
典型的广告CPC、CPM计费原理是:收集用户在客户端或者网页上的点击和浏览行为,按照点击和浏览进行计费。计费业务有如下典型特征:
-
采集者和处理者解耦,采集发生在客户端,而计费发生在服务端。
-
计费与钱息息相关。
-
重复计费意味着灾难。
-
计费是动态实时行为,需要接受预算约束,如果消耗超过预算,则广告投放需要停止。
-
用户的浏览和点击量非常大。
挑战
计费业务的典型特征给我们带来了如下挑战:
-
高吞吐量--广告的浏览和点击量非常巨大,我们需要设计一个高吞吐量的采集架构。
-
高可用性--计费信息的丢失意味着直接的金钱损失。任何处理服务器的崩溃不应该导致系统不可用。
-
高一致性要求--计费是一个实时动态处理过程,但要受到预算的约束。收集到的浏览和点击行为如果不能快速处理,可能会导致预算花超,或者点击率预估不准确。所以采集到的信息应该在最短的时间内传输到计费中心进行计费。
-
完整性约束--这包括反作弊规则,单个用户行为不能重复计费等。这要求计费是一个集中行为而非分布式行为。
-
持久化要求--计费信息需要持久化,避免因为机器崩溃而导致收集到的数据产生丢失。
构思
采集的高可用性意味着我们需要多台服务器同时采集,为了避免单IDC故障,采集服务器需要部署在多IDC里面。
实现一个高可用、高吞吐量、高一致性的信息传递系统显然是一个挑战,为了控制项目开发成本,采用开源的消息中间件进行消息传输就成了必然选择。
完整性约束要求集中进行计费,所以计费系统发生在核心IDC。
计费服务并不关心采集点在哪里,采集服务也并不关心谁进行计费。
根据以上构思,我们认为采集计费符合典型的“生产者消费者模型”。
架构
采集计费系统架构图如下:
-
用户点击浏览收集服务(Click/View Collector)作为生产者部署在多个机房里,以提高收集服务可用性。
-
每个机房里采集到的数据通过消息队列中间件发送到核心机房IDC_Master。
-
Billing服务作为消费者部署在核心机房集中计费。
采用此架构,我们可以在如下方面做进一步优化:
-
提高可扩展性,如果一个Billing部署实例在性能上无法满足要求,可以对采集的数据进行主题分区(Topic Partition)计费,即采用发布订阅模式以提高可扩展性(Scalability)。
-
全局排重和反作弊。采用集中计费架构解决了点击浏览排重的问题,另一方面,这也给反作弊提供了全局信息。
-
提高计费系统的可用性。采用下文单例服务优化策略,在保障计费系统集中性的同时,提高计费系统可用性。