remove bold in ref

This commit is contained in:
qtmuniao 2024-03-26 10:36:43 +08:00 committed by GitHub
parent 54e576c115
commit 6df7f28529
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

68
ch11.md
View File

@ -66,7 +66,7 @@
- **UDP 多播**。UDP 多播广泛用在金融系统的数据流中,如对时延要求很高的股票市场中的大盘动态。尽管 UDP 本身是不可靠的,但是可以在应用层增加可靠性算法(类似在应用层实现 TCP 的一些算法),对丢失的信息进行恢复(生产者需要记住所有已发送的消息,才可以按需进行重传)。 - **UDP 多播**。UDP 多播广泛用在金融系统的数据流中,如对时延要求很高的股票市场中的大盘动态。尽管 UDP 本身是不可靠的,但是可以在应用层增加可靠性算法(类似在应用层实现 TCP 的一些算法),对丢失的信息进行恢复(生产者需要记住所有已发送的消息,才可以按需进行重传)。
- **无 broker 的消息队列**。像 ZeroMQ 和 nanomsg 等不使用消息 broker 的以库形式提供的消息队列,依赖 TCP 或者 IP 多播等方式实现了支持发布订阅的消息队列。 - **无 broker 的消息队列**。像 ZeroMQ 和 nanomsg 等不使用消息 broker 的以库形式提供的消息队列,依赖 TCP 或者 IP 多播等方式实现了支持发布订阅的消息队列。
- **StatsD 和 Brubeck**。这两个系统底层依赖 UDP 协议进行传递消息,以监控所有机器、并收集相关数据指标。(在 **StatsD** 协议中只有事件都收到counter 相关指标才会正确;使用 UDP 就意味者使用一种**尽可能正确**的保证)。 - **StatsD 和 Brubeck**。这两个系统底层依赖 UDP 协议进行传递消息,以监控所有机器、并收集相关数据指标。(在 **StatsD协议中只有事件都收到counter 相关指标才会正确;使用 UDP 就意味者使用一种**尽可能正确**的保证)。
- **Webhooks**。如果消费者在网络上暴露出了一个服务,则生产者可以通过 HTTP 或者 RPC 请求(参见[经由服务的数据流REST 和 RPC](https://ddia.qtmuniao.com/#/ch04?id=%e7%bb%8f%e7%94%b1%e6%9c%8d%e5%8a%a1%e7%9a%84%e6%95%b0%e6%8d%ae%e6%b5%81%ef%bc%9arest-%e5%92%8c-rpc))来将数据打到消费者中。这就是 webhooks 背后的思想:一个服务会向另一个服务进行注册,并在有事件产生时向该服务发送一个请求。 - **Webhooks**。如果消费者在网络上暴露出了一个服务,则生产者可以通过 HTTP 或者 RPC 请求(参见[经由服务的数据流REST 和 RPC](https://ddia.qtmuniao.com/#/ch04?id=%e7%bb%8f%e7%94%b1%e6%9c%8d%e5%8a%a1%e7%9a%84%e6%95%b0%e6%8d%ae%e6%b5%81%ef%bc%9arest-%e5%92%8c-rpc))来将数据打到消费者中。这就是 webhooks 背后的思想:一个服务会向另一个服务进行注册,并在有事件产生时向该服务发送一个请求。
这种直接消息系统在其目标场景中通常能够工作的很好,但需要应用层代码自己承担、处理消息丢失的可能性。此外,这些系统能够进行的容错很有限:虽然这些系统在检测到丢包后会进行重传,但它们通常会假设**生产者和消费者都一直在线**(这是一个很强的假设)。 这种直接消息系统在其目标场景中通常能够工作的很好,但需要应用层代码自己承担、处理消息丢失的可能性。此外,这些系统能够进行的容错很有限:虽然这些系统在检测到丢包后会进行重传,但它们通常会假设**生产者和消费者都一直在线**(这是一个很强的假设)。
@ -86,7 +86,7 @@
### 对比消息代理和数据库 ### 对比消息代理和数据库
有一些消息代理甚至能够参与两阶段提交(使用 XA 或者 JTA参见 **[实践中的分布式事务](https://ddia.qtmuniao.com/#/ch09?id=%e5%ae%9e%e8%b7%b5%e4%b8%ad%e7%9a%84%e5%88%86%e5%b8%83%e5%bc%8f%e4%ba%8b%e5%8a%a1)** )。这种功能让消息代理看起来非常像数据库,尽管在实践中他们有一些非常重要的区别: 有一些消息代理甚至能够参与两阶段提交(使用 XA 或者 JTA参见[实践中的分布式事务](https://ddia.qtmuniao.com/#/ch09?id=%e5%ae%9e%e8%b7%b5%e4%b8%ad%e7%9a%84%e5%88%86%e5%b8%83%e5%bc%8f%e4%ba%8b%e5%8a%a1))。这种功能让消息代理看起来非常像数据库,尽管在实践中他们有一些非常重要的区别:
- **删除过程**:数据库会一直保存数据,直到其被**显式地**删除。然而,大部分的消息代理会在消息被消费后,隐式的对其自动删除。这种类型的消息代理并不适合对数据的长时间存储。 - **删除过程**:数据库会一直保存数据,直到其被**显式地**删除。然而,大部分的消息代理会在消息被消费后,隐式的对其自动删除。这种类型的消息代理并不适合对数据的长时间存储。
- **尺寸假设**:由于消息代理会在消息被消费后将其删除,因此大部分消息代理都会**假设**其所存数据并不是很多——所有队列都很短。在这样的假设下,如果由于消费者过慢而造成消息在消息代理中堆积(当内存中存不下后可能需要放到硬盘中),则可能造成消息代理的性能降级,所有消息都需要更长时间才能被处理。 - **尺寸假设**:由于消息代理会在消息被消费后将其删除,因此大部分消息代理都会**假设**其所存数据并不是很多——所有队列都很短。在这样的假设下,如果由于消费者过慢而造成消息在消息代理中堆积(当内存中存不下后可能需要放到硬盘中),则可能造成消息代理的性能降级,所有消息都需要更长时间才能被处理。
@ -173,7 +173,7 @@ Apache KafkaAmazon Kinesis Streams 和 Twitter 的 DistributedLog 背后都
在分区内顺序消费使得追踪消费者的消费进度非常容易:只需要记住一个偏移量即可,所有小于该偏移量的消息都被消费了,所有大于该偏移量的消息尚未被消费。因此,消息代理无需在消息粒度上追踪其是否被确认,只需要定期持久化**消费偏移量**即可。这种方法减少了元信息开销,但同时也降低了 batch 化(将一批消息一块发送出去,乱序确认)和**流水线化**(不等确认就给 Consumer 发送下一条)以提高系统吞吐的可能性。 在分区内顺序消费使得追踪消费者的消费进度非常容易:只需要记住一个偏移量即可,所有小于该偏移量的消息都被消费了,所有大于该偏移量的消息尚未被消费。因此,消息代理无需在消息粒度上追踪其是否被确认,只需要定期持久化**消费偏移量**即可。这种方法减少了元信息开销,但同时也降低了 batch 化(将一批消息一块发送出去,乱序确认)和**流水线化**(不等确认就给 Consumer 发送下一条)以提高系统吞吐的可能性。
这种偏移量的记录方式,很像单主模型数据库中的**序列号**log sequence number我们在 **[新增副本](https://ddia.qtmuniao.com/#/ch05?id=%e6%96%b0%e5%a2%9e%e5%89%af%e6%9c%ac)** 一节中讨论过。在多副本数据库中,使用序列号能让从副本在宕机重启后,从固定位置重新消费,以不错过任何写。同样的原则也适用于此,本质上,**消息代理就类似主节点,而消费者就类似从节点**。 这种偏移量的记录方式,很像单主模型数据库中的**序列号**log sequence number我们在[新增副本](https://ddia.qtmuniao.com/#/ch05?id=%e6%96%b0%e5%a2%9e%e5%89%af%e6%9c%ac)一节中讨论过。在多副本数据库中,使用序列号能让从副本在宕机重启后,从固定位置重新消费,以不错过任何写。同样的原则也适用于此,本质上,**消息代理就类似主节点,而消费者就类似从节点**。
如果一个消费者节点挂掉之后,会从消费者组中另挑选消费者来分担其原负责分区,并且从上次记录的偏移量处继续消费。如果之前的消费者处理了某些消息,但还没来得及更新消费偏移量。则这些消息会被其他节点**重复消费**,本章稍后我们会讨论解决这个问题的一些方法。 如果一个消费者节点挂掉之后,会从消费者组中另挑选消费者来分担其原负责分区,并且从上次记录的偏移量处继续消费。如果之前的消费者处理了某些消息,但还没来得及更新消费偏移量。则这些消息会被其他节点**重复消费**,本章稍后我们会讨论解决这个问题的一些方法。
@ -211,9 +211,9 @@ Apache KafkaAmazon Kinesis Streams 和 Twitter 的 DistributedLog 背后都
我们在之前提到过事件event是对某个时间点发生的事情记录。事件可以是一个用户行为一次搜索可以是传感器数值但其实也可以是**写入数据库**write to a database。写入数据库这个事情本身也可以被当做一个事件被捕获、存储和处理。我们通过这个连接可以发现硬盘上的日志只是数据库和流数据之间最基本的牵连其更深层次的关联远不止于此。 我们在之前提到过事件event是对某个时间点发生的事情记录。事件可以是一个用户行为一次搜索可以是传感器数值但其实也可以是**写入数据库**write to a database。写入数据库这个事情本身也可以被当做一个事件被捕获、存储和处理。我们通过这个连接可以发现硬盘上的日志只是数据库和流数据之间最基本的牵连其更深层次的关联远不止于此。
事实上,复制日志(在 **[日志复制](https://ddia.qtmuniao.com/#/ch05?id=%e6%97%a5%e5%bf%97%e5%a4%8d%e5%88%b6)** 小节中讨论过)就是数据库主节点在处理事务时产生的一系列写入事件。从节点将这些写入事件按顺序应用到本地数据库副本上,就会得到一样的数据库副本。复制日志中的这些事件**完整描述**了数据库中的所有**数据变更**。 事实上,复制日志(在[日志复制](https://ddia.qtmuniao.com/#/ch05?id=%e6%97%a5%e5%bf%97%e5%a4%8d%e5%88%b6)小节中讨论过)就是数据库主节点在处理事务时产生的一系列写入事件。从节点将这些写入事件按顺序应用到本地数据库副本上,就会得到一样的数据库副本。复制日志中的这些事件**完整描述**了数据库中的所有**数据变更**。
我们在 **[全序广播](https://ddia.qtmuniao.com/#/ch09?id=%e5%85%a8%e5%ba%8f%e5%b9%bf%e6%92%ad)** 也提到过状态机的复制state machine replication原则如果将所有数据库的更改都表达为事件且每个数据库副本都按照同样的顺序处理这些事件则所有的数据库副本最终会具有相同的状态。当然这里的前提是所有事件都是**确定性**的操作。)这里状态机的复制正是事件流的一种典型例子。 我们在[全序广播](https://ddia.qtmuniao.com/#/ch09?id=%e5%85%a8%e5%ba%8f%e5%b9%bf%e6%92%ad)也提到过状态机的复制state machine replication原则如果将所有数据库的更改都表达为事件且每个数据库副本都按照同样的顺序处理这些事件则所有的数据库副本最终会具有相同的状态。当然这里的前提是所有事件都是**确定性**的操作。)这里状态机的复制正是事件流的一种典型例子。
在本节中,我们首先看下异构数据系统中的一些问题,然后探索下如何从消息系统中借鉴一些思想来解决数据库中的这些问题。 在本节中,我们首先看下异构数据系统中的一些问题,然后探索下如何从消息系统中借鉴一些思想来解决数据库中的这些问题。
@ -221,7 +221,7 @@ Apache KafkaAmazon Kinesis Streams 和 Twitter 的 DistributedLog 背后都
从贯穿全书的思想可以看出,没有任何单一系统能够满足所有的数据存储、查询和处理需求。在实际中,大部分的问题场景都是由**不同的技术**组合来解决的:如,使用 OLTP 数据库来服务用户请求,使用缓存来加速常见查询,使用全文索引来处理用户搜索,使用数据仓库来进行数据分析。所有这些系统都保有一份为其服务场景优化的**专用形式**的数据副本。 从贯穿全书的思想可以看出,没有任何单一系统能够满足所有的数据存储、查询和处理需求。在实际中,大部分的问题场景都是由**不同的技术**组合来解决的:如,使用 OLTP 数据库来服务用户请求,使用缓存来加速常见查询,使用全文索引来处理用户搜索,使用数据仓库来进行数据分析。所有这些系统都保有一份为其服务场景优化的**专用形式**的数据副本。
当**同一份数据**以不同形式出现在多个数据系统中时,就需要某种手段来保持其同步:如数据库中数据条目更新后,也需要同时在缓存、搜索引擎和数据库仓库中进行同步更新。对于数据仓库来说,我们通常将该过程称之为:**ETL**。ETL 通常会拿到数据库中的所有数据、进行变换然后一次性载入数仓中。可以看出 ETL 通常是一个批处理的过程。类似的,在 **[批处理工作流的输出](https://ddia.qtmuniao.com/#/ch10?id=%e6%89%b9%e5%a4%84%e7%90%86%e5%b7%a5%e4%bd%9c%e6%b5%81%e7%9a%84%e8%be%93%e5%87%ba)** 一节中我们提到过,如何用批处理来构建搜索引擎、推荐系统和其他衍生数据系统的过程。 当**同一份数据**以不同形式出现在多个数据系统中时,就需要某种手段来保持其同步:如数据库中数据条目更新后,也需要同时在缓存、搜索引擎和数据库仓库中进行同步更新。对于数据仓库来说,我们通常将该过程称之为:**ETL**。ETL 通常会拿到数据库中的所有数据、进行变换然后一次性载入数仓中。可以看出 ETL 通常是一个批处理的过程。类似的,在[批处理工作流的输出](https://ddia.qtmuniao.com/#/ch10?id=%e6%89%b9%e5%a4%84%e7%90%86%e5%b7%a5%e4%bd%9c%e6%b5%81%e7%9a%84%e8%be%93%e5%87%ba)一节中我们提到过,如何用批处理来构建搜索引擎、推荐系统和其他衍生数据系统的过程。
如果你觉得定期全量同步数据太慢了,另一种替代方法是“**双写**”dual writes当有数据变动时应用层代码会显示的更新所有系统如写入数据的同时更新搜索引擎、让缓存失效等等。双写本质上一中**推**的方式,“推”的方式的一大特点就是及时性好,但容错性差。 如果你觉得定期全量同步数据太慢了,另一种替代方法是“**双写**”dual writes当有数据变动时应用层代码会显示的更新所有系统如写入数据的同时更新搜索引擎、让缓存失效等等。双写本质上一中**推**的方式,“推”的方式的一大特点就是及时性好,但容错性差。
@ -229,11 +229,11 @@ Apache KafkaAmazon Kinesis Streams 和 Twitter 的 DistributedLog 背后都
![Untitled](img/ch11-fig04.png) ![Untitled](img/ch11-fig04.png)
除非你使用了某些并发检测机制(参见 **[并发写入检测](https://ddia.qtmuniao.com/#/ch05?id=%e5%b9%b6%e5%8f%91%e5%86%99%e5%85%a5%e6%a3%80%e6%b5%8b)** ),否则你可能根本注意不到并发写的发生——一个值就这样悄悄的被其他值覆盖了。 除非你使用了某些并发检测机制(参见[并发写入检测](https://ddia.qtmuniao.com/#/ch05?id=%e5%b9%b6%e5%8f%91%e5%86%99%e5%85%a5%e6%a3%80%e6%b5%8b)),否则你可能根本注意不到并发写的发生——一个值就这样悄悄的被其他值覆盖了。
双写的另一个重要问题是:一个系统中的写入成功了而往另外一个系统中的写入却失败了。当然,这本质上是一个容错问题而非并发写问题,但仍然会导致两个数据系统处于不一致的状态。想要保证两个系统的写入“要么都成功、要么都失败”是一个原子提交问题(参见 **[原子提交和两阶段提交](https://ddia.qtmuniao.com/#/ch09?id=%e5%8e%9f%e5%ad%90%e6%8f%90%e4%ba%a4%e5%92%8c%e4%b8%a4%e9%98%b6%e6%ae%b5%e6%8f%90%e4%ba%a4)**),解决这个问题的代价十分高昂(两阶段提交代价很大)。 双写的另一个重要问题是:一个系统中的写入成功了而往另外一个系统中的写入却失败了。当然,这本质上是一个容错问题而非并发写问题,但仍然会导致两个数据系统处于不一致的状态。想要保证两个系统的写入“要么都成功、要么都失败”是一个原子提交问题(参见[原子提交和两阶段提交](https://ddia.qtmuniao.com/#/ch09?id=%e5%8e%9f%e5%ad%90%e6%8f%90%e4%ba%a4%e5%92%8c%e4%b8%a4%e9%98%b6%e6%ae%b5%e6%8f%90%e4%ba%a4)**),解决这个问题的代价十分高昂(两阶段提交代价很大)。
在使用单主模型的数据库中,主节点会决定写入的顺序,从节点会跟随主节点,最终数据库中所有节点的状态机都会收敛到相同的状态。但在上图中,并没有一个跨系统的、全局的主节点:数据库和搜索引擎都会**独立地**接受写入(主节点本质上就是一个对外的数据接收点,而如果有多个写入接收点,本质上是多主),而互不跟随,因此很容易发生冲突(参见 **[多主模型](https://ddia.qtmuniao.com/#/ch05?id=%e5%a4%9a%e4%b8%bb%e6%a8%a1%e5%9e%8b)**)。 在使用单主模型的数据库中,主节点会决定写入的顺序,从节点会跟随主节点,最终数据库中所有节点的状态机都会收敛到相同的状态。但在上图中,并没有一个跨系统的、全局的主节点:数据库和搜索引擎都会**独立地**接受写入(主节点本质上就是一个对外的数据接收点,而如果有多个写入接收点,本质上是多主),而互不跟随,因此很容易发生冲突(参见[多主模型](https://ddia.qtmuniao.com/#/ch05?id=%e5%a4%9a%e4%b8%bb%e6%a8%a1%e5%9e%8b)**)。
如果我们对于多个系统真正的只有一个主节点,让其他系统跟随这个主节点,这种情况才会被解决。比如,在上面的例子中,让数据库充当主节点,让存储引擎成为数据库的从节点跟,跟随其写入。但在实践中,这可能吗? 如果我们对于多个系统真正的只有一个主节点,让其他系统跟随这个主节点,这种情况才会被解决。比如,在上面的例子中,让数据库充当主节点,让存储引擎成为数据库的从节点跟,跟随其写入。但在实践中,这可能吗?
@ -255,17 +255,17 @@ Apache KafkaAmazon Kinesis Streams 和 Twitter 的 DistributedLog 背后都
本质上CDC 实现了我们上面提到的,让数据库成为领导者(事件捕获的源头),让其他系统成为跟随者。由于对消息的保序性,基于日志的消息代理非常适合将 CDC 的事件流导给其他数据系统。 本质上CDC 实现了我们上面提到的,让数据库成为领导者(事件捕获的源头),让其他系统成为跟随者。由于对消息的保序性,基于日志的消息代理非常适合将 CDC 的事件流导给其他数据系统。
数据库的触发器可以用来实现 CDC参考 **[基于触发器的复制](https://ddia.qtmuniao.com/#/ch05?id=%e5%9f%ba%e4%ba%8e%e8%a7%a6%e5%8f%91%e5%99%a8%e7%9a%84%e5%a4%8d%e5%88%b6)**)。具体来说,可以注册一些触发器来监听所有数据库表的变更,然后将变更统一写入 changelog 表。然而,这种方式容错性很低且有性能问题。需要用比较鲁棒的方式来解析复制日志,尽管可能会遇到一些问题,比如处理模式变更。 数据库的触发器可以用来实现 CDC参考[基于触发器的复制](https://ddia.qtmuniao.com/#/ch05?id=%e5%9f%ba%e4%ba%8e%e8%a7%a6%e5%8f%91%e5%99%a8%e7%9a%84%e5%a4%8d%e5%88%b6)**)。具体来说,可以注册一些触发器来监听所有数据库表的变更,然后将变更统一写入 changelog 表。然而,这种方式容错性很低且有性能问题。需要用比较鲁棒的方式来解析复制日志,尽管可能会遇到一些问题,比如处理模式变更。
在工业界中LinkedIn 的 DatabusFacebook 的 Wormhole 和 Yahoo 的 Sherpa 就大规模的使用了这种思想。Bottled Water 依托解析 PostgreSQL 的 WAL 实现了 CDCMaxwell 和 Debezium 通过解析 MySQL 的 binlog 实现的 CDCMongoriver 通过读取 MongoDB 的 oplog 实现 CDCGoldenGate 也针对 Oracle 实现了类似的功能。 在工业界中LinkedIn 的 DatabusFacebook 的 Wormhole 和 Yahoo 的 Sherpa 就大规模的使用了这种思想。Bottled Water 依托解析 PostgreSQL 的 WAL 实现了 CDCMaxwell 和 Debezium 通过解析 MySQL 的 binlog 实现的 CDCMongoriver 通过读取 MongoDB 的 oplog 实现 CDCGoldenGate 也针对 Oracle 实现了类似的功能。
和日志代理一样CDC 通常是异步的:数据库在导出事件流时通常不会等待消费者应用完成后才提交。这种设计的优点是给 CDC 增加慢的消费者并不会对源系统造成影响;但缺点就是不同系统间可能会存在日志应用滞后。(参见 **[复制滞后问题](https://ddia.qtmuniao.com/#/ch05?id=%e5%a4%8d%e5%88%b6%e6%bb%9e%e5%90%8e%e9%97%ae%e9%a2%98)** 和日志代理一样CDC 通常是异步的:数据库在导出事件流时通常不会等待消费者应用完成后才提交。这种设计的优点是给 CDC 增加慢的消费者并不会对源系统造成影响;但缺点就是不同系统间可能会存在日志应用滞后。(参见 [复制滞后问题](https://ddia.qtmuniao.com/#/ch05?id=%e5%a4%8d%e5%88%b6%e6%bb%9e%e5%90%8e%e9%97%ae%e9%a2%98)
### 初始快照 ### 初始快照
如果你有数据库从开始以来的所有日志,你可以通过重放来恢复数据库的整个状态机。但,在大多数情况下,保存所有变更日志非常占用硬盘空间,恢复的时候重放也非常耗时。为此,我们必须定期对老日志进行截断。 如果你有数据库从开始以来的所有日志,你可以通过重放来恢复数据库的整个状态机。但,在大多数情况下,保存所有变更日志非常占用硬盘空间,恢复的时候重放也非常耗时。为此,我们必须定期对老日志进行截断。
构建全文索引需要一份数据库中的全量数据,只使用包含最近变动的日志是不够的,因为丢失之前的一些数据。因此,如果你没有全量的日志记录,也可以从某个一致性的快照开始,应用该快照对应时间点之后的所有日志,也可以得到一份全量状态。我们在 **[新增副本](https://ddia.qtmuniao.com/#/ch05?id=%e6%96%b0%e5%a2%9e%e5%89%af%e6%9c%ac)** 一节中讨论过这个问题。 构建全文索引需要一份数据库中的全量数据,只使用包含最近变动的日志是不够的,因为丢失之前的一些数据。因此,如果你没有全量的日志记录,也可以从某个一致性的快照开始,应用该快照对应时间点之后的所有日志,也可以得到一份全量状态。我们在[新增副本](https://ddia.qtmuniao.com/#/ch05?id=%e6%96%b0%e5%a2%9e%e5%89%af%e6%9c%ac)一节中讨论过这个问题。
要达到上述目的,就需要数据库的快照能够和变更日志中的**某个下标对应上**,这样我们在从快照中恢复之后,才能知道从哪个变更日志开始回放。有些 CDC 工具直接集成了快照功能,但有的就需要自己手动做快照。 要达到上述目的,就需要数据库的快照能够和变更日志中的**某个下标对应上**,这样我们在从快照中恢复之后,才能知道从哪个变更日志开始回放。有些 CDC 工具直接集成了快照功能,但有的就需要自己手动做快照。
@ -304,7 +304,7 @@ Kafka Connect 是一个可以将数据库 CDC 导出的流接入 Kafka 的工具
例如,我们如果保存事件“学生取消了选课”,就很清晰直观;但如果保存事件“从选课表中删除一行,往学生反馈表中增加一个取消原因”,其实就隐含了对系统底层的很多假设。如果之后应用层产生一个新的事件,例如“该课空缺将会被分配给等待列表中的下一个人”,则使用事件溯源的方式可以轻松将所有事件串联起来。但如果使用**记录副作用**的方式,一旦底层系统发生变更,就没办法将跨系统事件关联上了。 例如,我们如果保存事件“学生取消了选课”,就很清晰直观;但如果保存事件“从选课表中删除一行,往学生反馈表中增加一个取消原因”,其实就隐含了对系统底层的很多假设。如果之后应用层产生一个新的事件,例如“该课空缺将会被分配给等待列表中的下一个人”,则使用事件溯源的方式可以轻松将所有事件串联起来。但如果使用**记录副作用**的方式,一旦底层系统发生变更,就没办法将跨系统事件关联上了。
数据溯源模型很像**编年表数据模型**chronicle data model与此同时**事件日志**event log又很像星型模型参见 **[AP 建模:星状型和雪花型](https://ddia.qtmuniao.com/#/ch03?id=ap-%e5%bb%ba%e6%a8%a1%ef%bc%9a%e6%98%9f%e7%8a%b6%e5%9e%8b%e5%92%8c%e9%9b%aa%e8%8a%b1%e5%9e%8b)**中的事实表fact table 数据溯源模型很像**编年表数据模型**chronicle data model与此同时**事件日志**event log又很像星型模型参见[AP 建模:星状型和雪花型](https://ddia.qtmuniao.com/#/ch03?id=ap-%e5%bb%ba%e6%a8%a1%ef%bc%9a%e6%98%9f%e7%8a%b6%e5%9e%8b%e5%92%8c%e9%9b%aa%e8%8a%b1%e5%9e%8b)中的事实表fact table
人们也开发了一些专用的数据库用来进行事件溯源,如 [Event Store](https://www.eventstore.com/)。但一般来说,事件溯源并不和任何特定的底层存储绑定。也可以基于传统的数据库和消息代理来构建事件溯源的应用。 人们也开发了一些专用的数据库用来进行事件溯源,如 [Event Store](https://www.eventstore.com/)。但一般来说,事件溯源并不和任何特定的底层存储绑定。也可以基于传统的数据库和消息代理来构建事件溯源的应用。
@ -325,7 +325,7 @@ Kafka Connect 是一个可以将数据库 CDC 导出的流接入 Kafka 的工具
在事件溯源的哲学里,会仔细甄别**事件**event和**命令**commands。当用户的请求刚到达系统时表现形式是一个*命令*:因为还有可能失败,比如不符合系统的一致性检查。应用层必须先要校验该命令可以执行,如果校验成功,命令被接受,就会转变为系统内部的一个持久化的、不可变的*事件*。 在事件溯源的哲学里,会仔细甄别**事件**event和**命令**commands。当用户的请求刚到达系统时表现形式是一个*命令*:因为还有可能失败,比如不符合系统的一致性检查。应用层必须先要校验该命令可以执行,如果校验成功,命令被接受,就会转变为系统内部的一个持久化的、不可变的*事件*。
举个例子,如果用户想用某个用户名进行注册、在剧院或者航班上预定一个座位时,相关系统首先检查用户名有没有被使用、相关座位还在不在。(我们在 **[容错的共识算法](https://ddia.qtmuniao.com/#/ch09?id=%e5%ae%b9%e9%94%99%e7%9a%84%e5%85%b1%e8%af%86%e7%ae%97%e6%b3%95)** 小节中讨论过这个例子)如果检查通过,系统就会产生一个事件,表明该用户名被该用户 ID 注册了,或者特定的座位给该用户预留了。 举个例子,如果用户想用某个用户名进行注册、在剧院或者航班上预定一个座位时,相关系统首先检查用户名有没有被使用、相关座位还在不在。(我们在[容错的共识算法](https://ddia.qtmuniao.com/#/ch09?id=%e5%ae%b9%e9%94%99%e7%9a%84%e5%85%b1%e8%af%86%e7%ae%97%e6%b3%95) 小节中讨论过这个例子)如果检查通过,系统就会产生一个事件,表明该用户名被该用户 ID 注册了,或者特定的座位给该用户预留了。
在事件产生的那一刻,就变成了一个**事实**。即使客户之后打算更改或者取消预定,也只是会新产生一个新的事件,而不会修改或者删除之前的事件。 在事件产生的那一刻,就变成了一个**事实**。即使客户之后打算更改或者取消预定,也只是会新产生一个新的事件,而不会修改或者删除之前的事件。
@ -336,7 +336,7 @@ Kafka Connect 是一个可以将数据库 CDC 导出的流接入 Kafka 的工具
1. **意向预定**。系统会进行完整性校验。 1. **意向预定**。系统会进行完整性校验。
2. **确认预定**。收到系统校验通过回复后,再发一个请求进行确认。 2. **确认预定**。收到系统校验通过回复后,再发一个请求进行确认。
我们在 **[使用全序广播实现线性一致性存储](https://ddia.qtmuniao.com/#/ch09?id=%e4%bd%bf%e7%94%a8%e5%85%a8%e5%ba%8f%e5%b9%bf%e6%92%ad%e5%ae%9e%e7%8e%b0%e7%ba%bf%e6%80%a7%e4%b8%80%e8%87%b4%e6%80%a7%e5%ad%98%e5%82%a8)** 一节中讨论过。这种拆分使得校验环节可以异步的发生。 我们在[使用全序广播实现线性一致性存储](https://ddia.qtmuniao.com/#/ch09?id=%e4%bd%bf%e7%94%a8%e5%85%a8%e5%ba%8f%e5%b9%bf%e6%92%ad%e5%ae%9e%e7%8e%b0%e7%ba%bf%e6%80%a7%e4%b8%80%e8%87%b4%e6%80%a7%e5%ad%98%e5%82%a8)一节中讨论过。这种拆分使得校验环节可以异步的发生。
## 状态、流和不可变性 ## 状态、流和不可变性
@ -370,7 +370,7 @@ Kafka Connect 是一个可以将数据库 CDC 导出的流接入 Kafka 的工具
如果某条记录出错了,会计通常不会直接更改账簿中的出错记录,而是通过追加一条修正该出错的交易。例如一条对客户多收了的钱的退款交易。由于审计需要等原因,这条错误交易会在账簿中一直存在下去。如果从错误账簿的计算出的报表已经对外发布,则需要在下一个记账周期中进行修正。这种不可变性在会计行业中很常见。 如果某条记录出错了,会计通常不会直接更改账簿中的出错记录,而是通过追加一条修正该出错的交易。例如一条对客户多收了的钱的退款交易。由于审计需要等原因,这条错误交易会在账簿中一直存在下去。如果从错误账簿的计算出的报表已经对外发布,则需要在下一个记账周期中进行修正。这种不可变性在会计行业中很常见。
这种保存所有不可变记录的**可审计性**不仅在财务系统中很重要,在其他没有那么严监管的系统中也有很多好处。我们在 **[批处理输出的哲学](https://ddia.qtmuniao.com/#/ch10?id=%e6%89%b9%e5%a4%84%e7%90%86%e8%be%93%e5%87%ba%e7%9a%84%e5%93%b2%e5%ad%a6)**中讨论过,如果我们允许对原始数据进行破坏性的**原地修改**,则在部署的有 bug 的代码后造成了数据破坏时,恢复原始数据会非常困难。如果我们使用 append-only 的不可变事件日志形式来进行修改,**定位故障位置**和**进行错误恢复**都会变得简单很多。 这种保存所有不可变记录的**可审计性**不仅在财务系统中很重要,在其他没有那么严监管的系统中也有很多好处。我们在 [批处理输出的哲学](https://ddia.qtmuniao.com/#/ch10?id=%e6%89%b9%e5%a4%84%e7%90%86%e8%be%93%e5%87%ba%e7%9a%84%e5%93%b2%e5%ad%a6)中讨论过,如果我们允许对原始数据进行破坏性的**原地修改**,则在部署的有 bug 的代码后造成了数据破坏时,恢复原始数据会非常困难。如果我们使用 append-only 的不可变事件日志形式来进行修改,**定位故障位置**和**进行错误恢复**都会变得简单很多。
另一方面,**不可变的事件记录下了比当前状态更多的信息**。例如,在购物网站的场景中,一个用户将某个商品加到了购物车中,后来又删掉了。尽管从最终下单的状态来说,第二个事件抵消了第一个事件的影响。但从分析用户先增后删意图的角度来讲,这两个事件并不能抵消。可能是因为他们想之后再买,也可能是因为他们找到了替代品。这个先增后删的信息回被事件日志中记录下来,但是数据库中的订单表中却没有相关条目。 另一方面,**不可变的事件记录下了比当前状态更多的信息**。例如,在购物网站的场景中,一个用户将某个商品加到了购物车中,后来又删掉了。尽管从最终下单的状态来说,第二个事件抵消了第一个事件的影响。但从分析用户先增后删意图的角度来讲,这两个事件并不能抵消。可能是因为他们想之后再买,也可能是因为他们找到了替代品。这个先增后删的信息回被事件日志中记录下来,但是数据库中的订单表中却没有相关条目。
@ -384,25 +384,25 @@ Kafka Connect 是一个可以将数据库 CDC 导出的流接入 Kafka 的工具
传统数据库的和模式设计有一种误解:**数据必须以面向查询的方式进行写入**(注:其实也不算误解,因为这样可以避免翻译计算耗费和额外存储耗费)。如果你能自由的将数据从对写优化的格式转化为对读优化的数据状态,那么针对数据**规范化**(去除冗余)和**去规范化**(保持冗余)的考量讨论就变的无关紧要了:因为可以使用翻译过程来保证冗余数据的一致性性,且同时按所需方式按利于读的方式来去规范化(也就是冗余)地组织数据。 传统数据库的和模式设计有一种误解:**数据必须以面向查询的方式进行写入**(注:其实也不算误解,因为这样可以避免翻译计算耗费和额外存储耗费)。如果你能自由的将数据从对写优化的格式转化为对读优化的数据状态,那么针对数据**规范化**(去除冗余)和**去规范化**(保持冗余)的考量讨论就变的无关紧要了:因为可以使用翻译过程来保证冗余数据的一致性性,且同时按所需方式按利于读的方式来去规范化(也就是冗余)地组织数据。
**[衡量负载](https://ddia.qtmuniao.com/#/ch01?id=%e8%a1%a1%e9%87%8f%e8%b4%9f%e8%bd%bd)** 一节中我们讨论过 Twitter 的首页时间线(也称“瀑布流”),本质上是对所有关注人最近发的推文的缓存(类似于一个信箱)。这也是一个针对状态进行读优化的例子:首页时间线是非常去规范化的,因为你的推文会被所有关注你的用户首页冗余了一份。然而,**扇出服务**fan-out service类似我们上面提到的“翻译服务”会保持这些首页“瀑布流”信息和新发推文、新的关注关系保持同步和一致从而让这些冗余的一致性可控。 在[衡量负载](https://ddia.qtmuniao.com/#/ch01?id=%e8%a1%a1%e9%87%8f%e8%b4%9f%e8%bd%bd)一节中我们讨论过 Twitter 的首页时间线(也称“瀑布流”),本质上是对所有关注人最近发的推文的缓存(类似于一个信箱)。这也是一个针对状态进行读优化的例子:首页时间线是非常去规范化的,因为你的推文会被所有关注你的用户首页冗余了一份。然而,**扇出服务**fan-out service类似我们上面提到的“翻译服务”会保持这些首页“瀑布流”信息和新发推文、新的关注关系保持同步和一致从而让这些冗余的一致性可控。
### 并发控制 ### 并发控制
事件溯源event souring和 CDC 的最大缺点在于,事件日志的产生和消费过程通常是**异步的**,因此可能会出现:用户已经写入了某个事件到日志中,但从某个日志衍生视图中去读取,却发现该写入还并没有反映到该**读取视图**中。我们在 **[读你所写](https://ddia.qtmuniao.com/#/ch05?id=%e8%af%bb%e4%bd%a0%e6%89%80%e5%86%99)** 一节中讨论过该问题和一些可用的解决方案。 事件溯源event souring和 CDC 的最大缺点在于,事件日志的产生和消费过程通常是**异步的**,因此可能会出现:用户已经写入了某个事件到日志中,但从某个日志衍生视图中去读取,却发现该写入还并没有反映到该**读取视图**中。我们在[读你所写](https://ddia.qtmuniao.com/#/ch05?id=%e8%af%bb%e4%bd%a0%e6%89%80%e5%86%99)一节中讨论过该问题和一些可用的解决方案。
一种方案是将**追加事件到日志**和**更新读取视图**两个过程进行同步。但这要求使用事务将两者包进一个原子单元中,具体来说,你需要: 一种方案是将**追加事件到日志**和**更新读取视图**两个过程进行同步。但这要求使用事务将两者包进一个原子单元中,具体来说,你需要:
1. 将日志更新和视图读取放到一个存储系统中,或者 1. 将日志更新和视图读取放到一个存储系统中,或者
2. 使用一个分布式事务来协调这两个系统,又或 2. 使用一个分布式事务来协调这两个系统,又或
3. **[使用全序广播实现线性一致性存储](https://ddia.qtmuniao.com/#/ch09?id=%e4%bd%bf%e7%94%a8%e5%85%a8%e5%ba%8f%e5%b9%bf%e6%92%ad%e5%ae%9e%e7%8e%b0%e7%ba%bf%e6%80%a7%e4%b8%80%e8%87%b4%e6%80%a7%e5%ad%98%e5%82%a8)** 3. [使用全序广播实现线性一致性存储](https://ddia.qtmuniao.com/#/ch09?id=%e4%bd%bf%e7%94%a8%e5%85%a8%e5%ba%8f%e5%b9%bf%e6%92%ad%e5%ae%9e%e7%8e%b0%e7%ba%bf%e6%80%a7%e4%b8%80%e8%87%b4%e6%80%a7%e5%ad%98%e5%82%a8)
但另一方面,从事件日志中计算当前系统状态也会简化并发控制。很多针对多对象的事务(参见 **[单对象和多对象操作](https://ddia.qtmuniao.com/#/ch07?id=%e5%8d%95%e5%af%b9%e8%b1%a1%e5%92%8c%e5%a4%9a%e5%af%b9%e8%b1%a1%e6%93%8d%e4%bd%9c)** )需求本质上是因为单个用户需要在**多个物理位置**同时更改数据。但使用事件溯源,我们可以设计一个对用户的多个行为自包含的事件,从而将所有单用户的写入点收束到一处——对事件日志进行追加——从而很容易实现原子性。 但另一方面,从事件日志中计算当前系统状态也会简化并发控制。很多针对多对象的事务(参见[单对象和多对象操作](https://ddia.qtmuniao.com/#/ch07?id=%e5%8d%95%e5%af%b9%e8%b1%a1%e5%92%8c%e5%a4%9a%e5%af%b9%e8%b1%a1%e6%93%8d%e4%bd%9c) )需求本质上是因为单个用户需要在**多个物理位置**同时更改数据。但使用事件溯源,我们可以设计一个对用户的多个行为自包含的事件,从而将所有单用户的写入点收束到一处——对事件日志进行追加——从而很容易实现原子性。
如果事件日志和应用状态使用相同的分区方式(例如,消费分区 3 的事件日志,产生的更新也只需要作用于分区 3 的应用状态),则只需要用一个单线程消费事件日志即可,而无需任何对写的并发控制 —— 我们可以通过安排,让其每次只消费一个事件即可(参见 **[物理上串行](https://ddia.qtmuniao.com/#/ch07?id=%e7%89%a9%e7%90%86%e4%b8%8a%e4%b8%b2%e8%a1%8c)**)。通过确定该分区中所有事件的状态,日志本身就已经消除了并发带来的不确定性。但如果单个事件会涉及多个分区状态的更新,就需要做一些额外的工作了,我们下一章中会继续讨论。 如果事件日志和应用状态使用相同的分区方式(例如,消费分区 3 的事件日志,产生的更新也只需要作用于分区 3 的应用状态),则只需要用一个单线程消费事件日志即可,而无需任何对写的并发控制 —— 我们可以通过安排,让其每次只消费一个事件即可(参见[物理上串行](https://ddia.qtmuniao.com/#/ch07?id=%e7%89%a9%e7%90%86%e4%b8%8a%e4%b8%b2%e8%a1%8c))。通过确定该分区中所有事件的状态,日志本身就已经消除了并发带来的不确定性。但如果单个事件会涉及多个分区状态的更新,就需要做一些额外的工作了,我们下一章中会继续讨论。
### 不可变性的一些局限 ### 不可变性的一些局限
很多不使用事件溯源的系统很多时候也**间接地依赖**了不可变性:很多数据库在内部实现时,使用了不可变的数据结构和**多版本的数据管理**来支持基于时间点的**快照**(参见 **[索引和快照隔离](https://ddia.qtmuniao.com/#/ch07?id=%e7%b4%a2%e5%bc%95%e5%92%8c%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb)**)。像 Git、Mercurial 和 Fossil 之类的版本控制系统,也是依赖不可变数据来保存每个文件的历史版本。 很多不使用事件溯源的系统很多时候也**间接地依赖**了不可变性:很多数据库在内部实现时,使用了不可变的数据结构和**多版本的数据管理**来支持基于时间点的**快照**(参见[索引和快照隔离](https://ddia.qtmuniao.com/#/ch07?id=%e7%b4%a2%e5%bc%95%e5%92%8c%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb))。像 Git、Mercurial 和 Fossil 之类的版本控制系统,也是依赖不可变数据来保存每个文件的历史版本。
我们若想将所有**不可变的**事件变更历史保存下来,在多大程度上是可行的?其回答取决于我们数据集的“**流失量**”the amount of churn。在某些负载中读取居多而增删极少因此很容易做成不可变的。但在另外一些负载中但涉及更改的数据子集在整体数据集中占比很大在这种情况下不可变的更改历史将会非常大数据的碎片化会变成一个大问题**数据压缩**和**垃圾回收**的性能便成为维持系统稳定性和鲁棒性中至关重要的因素了。 我们若想将所有**不可变的**事件变更历史保存下来,在多大程度上是可行的?其回答取决于我们数据集的“**流失量**”the amount of churn。在某些负载中读取居多而增删极少因此很容易做成不可变的。但在另外一些负载中但涉及更改的数据子集在整体数据集中占比很大在这种情况下不可变的更改历史将会非常大数据的碎片化会变成一个大问题**数据压缩**和**垃圾回收**的性能便成为维持系统稳定性和鲁棒性中至关重要的因素了。
@ -418,7 +418,7 @@ Kafka Connect 是一个可以将数据库 CDC 导出的流接入 Kafka 的工具
余下部分要讨论的是,当你已经有一个流了要如何做——即,如何处理流。广义上来说,我们有三种选择: 余下部分要讨论的是,当你已经有一个流了要如何做——即,如何处理流。广义上来说,我们有三种选择:
1. **写入存储系统**。我们可以将事件流中的数据写入到数据库、缓存、搜索引擎等类似的存储系统中,以供其他客户端来查询。如图 11-5 所示,这种方法可以和那好的保持数据库和其他数据系统的一致。尤其当流写入作为数据库的唯一数据来源时,更是如此。我们在 **[批处理工作流的输出](https://ddia.qtmuniao.com/#/ch10?id=%e6%89%b9%e5%a4%84%e7%90%86%e5%b7%a5%e4%bd%9c%e6%b5%81%e7%9a%84%e8%be%93%e5%87%ba)** 讨论过将批数据写入存储系统中,此处流也是类似的。 1. **写入存储系统**。我们可以将事件流中的数据写入到数据库、缓存、搜索引擎等类似的存储系统中,以供其他客户端来查询。如图 11-5 所示,这种方法可以和那好的保持数据库和其他数据系统的一致。尤其当流写入作为数据库的唯一数据来源时,更是如此。我们在[批处理工作流的输出](https://ddia.qtmuniao.com/#/ch10?id=%e6%89%b9%e5%a4%84%e7%90%86%e5%b7%a5%e4%bd%9c%e6%b5%81%e7%9a%84%e8%be%93%e5%87%ba)讨论过将批数据写入存储系统中,此处流也是类似的。
2. **给人看**。可以将流中的事件以某种方式(比如邮件或者通知)推送给用户,或者将流中的事件渲染到实时可视化的数据面板上。在这些情形中,人是数据流最终的消费者。 2. **给人看**。可以将流中的事件以某种方式(比如邮件或者通知)推送给用户,或者将流中的事件渲染到实时可视化的数据面板上。在这些情形中,人是数据流最终的消费者。
3. **产生新的流形成拓扑**。可以在一个系统内处理多个流。并生成多个新的流作为输出。此时,一个数据流在最终产生输出前,可能会经过由多个处理阶段组成的流水线。 3. **产生新的流形成拓扑**。可以在一个系统内处理多个流。并生成多个新的流作为输出。此时,一个数据流在最终产生输出前,可能会经过由多个处理阶段组成的流水线。
@ -459,13 +459,13 @@ CEP 系统常使用偏高层的描述式查询语言,如 SQL 或者图形用
这些统计信息通常是针对固定时间间隔计算出来的——例如,你可能想知道过去五分钟内的平均 QPS 和九十九分位的响应延迟。取几分钟内的平均延迟能够平滑掉突发的尖刺,但仍能够展示出流量变化的趋势。我们用于聚合数据进行计算的时间段通常称为“**窗口**”window或称时间窗口在“时间推理”一节我们将会讨论时间窗口的更多细节。 这些统计信息通常是针对固定时间间隔计算出来的——例如,你可能想知道过去五分钟内的平均 QPS 和九十九分位的响应延迟。取几分钟内的平均延迟能够平滑掉突发的尖刺,但仍能够展示出流量变化的趋势。我们用于聚合数据进行计算的时间段通常称为“**窗口**”window或称时间窗口在“时间推理”一节我们将会讨论时间窗口的更多细节。
数据流分析中有时会使用一些概率算法,如使用布隆过滤器(我们 **[性能优化](https://ddia.qtmuniao.com/#/ch03?id=%e6%80%a7%e8%83%bd%e4%bc%98%e5%8c%96)** 小节中遇到过)来管理成员资格、使用 HyperLogLog 来进行基数估计,还有各种各样的分位数值估计算法。概率算法会产生近似结果,但通常比确定算法需要更少的资源(如 CPU内存。使用近似算法容易让人们认为流式处理系统总是损失精度和不精确的但这种看法并不正确**流式处理本身并非近似的,概率算法只是一种在分析场景中的处理优化**。 数据流分析中有时会使用一些概率算法,如使用布隆过滤器(我们[性能优化](https://ddia.qtmuniao.com/#/ch03?id=%e6%80%a7%e8%83%bd%e4%bc%98%e5%8c%96)小节中遇到过)来管理成员资格、使用 HyperLogLog 来进行基数估计,还有各种各样的分位数值估计算法。概率算法会产生近似结果,但通常比确定算法需要更少的资源(如 CPU内存。使用近似算法容易让人们认为流式处理系统总是损失精度和不精确的但这种看法并不正确**流式处理本身并非近似的,概率算法只是一种在分析场景中的处理优化**。
很多开源的分布式流处理库框架都是针对分析场景设计的例如Apache Storm、Spark Streaming、Flink、Concord、Samza 和 Kafka Streams。有一些云上的托管服务也是如Google Cloud Dataflow 和 Azure Stream Analytics。 很多开源的分布式流处理库框架都是针对分析场景设计的例如Apache Storm、Spark Streaming、Flink、Concord、Samza 和 Kafka Streams。有一些云上的托管服务也是如Google Cloud Dataflow 和 Azure Stream Analytics。
### 管理物化视图 ### 管理物化视图
我们在数据库和流处理TODO: link一节中提到过数据库的变更流可以用来维护一些衍生数据系统如缓存、搜索索引和数据仓库。这些衍生数据系统可以看做物化视图参见 **[聚合:数据立方和物化视图](https://ddia.qtmuniao.com/#/ch03?id=%e8%81%9a%e5%90%88%ef%bc%9a%e6%95%b0%e6%8d%ae%e7%ab%8b%e6%96%b9%e5%92%8c%e7%89%a9%e5%8c%96%e8%a7%86%e5%9b%be)**)的一些特例:**构造一个面向某种查询优化的视图,并将新到来的更改不断更新到该视图上** 我们在数据库和流处理TODO: link一节中提到过数据库的变更流可以用来维护一些衍生数据系统如缓存、搜索索引和数据仓库。这些衍生数据系统可以看做物化视图参见[聚合:数据立方和物化视图](https://ddia.qtmuniao.com/#/ch03?id=%e8%81%9a%e5%90%88%ef%bc%9a%e6%95%b0%e6%8d%ae%e7%ab%8b%e6%96%b9%e5%92%8c%e7%89%a9%e5%8c%96%e8%a7%86%e5%9b%be)**)的一些特例:**构造一个面向某种查询优化的视图,并将新到来的更改不断更新到该视图上。
类似的,在事件溯源系统中,应用层的状态也是通过持续应用事件日志来维持的;这里的应用层的状态本质上也是一种物化视图。但和**数据流分析**场景不同,在**物化视图**场景中仅考虑固定的时间窗口内的状态是不够的——物化视图通常需要将所有时间以来的事件进行叠加应用除非有些过时日志已经通过日志紧缩TODO: link被删掉了。如果仍然用时间窗口来解释的话就是——在物化视图的场景中你需要一个足够长的、一直延伸到事件流起点的时间窗口。 类似的,在事件溯源系统中,应用层的状态也是通过持续应用事件日志来维持的;这里的应用层的状态本质上也是一种物化视图。但和**数据流分析**场景不同,在**物化视图**场景中仅考虑固定的时间窗口内的状态是不够的——物化视图通常需要将所有时间以来的事件进行叠加应用除非有些过时日志已经通过日志紧缩TODO: link被删掉了。如果仍然用时间窗口来解释的话就是——在物化视图的场景中你需要一个足够长的、一直延伸到事件流起点的时间窗口。
@ -481,13 +481,13 @@ CEP 系统常使用偏高层的描述式查询语言,如 SQL 或者图形用
### 消息传递和 RPC ### 消息传递和 RPC
**[经由消息传递的数据流](https://ddia.qtmuniao.com/#/ch04?id=%e7%bb%8f%e7%94%b1%e6%b6%88%e6%81%af%e4%bc%a0%e9%80%92%e7%9a%84%e6%95%b0%e6%8d%ae%e6%b5%81)**一节中我们讨论过,消息传递系统在某种程度上可以替代 RPC——即消息系统也可以作为一种服务间的沟通机制比如在 Actor 模型中。尽管这些系统也是基于消息和事件的,但我们通常**并不会**将其当成一种流式处理: 在 [经由消息传递的数据流](https://ddia.qtmuniao.com/#/ch04?id=%e7%bb%8f%e7%94%b1%e6%b6%88%e6%81%af%e4%bc%a0%e9%80%92%e7%9a%84%e6%95%b0%e6%8d%ae%e6%b5%81)一节中我们讨论过,消息传递系统在某种程度上可以替代 RPC——即消息系统也可以作为一种服务间的沟通机制比如在 Actor 模型中。尽管这些系统也是基于消息和事件的,但我们通常**并不会**将其当成一种流式处理:
- Actor 框架本质上是一种管理**并发实体**间的分布式执行和通信沟通的机制,而流处理则更多是侧重**数据处理**技术。 - Actor 框架本质上是一种管理**并发实体**间的分布式执行和通信沟通的机制,而流处理则更多是侧重**数据处理**技术。
- Actor 之间的通信通常是短暂且一对一的,而事件日志则是持久的且通常是多下游的(多个订阅者/消费者)。 - Actor 之间的通信通常是短暂且一对一的,而事件日志则是持久的且通常是多下游的(多个订阅者/消费者)。
- Actor 间可以用任意模式注意区分模式和方式Actor 的通信方式肯定是消息传递)进行通信(包括循环往复的请求-应答模式),但流处理通常由有向无环的流水线构成。每个任务通常以多个流作为输入,进行处理后,然后产生一个新的流。 - Actor 间可以用任意模式注意区分模式和方式Actor 的通信方式肯定是消息传递)进行通信(包括循环往复的请求-应答模式),但流处理通常由有向无环的流水线构成。每个任务通常以多个流作为输入,进行处理后,然后产生一个新的流。
也就是说,类 RPC 系统和流处理系统间在定位上有一些交叉。举个例子Apache Storm 有一个叫做**分布式 RPC** 的功能,可以将用户的一个查询分发到所有处理事件流上的节点。在这些节点上,查询请求和事件会被交替的执行,之后所有查询结果会被聚合后返回给用户(参阅多分区的数据处理)。 也就是说,类 RPC 系统和流处理系统间在定位上有一些交叉。举个例子Apache Storm 有一个叫做**分布式 RPC的功能可以将用户的一个查询分发到所有处理事件流上的节点。在这些节点上查询请求和事件会被交替的执行之后所有查询结果会被聚合后返回给用户参阅多分区的数据处理
当然,也可以使用 Actor 框架来进行流处理。但在系统节点宕机时,这些框架通常不对消息的交付有任何保证。因此,除非你实现额外的逻辑,否则这种方式通常不能够进行容错。 当然,也可以使用 Actor 框架来进行流处理。但在系统节点宕机时,这些框架通常不对消息的交付有任何保证。因此,除非你实现额外的逻辑,否则这种方式通常不能够进行容错。
@ -503,7 +503,7 @@ CEP 系统常使用偏高层的描述式查询语言,如 SQL 或者图形用
### 事件时间点 vs 处理时间点 ### 事件时间点 vs 处理时间点
有很多原因会造成**处理延迟**:处理排队、网络故障(参见 **[不可靠的网络](https://ddia.qtmuniao.com/#/ch08?id=%e4%b8%8d%e5%8f%af%e9%9d%a0%e7%9a%84%e7%bd%91%e7%bb%9c)**)、机器性能低下造成的消息在消息代理和处理节点的堆积、流消费者的重启或者 bug 修复后对之前事件进行重新处理。 有很多原因会造成**处理延迟**:处理排队、网络故障(参见[不可靠的网络](https://ddia.qtmuniao.com/#/ch08?id=%e4%b8%8d%e5%8f%af%e9%9d%a0%e7%9a%84%e7%bd%91%e7%bb%9c))、机器性能低下造成的消息在消息代理和处理节点的堆积、流消费者的重启或者 bug 修复后对之前事件进行重新处理。
更有甚者,**消息延迟可能会导致无法预知的消息乱序**。举个例子,一个用户首先发送了一个 web 请求(被 web 服务器 A 处理),然后发送了第二个请求(被服务器 B 处理。A 和 B 各自将其包装成事件消息进行发送,但是 B 的事件比 A 的事件先到达消息代理。则此时,流处理任务会首先看到 B 事件,然后才看到 A 事件,但他们的实际产生顺序其实是相反的。 更有甚者,**消息延迟可能会导致无法预知的消息乱序**。举个例子,一个用户首先发送了一个 web 请求(被 web 服务器 A 处理),然后发送了第二个请求(被服务器 B 处理。A 和 B 各自将其包装成事件消息进行发送,但是 B 的事件比 A 的事件先到达消息代理。则此时,流处理任务会首先看到 B 事件,然后才看到 A 事件,但他们的实际产生顺序其实是相反的。
@ -530,7 +530,7 @@ CEP 系统常使用偏高层的描述式查询语言,如 SQL 或者图形用
当事件能够在系统中的不同环节进行缓存时,使用给事件附加时间戳的方式就会变得问题多多。举个例子,考虑一个手机定时上报使用参数给服务器的场景。当设备**离线**时,该 APP 也可能被使用,此时只能选择先将这些指标**缓存**到设备本地,等待将来(可能是数小时甚至数天后)重新联网后再次发送。对于该事件流的消费者来说,这些事件都会成为“拖油瓶”事件。 当事件能够在系统中的不同环节进行缓存时,使用给事件附加时间戳的方式就会变得问题多多。举个例子,考虑一个手机定时上报使用参数给服务器的场景。当设备**离线**时,该 APP 也可能被使用,此时只能选择先将这些指标**缓存**到设备本地,等待将来(可能是数小时甚至数天后)重新联网后再次发送。对于该事件流的消费者来说,这些事件都会成为“拖油瓶”事件。
在这个场景中,原则上事件的时间戳应该是用户交互发生时的手机本地始终所显示的时间点。但由于该时钟可能会被用户有意或者无意的进行错误设置(参阅 **[时钟同步和精度问题](https://ddia.qtmuniao.com/#/ch08?id=%e6%97%b6%e9%92%9f%e5%90%8c%e6%ad%a5%e5%92%8c%e7%b2%be%e5%ba%a6%e9%97%ae%e9%a2%98)**),该设备时钟系统通常是不可信的。相比来说,由于服务器是受我们控制的,其收到事件的时间点通常更为准确一些。但如果用其来去近似用户交互时间,又可能会有很大偏差。 在这个场景中,原则上事件的时间戳应该是用户交互发生时的手机本地始终所显示的时间点。但由于该时钟可能会被用户有意或者无意的进行错误设置(参阅[时钟同步和精度问题](https://ddia.qtmuniao.com/#/ch08?id=%e6%97%b6%e9%92%9f%e5%90%8c%e6%ad%a5%e5%92%8c%e7%b2%be%e5%ba%a6%e9%97%ae%e9%a2%98)),该设备时钟系统通常是不可信的。相比来说,由于服务器是受我们控制的,其收到事件的时间点通常更为准确一些。但如果用其来去近似用户交互时间,又可能会有很大偏差。
为了校正可能有误的设备时钟,一种方法是给每个事件记录三个时间戳: 为了校正可能有误的设备时钟,一种方法是给每个事件记录三个时间戳:
@ -569,11 +569,11 @@ CEP 系统常使用偏高层的描述式查询语言,如 SQL 或者图形用
### 流表连接(流扩充) ### 流表连接(流扩充)
**[例子:用户行为数据分析](https://ddia.qtmuniao.com/#/ch10?id=%e4%be%8b%e5%ad%90%ef%bc%9a%e7%94%a8%e6%88%b7%e8%a1%8c%e4%b8%ba%e6%95%b0%e6%8d%ae%e5%88%86%e6%9e%90)** (图 10-2一节中我们讨论了使用批处理任务对两个数据集进行 join 的例子——对用户活动事件集和用户资料数据库进行 join。可以把用户活动事件看做流然后在流处理任务中进行持续的、但同样逻辑的 join 。在这种情形下,输入是一个包含用户 ID 的事件流,输出是附加了用户的个人资料信息扩充事件流。因此,该过程有时候也被称为使用数据库中的信息对活动事件进行**扩充**。 在[例子:用户行为数据分析](https://ddia.qtmuniao.com/#/ch10?id=%e4%be%8b%e5%ad%90%ef%bc%9a%e7%94%a8%e6%88%b7%e8%a1%8c%e4%b8%ba%e6%95%b0%e6%8d%ae%e5%88%86%e6%9e%90)(图 10-2一节中我们讨论了使用批处理任务对两个数据集进行 join 的例子——对用户活动事件集和用户资料数据库进行 join。可以把用户活动事件看做流然后在流处理任务中进行持续的、但同样逻辑的 join 。在这种情形下,输入是一个包含用户 ID 的事件流,输出是附加了用户的个人资料信息扩充事件流。因此,该过程有时候也被称为使用数据库中的信息对活动事件进行**扩充**。
为了进行该 join在处理时需要每次从流中取一个活动事件然后依据事件中的用户 ID 在数据库中查找相关信息,最后将找到的用户资料信息附加到事件中。数据库的查找可能**发生在远端**;然而,就像我们在 **[例子:用户行为数据分析](https://ddia.qtmuniao.com/#/ch10?id=%e4%be%8b%e5%ad%90%ef%bc%9a%e7%94%a8%e6%88%b7%e8%a1%8c%e4%b8%ba%e6%95%b0%e6%8d%ae%e5%88%86%e6%9e%90)** 中讨论过的,这些远端查询通常会比较慢而且有打垮数据库的风险。 为了进行该 join在处理时需要每次从流中取一个活动事件然后依据事件中的用户 ID 在数据库中查找相关信息,最后将找到的用户资料信息附加到事件中。数据库的查找可能**发生在远端**;然而,就像我们在[例子:用户行为数据分析](https://ddia.qtmuniao.com/#/ch10?id=%e4%be%8b%e5%ad%90%ef%bc%9a%e7%94%a8%e6%88%b7%e8%a1%8c%e4%b8%ba%e6%95%b0%e6%8d%ae%e5%88%86%e6%9e%90)中讨论过的,这些远端查询通常会比较慢而且有打垮数据库的风险。
另外一种方式是将数据库的数据加载一份到各个流处理节点的内存中,则每次 join 直接从本地内存查找即可,省掉了 RPC 往返的网络开销。这种技术很像我们在 **[Map 侧的连接](https://ddia.qtmuniao.com/#/ch10?id=map-%e4%be%a7%e7%9a%84%e8%bf%9e%e6%8e%a5)** 中讨论过的哈希 join如果数据库表足够小可以将其以哈希表的形式缓存在各个节点内存或以索引的形式放在本地磁盘中。 另外一种方式是将数据库的数据加载一份到各个流处理节点的内存中,则每次 join 直接从本地内存查找即可,省掉了 RPC 往返的网络开销。这种技术很像我们在[Map 侧的连接](https://ddia.qtmuniao.com/#/ch10?id=map-%e4%be%a7%e7%9a%84%e8%bf%9e%e6%8e%a5)中讨论过的哈希 join如果数据库表足够小可以将其以哈希表的形式缓存在各个节点内存或以索引的形式放在本地磁盘中。
这种方式和批处理任务的区别在于,批处理在进行 join 时用的是某个时间点的数据库的快照作为输入。但流式处理任务是常驻的,随着时间的推移,数据库中的数据就可能发生变化,因此每个流处理节点的数据库副本需要不断更新。我们可以用 CDC 来解决这个问题:流处理任务去同时订阅数据库中用户信息表的变更日志。当用户资料在数据库中被创建或者更新时,流处理任务就会收到相关事件以更新其本地副本。如此一来,我们**本质上也是在 join 两个流**:活动事件流和资料变更流。 这种方式和批处理任务的区别在于,批处理在进行 join 时用的是某个时间点的数据库的快照作为输入。但流式处理任务是常驻的,随着时间的推移,数据库中的数据就可能发生变化,因此每个流处理节点的数据库副本需要不断更新。我们可以用 CDC 来解决这个问题:流处理任务去同时订阅数据库中用户信息表的变更日志。当用户资料在数据库中被创建或者更新时,流处理任务就会收到相关事件以更新其本地副本。如此一来,我们**本质上也是在 join 两个流**:活动事件流和资料变更流。
@ -584,7 +584,7 @@ CEP 系统常使用偏高层的描述式查询语言,如 SQL 或者图形用
### 表表连接(维持物化视图) ### 表表连接(维持物化视图)
考虑我们在 **[衡量负载](https://ddia.qtmuniao.com/#/ch01?id=%e8%a1%a1%e9%87%8f%e8%b4%9f%e8%bd%bd)** 一节中讨论过的 Twitter 时间线例子。当时我们说,每次用户在看其首页瀑布流时,如果都去所有关注者那拉取一遍最近的推文后进行合并,代价会非常高。 考虑我们在[衡量负载](https://ddia.qtmuniao.com/#/ch01?id=%e8%a1%a1%e9%87%8f%e8%b4%9f%e8%bd%bd)一节中讨论过的 Twitter 时间线例子。当时我们说,每次用户在看其首页瀑布流时,如果都去所有关注者那拉取一遍最近的推文后进行合并,代价会非常高。
相反,我们想要对该时间线进行缓存:**某种程度上类似于用户粒度的“信箱”,即以用户为单位保存其所有关注者发的推文**。基于此缓存,为每个用户生成时间线时,只需要从“信箱”中拉取最近的推文即可。对该缓存进行物化和维护需要进行以下处理: 相反,我们想要对该时间线进行缓存:**某种程度上类似于用户粒度的“信箱”,即以用户为单位保存其所有关注者发的推文**。基于此缓存,为每个用户生成时间线时,只需要从“信箱”中拉取最近的推文即可。对该缓存进行物化和维护需要进行以下处理:
@ -722,4 +722,4 @@ Apache Flink 使用的是一种变种容错方式——周期性地将状态做
输入流两侧都是数据库的变更日志。在这种情况下,每一侧的到来的变更都要和另一侧的最新状态来 Join。其结果其实是两表 Join 的物化视图的变更流。 输入流两侧都是数据库的变更日志。在这种情况下,每一侧的到来的变更都要和另一侧的最新状态来 Join。其结果其实是两表 Join 的物化视图的变更流。
最后,我们讨论了在流处理器中进行容错和实现严格一次语义的一些技术。对于批处理来说,我们可以简单的抛掉故障任务的中间输出。但流处理是长时间运行的、并持续产生输出的。因此我们不能将所有历史输出都丢掉,然后重算。相反,我们需要引入一些细粒度的恢复机制,可以基于微批、检查点、事务或者幂等更新来实现。 最后,我们讨论了在流处理器中进行容错和实现严格一次语义的一些技术。对于批处理来说,我们可以简单的抛掉故障任务的中间输出。但流处理是长时间运行的、并持续产生输出的。因此我们不能将所有历史输出都丢掉,然后重算。相反,我们需要引入一些细粒度的恢复机制,可以基于微批、检查点、事务或者幂等更新来实现。