《数据密集型应用系统设计》整理10

第十一章 流处理系统

为了解决批处理需要一定时间后才能处理的问题,流式处理在每秒钟结束时(甚至持续不断)处理每秒的数据,完全放弃固定的时间片,有事件就处理。

“流”是指随着时间的推移而持续可用的数据。

发送事件流

在流处理的上下文中,记录通常被称为事件,但本质上跟批处理是一回事:一个小的、独立的、不可变的对象。

数据产出后的流转阶段,批处理选择通过文件或数据库连接生产者和消费者:生产者将其生产的每个事件写入数据存储,并且每个消费者定期轮询数据存储以检查自上次运行以来出现的事件。

但是,轮询的代价很大,当事件发生时,最好通知消费者。

消息系统

生产者发送包含事件的消息,然后该消息被推送给一个或多个消费者。

UDP组播、无代理的消息库、RPC等将生产者直接连接到消费者,它们通常要求应用程序代码意识到消息丢失的可能性。

消息代理

消息代理作为服务器运行,生产者和消费者作为客户端连接到它。持久性问题被转移到消息代理那里去。

多个消费者

  • 负载均衡式:消费者共享主题中处理消息的工作。
  • 扇出式:每条消息都被传递给所有的消费者。
    -----2019-07-27---3.15.00

确认和重新传递

消费者处理完之后通知消息代理,消息代理才将消息从队列中移除。

如果消费者关闭或者超时,代理没有收到确认,则会重新发送给另一个消费者,这样会导致重新排序。为了避免此类问题,可以为每个消费者使用单独的队列。
-----2019-07-27---3.21.07

分区日志

消息代理收到确认后,就会把消息从队列中移除,没有持久化的过程。新加入的消费者只能从当前时刻开始消费。

为了解决这个问题,把持久存储与消息代理的低延迟相结合,形成了日志消息代理。

基于日志的消息存储

类似于tail -f,生产者将消息追加到日志末尾来发送消息,消费者通过依次读取日志来接收消息。
-----2019-07-27---3.26.08

  • 为了突破单个磁盘的带宽吞吐的上限,可以对日志进行分区,将主题定位为一组分区。
  • 每个分区内,代理给每个消息分配一个单调递增的序列号或者偏移量。

对比日志与传统消息系统

为了在一组消费者之间实现负载均衡,代理可以将整个分区分配给消费者组中的节点。当消费者被分配了一个日志分区时,它将以直接的单线程方式顺序读取分区中的消息,这样会有些缺陷:

  • 消费一个主题的及诶单数最多等于该主题中的日志分区数,因为统一分区内的消息将被传递到同一个节点;
  • 如果单个消息处理缓慢,则会阻碍该分区后续消息的处理。

所以如果消息处理的代价很高,而消息排序不那么重要,则可以使用传统消息系统。如果消息吞吐量搞,每个消息处理块,消息顺序又很重要,则使用基于日志的方法。

磁盘空间使用

为了回收磁盘空间,日志被分割为段,并且不时的将旧段删除或者归档保存。
这意味着,如果消费者消费的很慢,则有可能导致其偏移量指向了已经被删除的片段,导致信息丢失。

数据库与流

没有一个系统能够满足所有的数据存储、查询和处理需求。
在实践中,大多数重要的应用都需要结合多种不同的技术。比如OLTP为用户提供服务,缓存加速,全文检索处理搜索查询,数据仓库用于分析。

多个系统的数据需要进行同步,通常使用ETL,通过不活数据库的完整版本,对其进行转换并将其批量加载到数据仓库中。

但是定期的ETL比较缓慢,有时候需要使用双重写入。
但是双重写入会有严重的问题:
-----2019-07-27---4.03.19

变更数据捕获(Change Data Capture,CDC)

CDC记录了写入数据库的所有更改,并以可复制到其他系统的形式来提取数据。
-----2019-07-27---4.05.33

CDC使得一个数据库编程主节点,其他变成从节点。由于基于日志的消息代理保留了消息的排序,所以非常适合从元数据库传输更改事件。

数据库触发器可以通过注册触发器来实现CDC,但是性能开销大。
解析复制日志是一种更健壮的方法。

需要解决以下问题:

  • 初始快照:CDC需要有快照功能,数据库的快照必须与更改日志中的已知位置或偏移量相对应;
  • 日志压缩:丢弃重复项,保留key的最新值。
  • 对变更流的API支持:以流的形式持续的从数据库中导出数据。Kafka Connect致力于将广泛的数据库系统变更数据采集工具与Kafka集成。

事件溯源

与CDC类似,事件溯源将所有对应用程序状态的更改保存为更改事件的日志。最大的区别在于事件溯源在不同抽象层次上应用了这个想法:

  • CDC中,应用程序以数据可变方式来操纵数据库。
  • 事件溯源中,应用程序逻辑是基于写入事件日志的不可变事件构建的。

事件溯源是一种强大的建模技术:从应用程序的角度来看,将用户的行为记录为不可变得事件更有意义,而不是记录这些行为对可变数据库的影响。

从事件日志导出当前状态

使用事件溯源的应用程序需要记录时间的日志(表示写入系统的数据),并将其转换为适合像用户现实的状态(从系统读取数据的方式)。

命令和事件

不允许事件流的消费者拒绝事件:当消费者看到事件时,它已经是日志中不可变的部分。

因此,任何命令的验证都需要在它成为事件之前完成。
或者,分为两个事件,第一个是暂时,第二个是确认。

状态,流与不可变性

事务日志记录了对数据库所做的所有更改。高速追加是更改日志的唯一方法。从这个角度来看,数据库的内容保存了日志中最新记录值的缓存。日志是事实。

不变日志的优势:类似于会计的记账,如果发生错误,不是撤销已记录的数据,而是添加一笔记录来修正。
相同的事件日志派生多个视图:通过从不变事件日志中分离可变状态,可以从相同的事件日志派生出多个面向读取的表示方式。
并发控制:事件日志的消费者通常是异步的,如何保持一致?一种方法是同步执行读取视图的更新,并将事件追加到日志中。
不变性的限制:不变性可保持多久?主要看数据集的变化情况。一些主要是添加数据,很少删除和更新的,很容易支持不变性。其他的有较高更新和删除率的,不变的历史数据会变得过于庞大。有时候确实是需要删除数据,而不是追加一条日志表示已被删除。真正的删除数据会变得非常困难,因为数据副本可能在很多地方都有。

流处理

流处理的适用场景:

  1. 复杂事件处理:CEP秒速一个应该检测到的事件模式,这些查询交给一个处理引擎,该引擎使用输入流并在内部维护匹配所需的状态机。一旦触发,则会产生一个复杂的事件。
  2. 流分析:在固定的时间间隔内计算统计信息,比如速率、滚动平均值。
  3. 维护物化视图:使用数据库更改流来保持派生数据系统与源数据库之间的同步。
  4. 在流上搜索:预先指定一个搜索查询,然后不断地将流与这个查询进行匹配。

流式join

流和流join(窗口join)

两个输入流都由活动事件组成,采用join操作用来搜索在特定事件窗口内发生的相关事件。
有/没有匹配事件,则发出一个派生事件表示。

流和表join

一个输入是活动事件,另一个是数据库变更日志,其维护了数据库的本地最新版本。为了保持本地数据库副本的最新状态,可以订阅数据库的更新日志以及活动事件流。

表和表join(物化视图维护)

twitter的例子,从“用户查看时,需要循环遍历所有关注者以及其推文并合并”改为“获取一个时间线缓存,发推的时候并入其中。维护这个缓存需要用于推文和关注关系的事件流”。

这个流处理的过程,就是维护一个两个表(推文和关注者)join查询的物化视图。

SELECT follows.follower_id AS timeline_id,
  array_agg(tweets.* ORDER BY tewwts.timestamp DESC)
FROM tweets
JOIN follows ON follows.followee_id = tweets.sender_od
GROUP BY follows.follower_id

容错

批处理中如果MR失败,则可以简单地在另一台上重启,并丢弃失败任务的输出。
流处理过程中也会有同样的容错问题,但是处理起来不那么简单:因为使输出结果可见之前等待某个任务完成是不可行的。

  • 微批处理和检验点:将流分解为多个小块,并像小型批处理一样处理每个块。Spark Streaming就用到了。而Flink采用了一个变体,定期生成状态滚动检查点并将其写入持久化存储。
  • 重新审视原子提交:需要确保当且仅当处理成功时,所有输出和副作用才会生效。
  • 幂等性:我们的目标是丢弃任何失败任务的部分输出,以便可以安全的重试。也可以依赖幂等性来完成。
  • 故障后重建状态:将状态在本地保存,并定期进行复制。之后,当流处理器从故障中恢复的时候,新任务可以读取副本的状态并且在不丢失数据的情况下恢复处理。
comments powered by Disqus