本文是《数据密集型应用系统设计》(DDIA)的读书笔记,一共十二章,我已经全部阅读并且整理完毕。
采用一问一答的形式,并且用列表形式整理了原文。 笔记的内容大概是原文的 1/5 ~
1/3,所以你如果没有很多时间看书的话,看我的笔记也就够了!
上一章的批处理技术讨论了什么?
批处理技术的问题?
什么是“流”?
流处理的输入输出的等价物看上去是什么样子?
流处理的数据消费方式?
流处理的数据怎么存储?
什么是消息传递系统?
怎么区分不同的消息传递系统?
是否可以接受消息丢失?
很多消息传递系统使用生产者消费者之间的直接网络通信,而不通过中间节点:
怎么实现可靠性?
什么是消息代理?
消息队列与数据库的差异:
多个消费者从同一主题消费消息时,两种不同的消息传递模式:
负载均衡(load balancing)
扇出(fan-out)
两种模式可以组合使用:两个独立的消费者组订阅同一个主题,可以一组选择负载均衡,另一组选择扇出。
为什么需要确认?
如果代理没有收到确认怎么办?
怎么保证消息顺序?
数据会永久保存吗?
怎么用日志实现消息代理?
当吞吐量超过单个磁盘,怎么办?
哪些消息队列产品基于日志?
基于日志的消息队列为什么适合扇出?
怎么选择消息队列?
顺序消费分区内消息的优点?
与单领导者数据库复制中常见的日志序列号很类似:
当消费者节点失效怎么办?
追加日志怎么防止磁盘耗尽?
消费者跟不上生产者发送消息的速度时怎么办?
消费者落后太多怎么办?
为什么能重播旧消息?
数据库与流的关系?
为什么需要保持系统同步?
怎么做到系统同步?
双写存在的问题?
大多数数据库的复制日志问题是什么?
什么是变更数据捕获(change data capture, CDC)?
举个 CDC 的例子:
我们将日志消费者叫做衍生数据系统。衍生数据就是原始数据的准确副本。
变更数据捕获的原理?
常见实现:
同步还是异步?
怎么从一个已有的数据库中使用 CDC 同步数据?
如何避免每添加一个衍生数据系统都要对数据库做一个快照?
日志压缩原理?
怎么使用日志压缩来重建衍生数据系统?
越来越多的数据库把变更流作为第一等的接口。
我们讨论的想法类似于事件溯源(Event Sourcing) 之间有一些相似之处,这是一个在 领域驱动设计(domain-driven design, DDD) 社区中折腾出来的技术。
什么是事件溯源?
为什么将用户的行为记录为不可变的事件?
事件溯源属于什么模型?
实际中的数据溯源?
用户关心事件日志吗?
事件日志和状态怎么转化?
日志压缩重构系统当前状态的方法?
什么是事件和命令?
对事件怎么操作?
状态和数据库?
状态和事件的关系?
用数学表达应用当前状态与事件流之间的关系:
s t a t e ( n o w ) = ∫ t = 0 n o w s t r e a m ( t ) ? d t s t r e a m ( t ) = d ? s t a t e ( t ) d t state(now) = \int_{t=0}^{now}{stream(t) \ dt} \\ stream(t) = \frac{d\ state(t)}{dt} state(now)=∫t=0now?stream(t)?dtstream(t)=dtd?state(t)?
日志和数据库的关系?
日志压缩是连接日志与数据库状态之间的桥梁:它只保留每条记录的最新版本,并丢弃被覆盖的版本。
不可变事件有什么优点?
不变的事件日志中分离出可变状态的优点?
存储数据很麻烦吗?
数据库和设计模式基于一种谬论:
针对读取优化的例子?
事件溯源和变更数据捕获的最大缺点是什么?
如何解决异步与并发问题?
不使用事件溯源模型的系统可以依赖不变性吗?
永远保持所有变更的不变历史,在多大程度上是可行的?
需要彻底删除的日志该怎么办?
真正删除数据是非常非常困难的:
我们已经讨论了
流能做什么?
流处理与批处理的关系?
长期以来,流处理一直用于监控目的,如果某个事件发生,组织希望能得到警报。例如:
新时代,流处理有了新用途。
什么是复合事件处理?
CEP 与普通数据库的区别?
CEP 的常见实现?
CEP与流分析之间的边界是模糊的,但一般来说,分析往往对找出特定事件序列并不关心,而更关注大量事件上的聚合与统计指标 —— 例如:
这种统计值聚合的时间间隔称为窗口(window)
流分析系统为什么使用概率算法?
许多开源分布式流处理框架的设计都是针对分析设计的:
什么是物化视图?
流系统怎么维护物化视图?
哪些系统支持?
为什么需要在流上搜索?
消息传递系统可以作为RPC的替代方案,即作为一种服务间通信的机制,比如在Actor模型中所使用的那样。
尽管这些系统也是基于消息和事件,但我们通常不会将其视作流处理组件:
也可以使用Actor框架来处理流。但是,很多这样的框架在崩溃时不能保证消息的传递,除非你实现了额外的重试逻辑,否则这种处理不是容错的。
关于时间戳:
处理延迟带来的问题?
用事件时间定义窗口的问题?
对滞留事件的两种选择:
可以用消息表示时间戳已经完成:
事件的时间戳怎么来的?
如何校正不准确的时钟?
要校正不正确的设备时钟,一种方法是记录三个时间戳:
当忽略掉网络的传输时间时,可以用时间戳三和二估算设备与服务器的时钟偏移。
确定了事件的时间戳后,下面就是定义时间段的窗口。
滚动窗口(Tumbling Window)
滚动窗口有着固定的长度,每个事件都仅能属于一个窗口。例如,假设你有一个1分钟的滚动窗口,则所有时间戳在10:03:00
和10:03:59
之间的事件会被分组到一个窗口中,10:04:00
和10:04:59
之间的事件被分组到下一个窗口,依此类推。通过将每个事件时间戳四舍五入至最近的分钟来确定它所属的窗口,可以实现1分钟的滚动窗口。
跳动窗口(Hopping Window)
跳动窗口也有着固定的长度,但允许窗口重叠以提供一些平滑。例如,一个带有1分钟跳跃步长的5分钟窗口将包含`10:03:00`至`10:07:59`之间的事件,而下一个窗口将覆盖`10:04:00`至`10:08:59`之间的事件,等等。通过首先计算1分钟的滚动窗口(tunmbling window),然后在几个相邻窗口上进行聚合,可以实现这种跳动窗口。
滑动窗口(Sliding Window)
滑动窗口包含了彼此间距在特定时长内的所有事件。例如,一个5分钟的滑动窗口应当覆盖`10:03:39`和`10:08:12`的事件,因为它们相距不超过5分钟(注意滚动窗口与步长5分钟的跳动窗口可能不会把这两个事件分组到同一个窗口中,因为它们使用固定的边界)。通过维护一个按时间排序的事件缓冲区,并不断从窗口中移除过期的旧事件,可以实现滑动窗口。
会话窗口(Session window)
与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果30分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(请参阅“[分组](ch10.md#%E5%88%86%E7%BB%84)”)。
流连接比批处理的连接更有挑战性。
有三种:流-流连接,流-表连接,与表-表连接
举例:在搜索-点击场景中,需要根据 id关联两者的数据。
为了实现这种类型的连接,流处理器需要维护状态:
举例:用户事件和用户档案数据库相关联。被称为 扩充(enriching) 活动事件。
做法:
流表连接类似于流流连接。区别是表的变更日志流相当于一个无限的窗口。
举例:维护推特时间线。
我们需要一个时间线缓存:一种每个用户的“收件箱”,在发送推文的时候写入这些信息,因而读取时间线时只需要简单地查询即可。
要在流处理器中实现这种缓存维护,你需要推文事件流(发送与删除)和关注关系事件流(关注与取消关注)。流处理需要维护一个数据库,包含每个用户的粉丝集合。以便知道当一条新推文到达时,需要更新哪些时间线。
流处理过程的另一种视角是:它维护了一个连接了两个表(推文与关注)的物化视图:
SELECT follows.follower_id AS timeline_id,
array_agg(tweets.* ORDER BY tweets.timestamp DESC)
FROM tweets
JOIN follows ON follows.followee_id = tweets.sender_id
GROUP BY follows.follower_id
流连接直接对应于这个查询中的表连接。时间线实际上是这个查询结果的缓存,每当底层的表发生变化时都会更新。
这里描述的三种连接(流流,流表,表表)有很多共通之处:它们都需要流处理器维护连接一侧的一些状态(搜索与点击事件,用户档案,关注列表),然后当连接另一侧的消息到达时查询该状态。
连接时的结果与事件顺序的关系?
怎么解决?
批处理怎么做容错?
什么是恰好处理一次?
流处理怎么容错呢?
什么是微批次?
什么是存档点?
上述功能实现了恰好一次吗?
如何保证出现故障时表现出恰好处理一次的样子?
怎么实现?
我们的目标是丢弃任何失败任务的部分输出,以便能安全地重试,而不会生效两次。
什么是幂等?
天生不幂等的操作,可以实现幂等吗?
流处理中幂等的实现?
使用幂等性的前提?
为什么需要失败后重建状态?
怎么实现?
常见实现?
一定要复制状态么?
比较两种消息代理:
AMQP/JMS风格的消息代理
代理将单条消息分配给消费者,消费者在成功处理单条消息后确认消息。消息被确认后从代理中删除。这种方法适合作为一种异步形式的RPC(另请参阅“[消息传递中的数据流](ch4.md#%E6%B6%88%E6%81%AF%E4%BC%A0%E9%80%92%E4%B8%AD%E7%9A%84%E6%95%B0%E6%8D%AE%E6%B5%81)”),例如在任务队列中,消息处理的确切顺序并不重要,而且消息在处理完之后,不需要回头重新读取旧消息。
基于日志的消息代理
代理将一个分区中的所有消息分配给同一个消费者节点,并始终以相同的顺序传递消息。并行是通过分区实现的,消费者通过存档最近处理消息的偏移量来跟踪工作进度。消息代理将消息保留在磁盘上,因此如有必要的话,可以回跳并重新读取旧消息。
变更数据捕获
就流的来源而言,我们讨论了几种可能性:用户活动事件,定期读数的传感器,和Feed数据(例如,金融中的市场数据)能够自然地表示为流。我们发现将数据库写入视作流也是很有用的:我们可以捕获变更日志 —— 即对数据库所做的所有变更的历史记录 —— 隐式地通过变更数据捕获,或显式地通过事件溯源。日志压缩允许流也能保有数据库内容的完整副本。
我们区分了流处理中可能出现的三种连接类型:
流流连接
两个输入流都由活动事件组成,而连接算子在某个时间窗口内搜索相关的事件。例如,它可能会将同一个用户30分钟内进行的两个活动联系在一起。如果你想要找出一个流内的相关事件,连接的两侧输入可能实际上都是同一个流(自连接(self-join))。
流表连接
一个输入流由活动事件组成,另一个输入流是数据库变更日志。变更日志保证了数据库的本地副本是最新的。对于每个活动事件,连接算子将查询数据库,并输出一个扩展的活动事件。
表表连接
两个输入流都是数据库变更日志。在这种情况下,一侧的每一个变化都与另一侧的最新状态相连接。结果是两表连接所得物化视图的变更流。
最后,我们讨论了在流处理中实现容错和恰好一次语义的技术。与批处理一样,我们需要放弃任何失败任务的部分输出。然而由于流处理长时间运行并持续产生输出,所以不能简单地丢弃所有的输出。相反,可以使用更细粒度的恢复机制,基于微批次、存档点、事务或幂等写入。