有效的复杂系统总是从简单的系统演化而来。反之亦然:从零设计的复杂系统没一个能有效工作的。
—— 约翰·加尔
现实生活中,很多数据是无界限的,因为它随着时间的推移而逐渐变化。用户在昨天和今天产生了数据,明天他们还将继续产生数据。除非停业,否则这个过程永远不会结束,所以数据集从来就不会以任何有意义的方式「完成」。因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束的时候处理一小时的数据。
日常批处理中的问题是,输入的变更只会在一天之后的输出中反映出来,这对许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理——比如说,在每秒钟的末尾——或者甚至更连续一下,完全抛开固定的时间切片,当事情发生时就立刻进行处理,这就是流处理背后的想法。
一般来说,「流」是指随着时间的推移逐渐可用的数据。事件流是一种数据管理机制:无界限、增量处理。本节主要讨论
- 表示、存储、通过网络传输流——传递事件流
- 研究流与数据库之间的关系——数据库与流
- 研究连续处理这些流的方法和工具,以及它们用于应用构建的方式——流处理
1. 传递事件流
原则上讲,文件和数据库就足以连接生产者和消费者:生产者将其生成的每件时间写入数据库存储,且每个消费者定期轮询数据存储,检查自上次运行依赖新出现的事件。这实际上正是批处理在每天结束时处理当天数据时所做的事情。
当我们想要进行低延迟的连续处理时,如果数据存储不是为这种用途专门设计的,那么轮询开销就会很大。轮询的越频繁,能返回新事件的请求比例就越低,而额外开销也越高。相比之下,最好能在新事件出现时直接通知消费者。
1.1 消息传递系统
向消费者通知新事件的常用方式是使用消息传递系统:生产者发送包含事件的消息,然后将消息推送给消费者。
思考以下两个问题:
- 如果生产者发送消息的速度比消费者能够处理的速度快一般有三种选择?
- 丢掉消息
- 将消息放入缓冲区队列
- 使用背压(ps 如果填满,发送者会被阻塞)
- 如果节点崩溃或暂时脱机,是否会有消息丢失?
- 与数据库一样,持久性可能需要写入磁盘或复制的某种组合,这是有代价的。
1.1.1 直接从生产者传递给消费者
许多消息传递系统使用生产者和消费者之间的直接网络通信,而不通过中间节点:
- UDP 组播广泛应用于金融行业,例如股票市场,其中低时延非常重要。虽然 UDP 本身是不可靠的,但应用层的协议可以恢复丢失的数据包。
- 无代理的消息库,如 ZeroMQ 和 nanomsg 采取类似的方法,通过 TCP 或 IP 多播实现发布/订阅消息传递。
- StatsD 和 Brubeck 使用不可靠的 UDP 消息传递来收集网络中所有机器的指标并对其进行监控。
- 如果消费者在网络上公开服务,生产者可以直接发送 HTTP 或 RPC 请求将消息推送给使用者。
1.1.2 消息代理
消息代理实质上是一种针对消息流而优化的数据库。它作为服务器运行,生产者和消费者作为客户端连接到服务器。生产者将消息写入代理,消费者通过代理那里读取来接收消息。
注:
将数据集中在代理上,则持久性问题则转移到代理的身上。
在数据集代理上排队的结果是,消费者通常是异步的,向消费者传递消息将发生在未来某个未定的时间点。
1.1.3 消息代理与数据库对比
消息代理和数据库之间仍存在实践上的差异:
-
数据库通常保留数据直至显式删除,而大多数消息代理在消息成功发送给消费者时会自动删除消息。
-
由于它们很快就能删除消息,大多数消息代理都认为它们的工作集相当小——即队列很短。
-
数据库通常支持二级索引和各种搜索数据的方式,而消息代理通常支持按照某种模式匹配 topic,并进行订阅。
-
查询数据库时,结果通常基于某个时间点的数据快照
-
消息代理不支持任意查询,但当数据发生变化时,它们会通知客户端。
-
如果另一个客户端随后向数据库写入一些改变查询结果的内容,则第一个客户端不会发现其先前结果已经过期
-
1.1.4 多个消费者
当多个消费者从同一主题读取消息时,有两种主要的消息传递模式:
- 负载均衡:每条消息都被传递给消费者之一,处理该主题下消息的工作能被多个消费者共享。
- 扇出:每条消息都被传递给所有消费者。
1.1.5 确认与重现传递
消息代理使用确认:客户端必须显示告知代理消息处理完毕的时间,以便能将消息从队列中移除。
当负载均衡与重传消息互相结合时,必然会导致消息被重新排序。
1.2 分区日志
数据库和消息系统使用不同的处置策略:
- 数据库的数据在被显示删除之前,所有写入数据库的内容都要被永久记录下来。
- 消息系统收消息具有破坏性,因为确认可能导致消息被删除,以及新加入的消费者只能接收到注册之后开始发送的消息
能否将数据库的持久存储方式和消息传递的低延迟通知相结合——基于日志的消息代理
1.2.1 使用日志进行消息存储
日志支持磁盘上简单的仅追加记录序列。将日志的模式用于消息代理——生产者将消息追加到日志末尾来发送消息,而消费者通过依次读取来接收消息。
注: unix 的 tail -f 的基本原理就是如此。
Apache Kafka,Amazon Kinesis Streams 和 Twitter 的 DistributedLog 都是基于日志的消息代理
1.2.2 日志与传统的消息传递相比
基于日志的方法天然支持扇出式消息传递,因为多个消费者可以独立读取日志,而不会互相影响——读取消息不会将其从日志中删除。
代理可以将整个分区分配给消费者组中的节点,这种粗粒度的负载均衡方法有一些缺点:
- 共享消息主题工作的节点数,最多为该主题中的日志分区数,因为同一个分区内的所有消费被传递到同一个节点。
- 如果某条消息处理缓慢,则会阻塞该分区中后续的消息的处理。
1.2.3 消费者偏移量
顺序消费一个分区使得判断消息是否已经被处理变得相当容易:所有偏移量小于消费者的当前偏移量的消息已经被处理,而具有更大偏移量的消息还没有被看到。因此,代理不需要跟踪确认每条消息,只需要定期记录消费者的偏移即可。这种方法有助于提高系统的吞吐量。
1.2.4 磁盘空间使用
如果只追加写入日志,则磁盘空间终究会耗尽。实际上,日志实现了一个有限大小的缓冲区,当缓冲区填满时会丢弃旧消息,它也被称为循环缓冲区或环形缓冲区。
注:由于缓冲区在磁盘上,因此缓冲区可能相当大。
不论保留多长时间,日志的吞吐量保持不变,因为无论如何,每个消息都会被写入磁盘。这种行为与默认将消息保存在内存中,仅当队列太长时才写入磁盘的消息传递系统形成鲜明对比。当队列很短时,这些系统处理的非常快,而当这些系统开始写入磁盘时,就要慢的多。
1.2.5 当消费者跟不上生产者时
在消息传递系统中,如果消费者无法跟上生产者发送消息的速度时,讨论了三种选择:
- 丢弃消息
- 进行缓冲
- 施加背压
注:在这种分类方法中,基于日志的方法是缓冲的一种形式,具有很大但大小固定的缓冲区。
如果消费者远远落后,而所要求的信息比保留在磁盘上的信息还要旧,那么它将不能读取这些消息,所以代理实际上丢弃了比缓冲区容量更大的旧消息。
1.2.6 重播旧消息
- 使用 AMQP 和 JMS 风格的消息代理,处理和确认消息是一个破坏性的操作,因为它会导致消息在代理上被删除
- 基于日志的消息代理中,使用消息更像是从文件中读取数据:只有读操作,不会更改日志。
在基于日志的消息代理中,变更消费者偏移量的位置,能够起到重播旧消息的目的。
2. 数据库与流
基于日志的消息代理是从数据库中获得灵感并将其应用于消息传递。反过来,基于消息传递和流中获取灵感,并将它们应用于数据库。
事件是某个时刻发生的事情的记录,发生的事情可能是用户操作或读取传感器,也可能是写入数据库。比如:
- 事实上,复制日志是一个油数据库写入事件组成的流,由主库在处理事务时生成。从库将写入流应用到它们自己的数据库副本,从而最终得到相同数据的精确副本。复制日志中的事件描述发生的数据更改。
- 「全序广播」中的状态机复制原理:如果每个事件代表对数据库的写入,并且每个副本按相同的顺序处理相同的事件,则副本将达到相同的最终状态。
2.1 保持系统同步
没有一个系统能够满足所有的数据存储、查询和处理需求。在实践中,大多数应用都需要组合使用几种不同的技术来满足所有的需求:
- 使用 OLTP 数据库来为用户请求提供服务,使用缓存来加速常见请求,使用全文索引来处理搜索查询,使用数据仓库来分析。每一种技术都有自己的副本,并根据自己的目的进行存储方式的优化。
由于相同或相关的数据出现在了不同的地方,因此相互之间需要保持同步。同步的策略分为
- 周期性转储
- 双写,应用代码在数据变更时明确写入每个系统:例如,首先写入数据库,然后更新搜索索引,然后使缓存项失效。
但在双写的情况下,可能出现竞争条件:
- 数据库中 X 首先被设置为 A,然后被设置为 B,而在搜索索引处,写入以相反的顺序到达。
2.2 变更数据捕获
变更数据捕获(change data capture)是一种观察写入数据库的所有数据变更,并将其提取并转换为可以复制到其他系统中的形式的过程。
例如,可以捕获数据库中的变更,并不断将相同的变更应用至搜索索引。如果变更日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中数据是匹配的。搜索索引和任何其他衍生数据系统只是变更流的消费者。
将数据按顺序写入一个数据库,然后按照相同的顺序将这些更改应用到其他系统。
2.2.1 变更数据捕获的实现
可以将日志消费者叫做衍生数据系统。变更数据捕获是一种机制,可确保对记录系统所做的所有更改都反映在衍生数据系统中,以便衍生数据系统具有数据的准确副本。
从本质上说,变更数据捕获使得一个数据库成为领导者,并将其他组件变为追随者。基于日志的消息代理非常适合从源数据库传输变更时间,因为它保留了消息的顺序。
像消息代理一样,变更数据捕获通常是异步的:记录数据库系统不会等待消费者应用变更再进行提交。这种设计具有的运维优势是,添加缓慢的消费者不会过度影响记录系统。
2.2.2 初始快照
如果你拥有所有对数据库进行变更的日志,则可以通过重播该日志,来重建数据库的完整状态。但是在许多情况下,永远保留所有更改会耗费太多磁盘空间,且重播过于费时,因此日志需要被截断。
如果你没有完整的历史日志,则需要从一个一致的快照开始,数据库的快照必须与变更日志中的已知位置或偏移量相对应,以便在处理完快照后知道从哪里开始应用变更。
2.2.3 日志压缩
在「哈希索引」中关于日志结构存储引擎的上下文中讨论了日志压缩,即:存储引擎定期在日志中寻找具有相同键的记录,丢掉所有重复的内容,并只保留每个键的最新更新。这个压缩与合并过程在后台进行。
在基于日志的消息代理与变更数据捕获的上下文中也适用相同的想法。可以从压缩日志 topic 的零偏移量出启动新的消费者,然后一次扫描日志中对的所有消息。
2.2.4 更新流的 API 支持
越来越多的数据库开始将变更流作为第一等的接口 :
- RethinkDB 允许查询订阅通知,当查询结果变更时获得通知。
- Firebase 和 CouchDB 基于变更流进行同步,该变更流同样可用于应用。
- Meteor 和 MongoDB oplog 订阅数据变更,并改变了用户接口。
2.3 事件溯源
与变更数据捕获类似,事件溯源涉及到「将所有对应用状态的变更存储」为变更事件日志。最大的区别是事件溯源将这一想法应用到了一个不同的抽象层次上:
- 在变更数据捕获中,应用以可变方式使用数据库,可以任意更新和删除记录。变更日志是从数据库的底层提取的(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入顺序相匹配。
- 在事件溯源中,应用逻辑显示构建在写入事件日志的不可变事件之上。在这种情况下,事件存储是仅追加写入的,更新与删除是不鼓励的或禁止的。事件被设计为旨在反映应用层发生的事情,而不是底层状态变更。
事件溯源是一种强大的数据建模技术:从应用的角度来看,将用户的行为记录为不可变的事件更有意义,而不是在可变数据库中记录这些行为的影响。事件溯源使得应用随时间演化更为容易,通过更容易理解事件发生的原因来帮助调试的进行。
注:Event Store 这样的专业数据库已经被开发出来,供使用事件溯源的应用使用。
2.3.1 从事件日志中派生出当前状态
使用事件溯源的应用需要拉去事件日志(表示写入系统的数据),并将其转换为适合向用户显示的应用状态(从系统读取数据的方式)。这种转换可以使用任意逻辑,但它应当是确定性的,以便能再次运行。
事件溯源在更高层次进行建模:事件通常表示用户操作的意图,而不是因为操作而发生的状态更新机制。在这种情况下,后面的时间通常不会覆盖先前的事件,所以你需要完整的历史事件来重新构建最终状态。
2.3.2 命令和事件
事件溯源的哲学是仔细区分事件(event)和命令(command)。当来自用户的请求刚到达时,它一开始是一个命令:在这个时间点上它仍然可能失败,比如,因为违反了一些完整性条件。应用必须首先验证它是否可以执行该命令。如果验证成功并且命令被接收,则它变为一个持久化且不可变的事件。
例如,如果用户试图注册特定用户名,或预定飞机或剧院的座位,则应用需要检查用户名或座位是否已被占用。当检查成功时,应用可以生成一个事件,指示特定的用户名是由特定的用户 ID 注册,或者座位已经预留给特定的顾客。
在事件生成的时刻,它就成为了事实。即使客户稍后决定更改或取消预定,他们之前曾经预定某个特定座位的事实仍然成立,而更改或取消是之后添加的单独的事件。
注:此处很像之前做的用户事件中心服务
2.4 状态、流和不变性
批处理因其输入文件不变性而受益良多,你可以在现有输入文件上运行实验性处理作业,而不担心损坏它们。这种不变性原则也是使得事件溯源与变更数据捕获如此强大的原因。
通常将数据库视为应用程序当前状态的存储——这种表示针对读取进行优化。状态的本质是,它会变化,所以数据库才会支持数据的增删改。
可变的状态与不可变事件的仅追加日志相互之间并不矛盾:它们是一体两面,互为阴阳的。所有变化的日志——变化日志,表示了随着时间演变的状态。
正如帕特·赫兰所说的:
事务日志记录了数据库的所有变更。高速追加是更改日志的唯一方法。从这个角度来看,数据库的内容其实是日志中记录最新值的缓存。日志才是真相,数据库是日志子集的缓存,这一缓存子集恰好来自日志中每条记录与索引值的最新值。
2.4.1 不可变事件的优点
不可变的事件包含了比当前状态更多的信息。例如在购物网站上,顾客可以将物品添加到他们的购物车,然后再将其移除。虽然从履行订单的角度,第二个事件取消了第一个事件,但对分析目的而言,知道客户考虑过某些特定项而之后又反悔,可能是很有用的。也许他们会选择在未来购买,或者他们已经找到了替代品。这个信息被记录在事件日志中,但对于移出购物车就删除记录的数据库而言,这个信息在移出购物车时就丢失了。
2.4.2 从同一事件日志中派生多个视图
通过从不变的事件日志中分离出可变的状态,可以针对不同的读取方式,从相同的事件日志中衍生出几种不同的表现形式。效果就像一个流的多个消费者一样。
注:Kafka Connect 能将来自 Kafka 的数据导出到各种不同的数据库与索引。
添加从事件日志到数据库的显示转换,能够使应用更容易地随时间演进:如果想要引入一个新功能,以新的方式表示现有数据,则可以使用事件日志来构建一个单独的、针对新功能的读取优化视图,无需修改现有系统而与之共存。并行运行新旧系统通常比在现有系统中执行复杂的模式迁移更容易。一旦不再需要旧的系统,就可以简单地关闭它并回收其资源。
2.4.3 并发控制
事件溯源和变更数据捕获的最大缺点是——更新延迟。比如:用户会写入日志,然后从日志衍生视图中读取,结果发现他的写入还没有反映在读取视图中。
一种解决方案是将事件追加到日志时同时执行读取视图的更新。而将这些写入操作合并为一个原子单元需要事务,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。
2.4.4 不可变局限性
许多不使用事件溯源模型的系统也还是依赖不可变性:各种数据库在内部使用不可变数据结构或多版本数据来支持时间点快照。
注:Git 等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。
永远保持所有变更的不变历史,在多大程度上是可行的?
答案取决于数据集的流失率。
- 一些工作负载主要添加数据,很少更新或删除,它们很容易保持不变。
- 其他工作负载在相对较小的数据集上有较高的更新/删除率,在这些情况下,不可变的历史可能增至难以接受的巨大,碎片化可能成为一个问题,压缩和垃圾收集表现对于运维的稳健性变得至关重要。
- 也有可能是出于管理方面的原因需要删除数据的情况。
3. 流处理
本章中已经讨论了流的来源包括,用户活动事件、传感器和写入数据库,流的传输直接通过消息传输、通过消息代理、通过事件日志。
接下来将讨论可以用流来做什么,一般来说有三种选项:
- 将事件中的数据写入数据库、缓存、搜索引擎或其他类似的存储系统,然后能被其他客户端查询。这是数据库与系统其他部分所发生变更保持同步的好方法——特别是当流消费者是写入数据库的唯一客户端时。
- 能以某种方式将事件推送给用户,例如发送报警邮件或推送通知,或将事件流式传输到可实时显示的仪表板上。
- 可以处理一个或多个输入流,并产生一个或多个输出流。流可能会经过由几个这样的处理阶段组成的流水线,最后
3.1 流处理的应用
长期以来,流处理一致用于监控目的,如果某个事件发生,组织希望能得到警报。例如:
- 欺诈检测系统需要确定信用卡的使用模式是否意外地变化,如果信用卡可能已被盗刷,则锁卡。
- 交易系统需要检查金融市场的价格变化,并根据指定的规则进行交易
- 制造系统需要监控工厂中机器的状态,如果出现故障,可以快速定位问题
- 军事和情报系统需要跟踪潜在侵略者的活动,并在出现袭击征兆时发出警报
3.1.1 复合事件处理
复合事件处理,适用于需要搜索某些事件模式的应用,与正则表达式允许你在字符串中搜索特定字符模式的方法类似。
复合事件处理通常使用高层次的声明式查询语言,比如 SQL,或者图形用户界面,来描述应该检测到的事件模式。这些查询被提交给处理引擎,该引起消费输入流,并在内部维护一个执行所需匹配的状态机。
查询和数据之间的关系与普通数据库相比是颠倒的。
-
通常情况下,数据库会持久数据,并将查询视为临时的:当查询进入时,数据库搜索与查询匹配的数据,然后在查询完成时丢掉查询。
-
复合事件处理引擎反转了角色:查询是长期存储的,来自输入流的事件不断流过它们,搜索匹配事件模式的查询。
3.1.2 流分析
使用流处理的另一个领域是对流进行分析。分析往往对特定事件序列并不关心,而更关注大量事件上的聚合与统计指标——例如:
- 测量某种类型事件的速率
- 滚动计算一段时间窗口内某个值的平均值
- 将当前的统计值与先前的时间区间的值对比(例如,检查趋势,当指标与上周同比异常偏高或偏低时报警)
注:这些统计通常是在固定时间区间内进行计算的
3.1.3 维护物化视图
在「数据库与流」中看到,数据库的变更流可以用户维护衍生数据系统(如缓存、搜索索引和数据仓库),并使其与源数据库保持一致。可以将这些示例视为维护物化视图的一种场景。
注:物化视图,在某个数据集上衍生出一种替代视图以便高效查询,并在底层数据变更时更新视图。
在事件溯源中,应用程序的状态是通过应用事件日志来维护的,这里的应用程序状态也是一种物化视图。
3.1.4 在流上搜索
处理允许搜索多个事件构成模式的复合事件处理外,有时也存在基于复杂标准来搜索单个事件的需求。
例如,媒体检测服务可以订阅新闻文章 Feed 与来自媒体的博客,搜索任何关于公司、产品或感兴趣的话题的新闻。这是通过预先构建一个搜索查询来完成的,然后不断地将新闻项的流与该查询进行匹配。
传统的搜索引擎首先索引文件,然后在索引上跑查询。相比之下,搜索一个数据流则反了过来:查询被存储下来,文档从查询中流过。
3.1.5 消息传递和RPC
消息传递系统可以作为 RPC 的替代方案,即作为一种服务间通信的机制,比如在 Actor 模型中所使用的那样。但二者本质上是不同的:
- Actor 框架主要管理模块通信的并发和分布式执行的一种机制,而流处理主要是一种数据管理技术。
- Actor 之间的交流往往是短暂的、一对一的;而事件日志则是持久的、多订阅者的。
- Actor 可以以任意方式进行通信(包括循环的请求/响应模式),但流处理通常配置在无环流水线中,其中每个流都是一个特定作业的输出,由良好定义的输入流中派生而来。
注:RPC 类系统与流处理之间有一些交叉领域。
3.2 时间推理
流处理通常需要与时间打交道,尤其是用于分析目的时候,会频繁使用时间窗口,例如「过去五分钟的平均值」。「过去五分钟」的含义看上去似乎是清晰而无歧义的,但不幸的是,这个概念非常棘手。
许多流处理框架使用处理机器上的本地系统时钟(处理时间)来确定窗口。这种方法的有点是简单,如果事件创建于事件处理之间的延迟可以忽略不计,那也是合理的。然而,如果存在任何显著的处理延迟——即,事件处理显著地晚于事件实际发生的时间,这种处理方式就失效了。
3.2.1 事件时间与处理时间
许多原因都可能导致处理延迟:排队,网络故障,性能问题导致消息代理/消息处理器出现争用,流消息者重启,从故障中恢复时重新处理过去的事件,或者在修复代码 BUG 之后。
同时,消息延迟还可能导致无法预测的消息顺序。
将事件事件和处理时间搞混会导致错误的数据。例如,假设有一个流处理器用于测量请求速率(计算每秒请求数)。如果重新部署流处理器,它可能会停止一分钟,并在恢复之后处理积压的事件。如果按处理时间来衡量速率,那么在处理积压日志时,请求速率看上去就像有一个异常的突发尖峰,而实际上请求速率是稳定的。
3.2.2 知道什么时候准备好了
用事件时间来定义窗口的棘手问题是,你永远也无法确定是不是已经收到了特定窗口的所有事件,还是说还有一些事件正在来的路上。
在一段时间没有看到任何新的事件之后,可以超时并宣布一个窗口已经就绪,但仍然可能发生这种情况:某些事情被缓冲在另一台机器上,由于网络中断而延迟。需要能够处理这种窗口宣告完成之后到达的滞留**事件。大体上,有两种选择:
- 忽略这些滞留事件,因为在正常情况下它们可能只是事件中的一小部分。可以将丢弃事件的数量作为一个监控指标,并在出现大量丢消息的情况时报警
- 发布一个更正,一个包括滞留事件的更新窗口值。可能还需要收回以前的输出
3.2.3 你用的是谁的时钟
当事件可能在系统内多个地方进行缓冲时,为事件分配事件戳更加困难。
这种情况下,事件上的时间戳实际上应当是用户交互发生的时间,取决于移动设备的本地时钟。然而用户控制的设备上的时钟通常是不可信的,因为它可能会被无意或故意设置成错误的时间。服务器收到事件的时间可能更准确的,因为服务器在你的控制之下,但在描述用户交互方面意义不大。
要校正不确定的设备时钟,一种方法是记录三个时间戳:
- 事件发生的时间,取决于设备时钟
- 事件发送服务器的时间,取决于设备时钟
- 事件被服务器接收的时间,取决于服务器时钟
通过从第三个时间戳减去第二个时间戳,可以估算设备时钟和服务器时钟之间的偏移(ps 其实我也没有理解清楚……)
3.2.4 窗口的类型
知道如何确定一个事件的时间戳后,下一步就是如何定义时间段的窗口。几种常见的窗口:
- 滚动窗口:滚动窗口有着固定的长度,每个事件都仅属于一个窗口。
- 跳动窗口:跳动窗口也有这固定的长度,但允许窗口重叠以提供一些平滑。
- 滑动窗口:滑动窗口包含了彼此间距在特定时长内的所有事件。
- 会话窗口:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时窗口结束。会话切分是网络分析的常见需求。
3.3 流连接
3.3.1 流流连接(窗口连接)
假设网站上有搜索功能,你想要找出搜索 URL 的近期趋势。每当有人键入搜索查询使,都会记录下一个包含查询与其返回结果的事件。每当有人点击其中一个搜索结果时,就会记录另一个点击事件。为了计算搜索结果中每个 URL 的点击率,需要将搜索动作与点击动作连在一起,这些事件通过相同的会话 ID 进行连接。
注:广告系统中需要类似的分析。
为了实现这种类型的连接,流处理器需要维护状态:例如,按会话 ID 索引最近一个小时内发生的所有事件。无论何时发生搜索事件或点击事件,都会被添加到合适的索引中,而流处理器也会检查另一个索引是否具有相同会话 ID 的事件达到。如果有匹配事件就会发出一个表示搜索结果被点击的事件;如果搜索事件直到过期都没看见有匹配的点击事件,就会发出一个表示搜索结果未被点击的事件。
3.3.2 流表连接(流扩充)
批处理中流表连接的示例:一组用户活动事件和一个用户档案数据库。将用户活动事件视为流,并在流处理中连续执行相同的连接是很自然的想法:输入是包含用户 ID 的活动事件流,而输出还是活动事件流,但其中用户 ID 已经被扩展为用户的档案信息。这个过程有时被称为使用数据库的信息来扩充活动事件。
与批处理作业的区别在于,批处理作业使用数据库的时间点快照作为输入,而流处理时长时间运行的,且数据库的内容可能随时间而改变,所以流处理器数据库的本地副本需要保持更新。
这个问题可以通过变更数据捕获来解决:流处理器可以订阅用户档案数据库的更新日志,如果活动事件流一样。当添加或修改档案时,流处理器会更新其本地副本。因此,有了两个流之间的连接:活动事件和档案更新。
3.3.3 表表连接(维护物化视图)
在「描述负载」中讨论的推特时间线例子时说过,当用户想要查看他们的主页时间线时,迭代用户所关注人群的推文并合并它们是一个开销巨大的操作。
相反,需要一个时间线缓存:一种每个用户的「收件箱」,在发送推文的时候写入这些消息,因而读取时间线只需要简单地查询即可。
3.3.4 连接的时间依赖性
上述描述的三种连接有很多共通之处:它们都需要流处理器维护连接一侧的一些状态,然后当连接另一侧的消息到达时查询该状态。
用于维护状态的事件顺序是很重要的(比如先关注然后取消关注,或者其他类似操作)。在分区日志中,单个分区内的事件顺序是保留下来的。但典型情况下是没有跨流或跨分区的顺序保证的。
如果跨越流的事件顺序是未定的,则连接会变为不确定性的,这意味着你在同样的输入上重跑相同的作业未必会得到相同的结果:当你重跑任务时,输入流上的事件可能会以不同的方式交织。
在数据仓库中,这个问题被称为缓慢变化的维度,通常通过对特定版本的记录使用唯一的标识符来解决:例如,每当税率改变时都会获得一个新的标识符,而发票在销售时会带有税率的标识符。这种变化是连接变为确定性的,但也会导致日志压缩无法进行:表中所有的记录版本都需要保留。
3.4 容错
在流处理中也需要处理容错问题,但是处理起来比较棘手,因为你永远无法处理完一个无限的流。
3.4.1 微批量和存档点
一个解决方案是将流分解成小块,并像微批处理一样处理每个块。这种方法被称为微批次,它被用于 Spark Streaming。
Apache Flink 则使用不同方法,它会定期生成状态的滚动存档点并将其写入持久存储。如果流算子崩溃,它可以从最近的存档点重启,并丢弃从最近检查点到崩溃之间的所有输出。
3.4.2 原子提交再现
为了在出现故障时表现出恰好处理一次的样子,我们需要确保事件处理的所有输出和副作用当且仅当处理成功时才会生效。这些影响包括发送给下游算子或外部消息传递系统的任何消息,任何数据库写入,对算子状态的任何变更,以及对输入消息的任何确认。
这些事情要么都原子地发生,要么都不发生,但是它们不应当失去同步。
3.4.3 幂等性
目标是丢弃失败任务的部分输出,以便能安全重试,而不会生效两次。分布式事务实现这个目标的一种方式,而另一种方式是依赖幂等性。
幂等操作是多次重复执行与单次执行效果相同的操作。例如,将键值存储中的某个键设置为某个特定值是幂等的,而递增一个计数器不是幂等的。
即使一个操作不是天上幂等的,往往可以通过一些额外的元数据做成幂等的。例如,在使用来自 Kafka 的消息时,每条消息都有一个持久化的、单调递增的偏移量。将值写入外部数据库时可以将这个偏移量带上,这样你就可以判断一条更新是不是已经执行过了,因而避免重复执行。
3.4.4 失败后重建状态
任何需要状态的流处理——例如,任何窗口聚合(例如计数器、平均值和直方图)以及任何用于连接的表和索引,都必须确保在失败之后能恢复其状态。
一种选择是将状态保持在远程数据存储中,并进行复制,然而正如在「流表连接」中所述,每个消息都要查询远程数据库可能会很慢。另一种方法是在流处理器本地保存状态,并定期复制。然后当流处理器从故障中恢复时,新任务可以读取状态副本,恢复处理而不丢失数据。
意思所有的这些权衡取决于底层基础架构的性能特征:在某些系统中,网络延迟可能低于磁盘访问延迟,网络带宽也可能与磁盘带宽相当。没有针对多有情况的普适理想权衡,随着存储和网络技术的发展,本地状态与远程状态的优点也可能会互换。
4. 碎碎念
就这么默默的快把这本书所有的章节都摘录完成了,竟然有了一种怅然若失的感觉。所有的摘录都是自己手打出来的,固执的觉得这种方式会让自己记忆的更加深刻。(ps 希望量能引起质变吧。
-
生命实在太短暂,所以一定要活得正面积极
-
如果觉得身边的一切都太不如意,那就去见喜欢的人,做喜欢的事,买喜欢的东西。
-
晚上坐出租车回家
没事和师傅闲聊
随口问了句师傅什么是生活啊
师傅缓缓说了句
我以前不喜欢开车的