一个可用的复杂系统总是从可用的简单系统演进而来。反过来这话也是正确的:从零开始设计的复杂系统从来都用不了,也没办法把它变成可用。
——John Gal,《系统学》(1975)
批处理的输入数据是有界的,而流处理输入是无界的。
发送事件流
消息系统
事件由生产者(也被称为发布者或发送者)生成一次,然后可能由多个消费者(订阅者或接收者)处理,在流系统中,相关的事件通常被组合成主题或流。
向消费者通知新事件的常见方法是使用消息系统,消息系统允许多个生产者节点将消息发送到同一主题,并允许多个消费者节点接收主题中的消息。
消息代理(消息队列)实质上是一种针对处理消息流而优化的数据库。
消息代理与数据库对比
- 数据库通常会保留数据直到被明确要求删除,而大多数消息代理在消息成功传递给消费者时就自动删除消息。
- 数据库通常支持二级索引和各种搜索数据的方式,而消息代理通常支持某种方式订阅匹配特定模式的主题。
- 查询数据库时,结果通常基于数据的时间点快照。数据库数据发生变化时,一般情况下客户端不会被通知。而消息代理会把变化的数据通知给客户端。
有两种主要的消息传递模式,分别是负载均衡式和扇出式。
分区日志
Apache Kafka、Amazon Kinesis Streams和Twitter DistributedLog都是基于日志的消息代理系统,通过在多台机器上进行分区,能够实现每秒数百万条消息的吞吐量,并且通过复制消息实现了容错性。基于日志的方法很自然地支持扇出式消息传递。此时消息代理的行为就像一个主节点数据库,消费者就像一个从节点。
在消息处理的代价很高,希望在逐个消息的基础上并行处理,而且消息排序又不那么重要的情况下,传统类型的消息代理更可取(JMS/AMQP)。在消息吞吐量高的情况下,每个消息处理速度快,消息顺序又很重要的情况下,基于日志的方法工作得很好。
基于日志的消息系统更像批处理过程,派生数据通过可重复的转换过程与输入数据明确分离。它支持更多实验性尝试,也更容易从错误和故障中进行恢复。
数据库与流(流系统给数据库的启发)
保持系统同步
由于相同或相关的数据出现在多个不同的地方,因此他们需要保持相互同步。如果定期地进行完整数据库转储过于缓慢,有时使用的替代方法是双重写入。但在异构系统中双重写入可能会产生写入请求乱序问题和部分失败问题,前者是并发导致的,后者是容错问题,从而多个系统数据可能会有不一致的风险。
变更数据捕获
变更数据捕获(Change Data Capture,CDC)记录了写入数据库的所有更改,并以可复制到其他系统的形式来提取数据。从本质上说,变更数据捕获使得一个数据库成为主节点,并将其它系统变成从节点。由于基于日志的消息代理保留了消息的排序,因此它非常适合从源数据库传输更改事件。
事件溯源
事件溯源是一种强大的数据建模技术:从应用程序的角度看,将用户的行为记录为不可变的事件更有意义,而不是记录这些行为对可变数据库的影响。
事件溯源的哲学是小心地区分事件和命令。当来自用户的请求第一次到达时,它最初是一个命令,应用程序必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受,它将变成一个持久且不可变的事件。
状态,流与不可变性
应用状态是事件流对时间的积分得到的,而变化流是状态对时间的求导得到的。通过不可变事件的追加日志,诊断问题和从问题中恢复就要容易得多。另外,通过从不变事件日志中分离可变状态,可以从相同的事件日志派生出多个面向读取的表示方式。
如果不必担心如何查询数据,那么存储数据通常是非常简单的。模式设计、索引和存储引擎的许多复杂性多是源于希望支持某些查询和访问模式。因此,将数据写入形式与读取形式分开,并允许多个不同的读取视图,可以获得很大的灵活性。这个想法有时被称为命令查询责任分离(Command Query Responsibility Segregation,CQRS)。
数据库和模式设计的传统方法是基于数据查询必须与数据写入形式相同这一谬误。如果可以将数据从写优化的事件日志转换为读优化的应用程序状态,有关规范化和非规范化的争论则会变得无关紧要:由于转换过程提供了响应机制使其与源事件日志保持一致,因此在读优化的视图中对数据进行反规范化处理是完全合理的。
流处理
流处理的适用场景
复杂事件处理
复杂事件处理(Complex Event Processing,CEP)是为了分析事件流而发展的一种方法,适用需要搜索特定的事件模式。类似正则表达式匹配字符串,CEP允许指定规则,从而可以在流中搜索特定模式的事件。
此时查询和数据之间的关系与普通数据库相比正好相反。数据库会持久存储数据并将查询视为暂时的:当查询到来时,数据库搜索与查询匹配的数据,然后在查询完成时忘记它。CEP引擎反转了这些角色:查询是长期存储的,来自输入流的事件不断流过他们以匹配事件模式。
流分析
流分析往往不太关心找到特定的事件序列,更多面向大量事件的累计效果和统计指标。
维护物化视图
对某个数据集导出一个特定的视图以便高效查询,并在底层数据更改时自动更新该导出视图。
在流上搜索
有时需要基于一些复杂事件(例如全文搜索查询)来搜索单个事件。例如房地产网站的用户需要当市场上出现符合其搜索条件的新房产时被及时通知,Elasticsearch的过滤器功能是实现这种流式搜索的一种方式。
流的时间问题
移动设备的本地时钟一般不可信,为了调整不正确的设备时钟,一种方法是记录三个时间戳。
确定了事件的时间戳之后,下一步是定义窗口(时间区间),窗口可用于聚合分析。有几种常见的窗口类型:
- 轮转窗口,轮转窗口的长度是固定的,每个事件都属于一个窗口。
- 跳跃窗口,跳跃窗口也具有固定长度,但允许窗口重叠以提供一些平滑过渡。
- 滑动窗口,滑动窗口是一个长度固定的时间区间,并跟随时间将过期的事件移除。
- 会话窗口,通过将同一用户在时间上紧密相关的所有事件分组在一起而定义的。
流式join
三种不同类型的join:
- 流和流join(窗口join),两个输入流都由活动事件组成,采用join操作来搜索在特定时间窗口内发生的相关事件。
- 流和表join,可以将数据库副本加载到流处理器中,以便进行本地查询。同时此本地副本可以通过变更数据捕获手段来更新。
- 表和表join(物化视图维护),参考twitter用户时间线的例子。
在流和表的join中,如果join表的状态信息随时间而改变,那么join变成了非确定性的,这个问题被称为缓慢变化的维度(Slowly Changing Dimension,SCD)。通常通过对特定版本的join记录赋予唯一的标识符来解决,这种方式使join操作具有确定性。
流处理的容错
- 一种解决方案是将流分解成多个小块,并像小型批处理一样处理每个块,这种方法称为微批处理,它已经用于Spark Streaming。另一个变体方法是定期生成状态滚动检查点,Apache Flink在使用此方法。但此两种方法不能防范流系统之外的副作用。
- 另外一种是分布式事务或原子提交
- 使操作变成拥有幂等性,即多次执行,与只执行一次具有相同的效果。
参考文献:
[1] (美)Martin Kleppmann. 数据密集型应用系统设计(赵军平,吕云松,耿煜,李三平 译)[M]. 北京:中国电力出版社,2018.