《数据密集型应用系统设计》读书笔记——第三部分 派生数据(二)

第11章 流处理系统

批处理系统有一个很大的假设:即输入是有界的,即已知和有限的⼤小,所以批处理知道它何时完成输⼊的读取。
实际上,很多数据是⽆界限的,因为它随着时间的推移而逐渐到达:你的用户在昨天和今天产⽣了数据,明天他们将继续产⽣更多的数据。除非你停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的⽅式“完成”。因此,批处理程序必须将数据⼈为地分成固定时间段的数据块, 例如,在每天结束时处理理一天的数据,或者在每小时结束时处理一小时的数据。
日常批处理中的问题是,输⼊的变更只会在⼀天之后的输出中反映出来,这对于许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理——比如说,在每秒钟的末尾——或者甚⾄更连续一些,完全抛开固定的时间切片,当事件发⽣时就立即进⾏处理,这就是流处理背后的想法。

发送事件流

当输⼊是⼀个⽂件(一个字节序列),第⼀个处理步骤通常是将其解析为⼀系列记录。在流处理的上下文中,记录通常被叫做事件(event),但它本质上是一样的:⼀个小的,独立的,不可变的对象, 包含某个时间点发生的某件事情的细节。一个事件通常包含⼀个来⾃墙上时钟的时间戳,以指明事件发⽣的时间。
事件可能被编码为文本字符串或JSON,或者某种二进制编码。这种编码允许你存储一个事件,例如将其附加到⼀个文件,将其插⼊关系表,或将其写⼊文档数据库。它还允许你通过网络将事件发送到另⼀个节点以进行处理。
在批处理中,⽂件被写⼊一次,然后可能被多个作业读取。类似地,在流处理术语中,一个事件由⽣产者生成⼀次,然后可能由多个消费者或接收者进⾏处理。在⽂件系统中,文件名标识⼀相关记录;在流媒体系统中,相关的事件通常被聚合为⼀个主题或流。
原则上将,⽂件或数据库就足以连接生产者和消费者:生产者将其生成的每个事件写入数据存储,且每个消费者定期轮询数据存储,检查自上次运行以来新出现的事件。这实际上正是批处理在每天结束时处理当天数据时所做的事情。
但当我们想要进⾏低延迟的连续处理时,如果数据存储不是为这种⽤途专门设计的,那么轮询开销就会很⼤。轮询的越频繁,能返回新事件的请求⽐例就越低,⽽额外开销也就越高。相比之下,最好能在新事件出现时直接通知消费者。

消息系统

向消费者通知新事件的常⽤方式是使用消息传递系统:生产者发送包含事件的消息,然后将消息推送给消费者。
像生产者和消费者之间的Unix管道或TCP连接这样的直接信道,是实现消息传递系统的简单⽅法。但是,大多数消息传递系统都在这一基本模型上进⾏扩展。特别的是,Unix管道和TCP仅连接一个发送者与一个接收者,⽽一个消息传递系统允许多个生产者节点将消息发送到同一个主题,并允许多个消费者节点接收主题中的消息。
在这个发布/订阅模式中,不同的系统采取各种各样的方法,并没有针对所有⽬的的通用答案。为了区分这些系统,问一下这两个问题会特别有帮助:

  1. 如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用背压(也称为流量量控;即阻塞生产者,以免其发送更多的消息)。例如Unix管道和TCP使用背压:它们有⼀个固定⼤小的缓冲区,如果填满,发送者会被阻塞,直到接收者从缓冲区中取出数据。
    如果消息被缓存在队列中,那么理解队列增⻓会发生什么是很重要的。当队列装不不进内存时系统会崩溃吗?还是将消息写⼊磁盘?如果是这样,磁盘访问又会如何影响消息传递系统的性能?
  2. 如果节点崩溃或暂时脱机,会发⽣什么情况?——是否会有消息丢失?与数据库一样,持久性可能需要写入磁盘和/或复制的某种组合,这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。
生产者与消费者之间的直接消息传递

许多消息传递系统使⽤生产者和消费者之间的直接⽹络通信,⽽不通过中间节点。
尽管这些直接消息传递系统在设计它们的环境中运⾏良好,但是它们通常要求应⽤用代码意识到消息丢失的可能性。它们的容错程度极为有限:即使协议检测到并重传在网络中丢失的数据包,它们通常也只是假设生产者和消费者始终在线。
如果消费者处于脱机状态,则可能会丢失其不可达时发送的消息。一些协议允许⽣产者重试失败的消息传递,但当⽣产者崩溃时,它可能会丢失消息缓冲区及其本应发送的消息,这种⽅法可能就没⽤了。

消息代理

⼀种⼴泛使⽤的替代⽅法是通过消息代理(message broker)(也称为消息队列列)发送消息,消息代理实质上是一种针对处理理消息流⽽优化的数据库。它作为服务器运行,⽣产者和消费者作为客户端连接到服务器。生产者将消息写入代理,消费者通过从代理那⾥读取来接收消息。
通过将数据集中在代理上,这些系统可以更容易地适应不断变化的客户端(连接,断开连接和崩溃),而持久性问题则转移到代理的身上。⼀一消息代理只将消息保存在内存中,而另一些消息代理(取决于配置)将其写⼊磁盘,以便在代理崩溃的情况下不会丢失。针对缓慢的消费者,它们通常会允许⽆上限的排队(而不是丢弃消息或背压),尽管这种选择也可能取决于配置。
排队的结果是,消费者通常是异步的:当生产者发送消息时,通常只会等待代理确 认消息已经被缓存,⽽不等待消息被消费者处理。向消费者递送消息将发生在未来某个未定的时间点——通常在⼏分之⼀秒之内,但有时当消息堆积时会显著延迟。

消息代理与数据库对比

有些消息代理甚⾄可以使用XA或JTA参与两阶段提交协议务。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在实践上很重要的差异:

  • 数据库通常保留数据直⾄显式删除,⽽大多数消息代理在消息成功递送给消费者时会⾃动删除消息。这样的消息代理不适合长期的数据存储。
  • 由于它们很快就能删除消息,大多数消息代理都认为它们的工作集相当⼩——即队列很短。如果代理需要缓冲很多消息,⽐如因为消费者速度较慢(如果内存装不下消息,可能会溢出到磁盘), 每个消息需要更长的处理时间,整体吞吐量可能会恶化。
  • 数据库通常⽀持⼆级索引和各种搜索数据的⽅式,而消息代理通常支持按照某种模式匹配主题,订阅其子集。 这可以些机制虽然不同,但本质上都是让客户端可以选择它们想要了解的部分数据。
  • 查询数据库时,结果通常基于某个时间点的数据快照;如果另⼀个客户端随后向数据库写⼊一些改变了查询结果的内容,则第⼀个客户端不会发现其先前结果现已过期(除非它重复查询或轮询变更)。相⽐之下,消息代理不支持任意查询,但是当数据发生变化时(即新消息可用时),它们会通知客户端。
多个消息者

当多个消费者从同一主题中读取消息时,有使⽤两种主要的消息传递模式:

  • 负载均衡
    每条消息都被传递给消费者之一,所以处理该主题下消息的工作能被多个消费者共享。代理可以为消费者任意分配消息。当处理消息的代价高昂,希望能并行处理消息时,此模式⾮常有⽤。
  • 扇出
    每条消息都被传递给所有消费者。扇出允许几个独立的消费者各自“收听”相同的消息广播,⽽不会相互影响
    在这里插入图片描述
    两种模式可以组合使⽤:例如,两个独立的消费者组可以每组各订阅一个主题,每⼀组都共同收到所有消息,但在每一组内部,每条消息仅由单个节点处理。
确认和重新传递

消费随时可能会崩溃,所以有一种可能的情况是:代理向消费者递送消息,但消费者没有处理,或者在消费者崩溃之前只进⾏了部分处理。为了确保消息不会丢失,消息代理使用确认:客户端必须显式告知代理消息处理完毕的时间,以便代理能将消息从队列中移除。
如果与客户端的连接关闭,或者代理超出一段时间未收到确认,代理则认为消息没有被处理,因此它将消息再递送给另一个消费者。 (请注意可能发生这样的情况,消息实际上是处理完毕的,但确认在⽹络中丢失了。这需要⼀种原子提交协议才能来处理)。
当与负载均衡相结合时,这种重传⾏为对消息的顺序有种有趣的影响。在下图中,消费者通常按照⽣产者发送的顺序处理消息。然⽽消费者2在处理消息m3时崩溃,与此同时消费者1正在处理消息m4。未确认的消息m3随后被重新发送给消费者1,结果消费者1按照m4,m3,m5的顺序处理消息。因此m3 和m4的交付顺序与以⽣产者1的发送顺序不同。
在这里插入图片描述
即使消息代理试图保留消息的顺序,负载均衡与重传的组合也不可避免地导致消息被重新排序。为避免此问题,你可以让每个消费者使用单独的队列(即不使⽤负载均衡功能)。如果消息是完全独立的,则消息顺序重排并不是一个问题。如果消息之间存在因果依赖关系,这就是⼀个很重要的问题。

分区日志
基于日志的消息存储

⽇志只是磁盘上仅支持追加式修改记录的序列。我们可以这样使用日志来实现消息代理:生产者通过将消息追加到⽇志末尾来发送消息,而消费者通过依次读取⽇志来接收消息。如果消费者读到⽇志末尾,则会等待新消息追加的通知。 Unix工具 tail -f 能监视文件被追加写入的数据,基本上就是这样工作的。
为了扩展到⽐单个磁盘所能提供的更高吞吐量,可以对⽇志进⾏分区。不同的分 区可以托管在不同的机器上,且每个分区都拆分出一份能独立于其他分区进⾏读写的日志。⼀个主题可以定义为⼀组携带相同类型消息的分区。这种方法下图所示:
在这里插入图片描述
在每个分区内,代理为每个消息分配一个单调递增的序列号或偏移量。这种序列号是有意义的,因为分区是仅追加写入的,所以分区内的消息是完全有序的。不同的分区之间则没有顺序保证。
Apache Kafka等消息系统采用的是这种基于日志的工作方式,而AMQP/JMS风格的传统消息系统在消费者确认消息后,会从代理中删除该消息。

对比日志与传统消息系统

基于⽇志的方法天然⽀持扇出式消息传递,因为多个消费者可以独⽴读取日志,⽽不会相互影响——读取消息不会将其从日志中删除。为了在一组消费者之间实现负载平衡,代理可以将整个分区分配给消费者组中的节点,⽽不是将单条消息分配给消费者客户端。
每个客户端消费指派分区中的所有消息。然后使⽤

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值