Kafka权威指南 读书笔记之(十一)流式处理
Kafka 一般被认为是一个强大的消息总线,可以传递事件流,但没有处理和转换事件的能力。 Kafka 可靠的传递能力让它成为流式处理系统完美的数据来源。很多基于 Kafka 构
建的流式处理系统都将 Kafka 作为唯一可靠的数据来源,如 Apache Storm 、 Apache Spark Streaming 、 Apache Flink 、 Apache Samza 等。
解释什么是流式处理(因为这个术语经常被人误解);
然后讨论流式处理的一些基本概念和流式处理系统常用的设计模式;
然后深入介绍 Kafka 的流式处理类库,包括它的目标和架构,接着将会给出一个示例,介绍如何使用 Kafka Streams (以下简称 Streams )来计算股价移动平均数;
最后讨论梳式处理的其他使用场景,并在结束部分列出为 Kafka 选择流式处理框架(如果有的话)的参考标准。
什么是流式处理
先来看看什么是数据流(也被称为“事件流”或“流数据”):
数据流是无边界数据集的抽象表示。
无边界意味着无限和持续增长。无边界数据集之所以是无限的,是因为随着时间的推移,新的记录会不断加入进来。
事件流是有序的
事件的发生总是有个先后顺序。以金融活动事件为例,先将钱存进账户后再花钱,这与先花钱再还钱的次序是完全不一样的。后者会出现透支,而前者不会。这是事件流与数
据库表的不同点之一,数据库表里的记录是无序的。
不可变的数据记录
事件一旦发生,就不能被改变。 一个金融交易被取消,并不是说它就消失了。
事件流是可重播的
这是事件流非常有价值的一个属性。用户可以很容易地找出那些不可重播的流(流经套接字的 TCP 数据包就是不可重播的),但对于大多数业务来说,重播发生在几个月
前(甚至几年前)的原始事件流是一个很重要的需求。可能是为了尝试使用新的分析方法纠正过去的错误,或是为了进行审计。这也就是为什么我们相信 Kafka 能够让现代业
务领域的流式处理大获成功一一可以借助 Kafka 来捕捉和重播事件流。如果没有这项能力,流式处理充其量只是数据科学实验室里的一个玩具而已。
如果事件流的定义里没有提到事件所包含的数据和每秒钟的事件数量,那么它就变得毫无意义。
流式处理是指实时地处理一个或多个事件流。流式处理是一种编程范式,就像请求与响应范式和批处理范式那样。
请求与响应
这是延迟最小的一种范式,响应时间处于亚毫秒到毫秒之间,而且响应时间一般非常稳定。这种处理模式一般是阻塞的,应用程序向处理系统发出请求,然后等待响应。在数
据库领域,这种范式就是线上交易处理 ( OLTP)。销售点( POS )系统、信用卡处理系统和基于时间的追踪系统一般都使用这种范式。
批处理
这种范式具有高延迟和高吞吐量的特点。处理系统按照设定的时间启动处理进程,比如每天的下午两点开始启动,每小时启动一次等。它读取所有的输入数据(从上一次执行
之后的所有可用数据,或者从月初开始的所有数据等).输出结果,然后等待下一次启动。处理时间从几分钟到几小时不等,并且用户从结果里读到的都是旧数据。在数据库
领域,它们就是数据仓库( DWH)或商业智能( BI) 系统。它们每天加载巨大批次的数据,并生成报表,用户在下一次加载数据之前看到的都是相同的报表。从规模上来
说,这种范式既高效又经济。
流式处理
这种范式介于上述两者之间。大部分的业务不要求亚毫秒级的响应,不过也接受不了要等到第二天才知道结果。大部分业务流程都是持续进行的,只要业务报告保持更新,业
务产品线能够持续响应,那么业务流程就可以进行下去,而无需等待特定的响应,也不要求在几毫秒内得到响应。一些业务流程具有持续性和非阻塞的特点,比如针对可疑信
用卡交易的警告、网络警告、根据供应关系实时调整价格、跟踪包衷裹。
流的定义不依赖任何一个特定的框架、 API 或特性。只要持续地从一个无边界的数据集读取数据,然后对它们进行处理并生成结果,那就是在进行流式处理。重点是,整个处理过
程必须是持续的。
流式处理的一些概念
流式处理的很多方面与普通的数据处理是很相似的:写一些代码来接收数据,对数据进行处理,可能做一些转换、聚合和增强的操作,然后把生成的结果输出到某个地方。不过流
式处理有一些特有的概念。
时间
时间或许就是流式处理最为重要的概念,也是最让人感到困惑的。大部分流式应用的操作都是基于时间窗口的。例如,流式应用可能会计算股价的 5 分钟移动平均数。
流式处理系统一般包含如下几个时间概念。
事件时间
事件时间是指所追踪事件的发生时间和记录的创建时间。例如,度量的获取时间、商店里商品的出售时间、网站用户访问网页的时间,等等。在 Kafka 0.10.0 和更高版本里,
生产者会自动在记录中添加记录的创建时间。如果这个时间戳与应用程序对“事件时间”的定义不一样,例如, Kafka 的记录是基于事件发生后的数据库记录创建的,那就
需要自己设置这个时间戳字段。在处理数据流时,事件时间是很重要的。
日志追加时间
日志追加时间是指事件保存到 broker 的时间。在 Kafka 0.10.0 和更高版本里,如果启用了自动添加时间戳的功能,或者记录是使用旧版本的生产者客户端生成的,而且没有包
含时间戳,那么 broker 会在接收这些记录时自动添加时间戳。这个时间戳一般与流式处理没有太大关系,因为用户一般只对事件的发生时间感兴趣。
处理时间
处理时间是指应用程序在收到事件之后要对其进行处理的时间。这个时间可以是在事件发生之后的几毫秒、几小时或几天。同一个事件可能会被分配不同的时间戳,这取决于
应用程序何时读取这个事件。如果应用程序使用了两个线程来读取同一个事件,这个时间戳也会不一样!所以这个时间戳非常不可靠,应该避免使用它。
状态
如果只是单独处理每一个事件,那么流式处理就很简单。
如果操作里包含了多个事件,流式处理就会变得很有意思,比如根据类型计算事件的数量、移动平均数、合并两个流以便生成更丰富的信息流。在这些情况下,光处理单个事件
是不够的,用户需要跟踪更多的信息,比如这个小时内看到的每种类型事件的个数、需要合并的事件、将每种类型的事件值相加 , 等等。事件与事件之间的信息被称为“状态”。
这些状态一般被保存在应用程序的本地变量里。
流式处理包含以下几种类型的状态。
本地状态或内部状态
这种状态只能被单个应用程序实例访问,它们一般使用内嵌在应用程序里的数据库进行维护和管理。本地状态的优势在于它的速度,不足之处在于它受到内存大小的限制 。 所
以,流式处理的很多设计模式都将数据拆分到多个子流,这样就可以使用有限的本地状态来处理它们。
外部状态
这种状态使用外部的数据存储来维护, 一般使用 NoSQL 系统,比如 Cassandra。使用外部存储的优势在于,它没有大小的限制,而且可以被应用程序的多个实例访问,甚至被
不同的应用程序访问。不足之处在于,引入额外的系统会造成更大的延迟和复杂性。大部分流式处理应用尽量避免使用外部存储,或者将信息缓存在本地,减少与外部存储发
生交互,以此来降低延迟,而这就引入了如何维护内部和外部状态一致’性的问题。
流和表的二元性
大家都熟悉数据库表,表就是记录的集合,每个表都有一个主键,并包含了 一 系列由schema 定义的属性。表的记录是可变的(可以在表上面执行更新和删除操作)。我们可以
通过查询表数据获知某一时刻的数据状态。
在将表与流进行对比时,可以这么想:流包含了变更一一流是一系列事件,每个事件就是一个变更。表包含了当前的状态,是多个变更所产生的结果。所以说, 表和流是同一个硬
币的两面 世界总是在发生变化,用户有时候关注变更事件,有时候则关注世界的当前状态。如果一个系统允许使用这两种方式来查看数据,那么它就比只支持一种方式的系统强大。
为了将表转化成流,需要捕捉到在表上所发生的变更,将“insert”、“ update”和“delete” 事件保存到流里。 大部分数据库提供了用于捕捉变更的“Change Data Capture" (CDC )解
决方案 , Kafka 连接器将这些变更发送到 Kafka,用于后续的流式处理。
为了将流转化成表, 需要“应用”流里所包含的所有变更,这也叫作流的“物化”。首先在 内存里 、 内部状态存储或外部数据库里创建一个表,然后从头到尾遍历流里的所
有事件,逐个地改变状态。在完成这个过程之后,得到了一个表,它代表了某个时间点的状态。
假设有一个鞋店,某零售活动可以使用一个事件流来表示 :
“红色、蓝色和绿色鞋子到货”
“蓝色鞋子卖出”
“红色鞋子卖出”
“蓝色鞋子退货”
“绿色鞋子卖出”
如果想知道现在仓库里还有哪些库存,或者到目前为止赚了多少钱,需要对视图进行物化。图 告诉我们,目前还有蓝色和黄色鞋子,账户上有 170 美元。如果想知道鞋店的
繁忙程度,可以查看整个事件流,会发现总共发生了 5 个交易,还可以查出为什么蓝色鞋子被退货。
时间窗口
大部分针对流的操作都是基于时间窗口的,比如移动平均数、 一周内销量最好的产品、系统的 99 百分位等。两个流的合并操作也是基于时间窗口的,我们会合并发生在相同时间
片段上的事件。不过,很少人会停下来仔细想想时间窗口的类型。
• 窗口的大小。是基于 5 分钟进行平均,还是 15 分钟,或者一天?窗口越小,就能越快地发现变更,不过噪声也越多。窗口越大,变更就越平滑,不过延迟也越严重,如果价
格涨了,需要更长的时间才能看出来。
• 窗口移动的频率(“移动间隔”)。 5 分钟的平均数可以每分钟变化一次,或者每秒钟变化一次,或者每当有新事件到达时发生变化。如果“移动间隔”与窗口大小相等,这种
情况被称为“滚动窗口( tumbling window )”。如果窗口随着每一条记录移动,这种情况被称为“滑动窗口( sliding window )”。
• 窗口的可更新时间多长。假设计算了 00:00 到 00:05 之间的移动平均数, 一个小时之后又得到了一些“事件时间”是 00:02 的事件,那么需要更新 00:00 到 00:05 这个窗口的
结果吗?或者就这么算了?理想情况下,可以定义一个时间段,在这个时间段内, 事件可以被添加到与它们相应的时间片段里。如果事件处于 4 个小时以内,那么就更新它们 ,
否则就忽略它们。
窗口可以与时间对齐,比如 5 分钟的窗口如果每分钟移动一次,那么第一个分片可以是00:00-00:05 ,第 二个就是 00:01 -00:06“。它也可以不与时间对齐,应用可以在任何时候启
动,那么第一个分片有可能是 03:17-03:22。滑动窗口永远不会与时间对齐,因为只要有新记录到达,它们就会发生移动。如图 展示了这两种时间窗口的不同之处。
流式处理的设计模式
单个事件处理
处理单个事件是流式处理最基本的模式。这个模式也叫 map 或 filter 模式,因为它经常被用于过滤无用的事件或者用于转换事件( map 这个术语是从 Map-Reduce 模式中来的, map
阶段转换事件, reduce 阶段聚合转换过的事件)。
在这种模式下,应用程序读取流中的事件 ,修改它们,然后把事件生成到另一个流上。比如 ,一个应用程序从一个流中读取日志消息,并把 ERROR 级别的消息写到高优先级的流
中,同时把其他消息写到低优先级的流中。再如,一个应用程序从流中读取事件,并把事件从 JSON 格式改为 Avro 格式。这类应用程序不需要在程序内部维护状态 ,因为每一个
事件都是独立处理的。这也意味着,从错误中恢复或进行负载均衡会非常容易 ,因为不需要进行恢复状态的操作,只需要将事件交给应用程序的另一个实例去处理。
这种模式可以使用一个生产者和一个消费者来实现,如图 所示。
使用本地状态
大部分流式处理应用程序关心的是如何聚合信息,特别是基于时间窗口进行聚合。例如,找出每天最低和最高的股票交易价格并计算移动平均数。
要实现这些聚合操作,需要维护流的状态。在本例中,为了计算每天的最小价格和平均价格 , 需要将最小值和最大值保存下来,并将它们与每一个新值进行对比。
这些操作可以通过本地状态(而不是共享状态)来实现,因为本例中的每一个操作都是基于组的聚合操作,如图 所示。例如,基于各个股票代码进行聚合,而不是基于整个股
票市场。我们使用了一个 Kafka 分区器来确保具有相同股票代码的事件总是被写入相同的分区 。 应用程序的每个实例从分配给它们的分区上获取事件(这是 Kafka 的消费者保证)。
也就是说,应用程序的每一个实例都可以维护一个股票代码子集的状态。
如果流式处理应用程序包含了本地状态,情况就会变得非常复杂,而且还需要解决下列的一些问题。
内存使用
应用实例必须有可用的内存来保存本地状态。
持久化
要确保在应用程序关闭时不会丢失状态,并且在应用程序重启后或者切换到另一个应用实例时可以恢复状态。 Streams 可以很好地处理这些问题,它使用内嵌的 RocksDB 将本
地状态保存在内存里,同时持久化到磁盘上,以便在重启后可以恢复。本地状态的变更也会被发送到 Kafka 主题上。如果 Streams 节点崩溃,本地状态并不会丢失,可以通过
重新读取 Kafka 主题上的事件来重建本地状态。例如,如果本地状态包含“IBM 当前最小价格是 167.19”,并且已经保存到了 Kafka 上,那么稍后就可以通过读取这些数据
来重建本地缓存。这些 Kafka 主题使用了压缩日志,以确保它们不会无限量地增长,方便重建状态。
再均衡
有时候,分区会被重新分配给不同的消费者。在这种情况下,失去分区的实例必须把最后的状态保存起来 , 同时获得分区的实例必须知道如何恢复到正确的状态。
不同的流式处理框架为开发者提供了不同的本地状态支持。如果应用程序需要维护本地状态,那么就要知道框架是否提供了支持。
多阶段处理和重分区
本地状态对按组聚合操作起到很大的作用。但如果需要使用所有可用的信息来获得一个结果呢? 例如,假设要发布每天的“前 10 支”股票,这 10 支股票需要从每天的交易股票中
挑选出来。很显然,如果只是在每个应用实例上进行处理是不够的,因为 10 支股票分布在多个实例上, 如图 所示。 我们需要一个两阶段解决方案。首先,计算每支股票当天
的涨跌,这个可以在每个实例上进行。然后将结果写到一个包含了单个分区的新主题上。另一个单独的应用实例读取这个分区, 找出当天的前 10 支股票。新主题只包含了每支股
票的慨要信息 ,比其他包含交易信息的主题要小很多,所以流量很小,使用单个应用实例就足以应付。有时候需要更多的步骤才能生成结果。
这种多阶段处理对于写过 Map-Reduce 代码的人来说应该很熟悉,因为他们经常要使用多个 reduce 步骤 。 如果写过 Map-Reduce 代码,就应该知道,处理每个 reduce 步骤的应用需
要被隔离开来。与 Map-Reduce 不同的是,大多数流式处理框架可以将多个步骤放在同一个应用里,框架会负责调配每一步需要运行哪一个应用实例(或 worker )。
使用外部查找一一流和表的连接
流式处理需要将外部数据和流集成在一起,比如使用保存在外部数据库里的规则来验证事务,或者将用户信息填充到点击事件当 中。
很明显 ,为了使用外部查找来实现数据填充,可以这样做 : 对于事件流里的每一个点击事件, 从用户信息表里查找相关的用户信息,从中抽取用户的年龄和性别信息,把它们包含
在点击事件里,然后将事件发布到另一个主题上,如图 所示。
这种方式最大的问题在于,外部查找会带来严重的延迟, 一般在 5~ 15ms 之间。这在很多情况下是不可行的。另外,外部数据存储也无法接受这种额外的负载一一流式处理系统每
秒钟可以处理 10~50 万个事件,而数据库正常情况下每秒钟只能处理 1 万个事件,所以需要伸缩性更强的解决方案。
为了获得更好的性能和更强的伸缩性,需要将数据库的信息缓存到流式处理应用程序里。不过,要管理好这个缓存也是一个挑战。比如,如何保证缓存里的数据是最新的?如果刷
新太频繁,那么仍然会对数据库造成压力,缓存也就失去了作用。如果刷新不及时, 那么流式处理中所用的数据就会过时。
如果能够捕捉数据库的变更事件,并形成事件流,流式处理作业就可以监听事件流, 并及时更新缓存。捕捉数据库的变更事件并形成事件流,这个过程被称为 CDC-变更数据捕
捉( Change Data Capture ) 。如果使用了 Connect,就会发现,有一些连接器可以用于执行CDC 任务,把数据库表转成变更事件流。这样就拥有了数据库表的私有副本, 一旦数据库
发生变更,用户会收到通知,并根据变更事件更新私有副本里的数据,如图 所示。
这样一来,当收到点击事件时,可以从本地的缓存里查找 user_id , 并将其填充到点击事件里。因为使用的是本地缓存,它具有更强的伸缩’性,而且不会影响数据库和其他使用数据
库的应用程序。 之所以将这种方案叫作流和表的连接,是因为其中的一个流代表了本地缓存表的变更。
流与流的连接
有时候需要连接两个真实的事件流。什么是“真实”的流?流是无边界的。如果使用一个流来表示一个表,那么就可以忽略流的大部分历史事件,因为你只关心表的当前状态。
如果要连接两个流,那么就是在连接所有的历史事件一一将两个流里具有相同键和发生在相同时间窗口内的事件匹配起来。这就是为什么流和流的连接也叫作基于时间窗口的连接( windowed-join )。
假设有一个由网站用户输入的搜索事件流和一个由用户对搜索结果进行点击的事件流。对用户的搜索和用户对搜索结果的点击进行匹配,就可以知道哪一个搜索的热度更高。很显然,
我们需要基于搜索关键词进行匹配,而且每个关键词只能与一定时间窗口内的事件进行匹配一→匪设用户在输入搜索关键词后几秒钟就会点击搜索结果。因此,我们为每一个流维护
了以几秒钟为单位的时间窗口,并对这些时间窗口事件结果进行匹配,如图 所示。
在 Streams 中,上述的两个流都是通过相同的键来进行分区的,这个键也是用于连接两个流的键。这样一来, user_id:42 的点击事件就被保存在点击主题的分区 5 上,而所有 user_
id:42 的搜索事件被保存在搜索主题的分区 5 上 。 Streams 可以确保这两个主题的分区 5 的事件被分配给同一个任务,这个任务就会得到所有与 user_id:42 相关的事件。 Streams 在内
嵌的 RocksDB 里维护了两个主题的连接时间窗口,所以能够执行连接操作。
乱序的事件
不管是对于流式处理还是传统的 ETL 系统来说,处理乱序事件都是一个挑战。物联网领域经常发生乱序事件: 一个移动设备断开 WiFi 连接几个小时,在重新连上 WiFi 之后将几个
小时累积的事件一起发送出去,如图所示。这在监控网络设备(故障交换机被修复之前不会发送任何诊断数据)或进行生产(装置间的网络连接非常不可靠) 时也时有发生 。
要让流处理应用程序处理好这些场景,需要做到以下几点。
• 识别乱序的事件。应用程序需要检查事件的时间,并将其与当前时间进行比较。
• 规定一个时间段用于重排乱序的事件。比如 3 个小时以内的事件可以重排,但 3 周以外的事件就可以直接扔掉。
• 具有在一定时间段内重排乱序事件的能力。这是流式处理应用与批处理作业的一个主要不同点。假设有一个每天运行的作业, 一些事件在作业结束之后才到达,那么可以重新
运行昨天的作业来更新事件。而在流式处理中,“重新运行昨天的作业”这种情况是不存在的,乱序事件和新到达的事件必须一起处理。
• 具备更新结果的能力。如果处理的结果保存到数据库里,那么可以通过 put 或 update 对结果进行更新。如果流应用程序通过邮件发送结果,那么要对结果进行更新,就需要很
巧妙的手段。
有一些流式处理框架,比如 Google 的 Dataftow 和 Kafka 的 Streams ,都支持独立于处理时间发生的事件,并且能够处理比当前处理时间更晚或更早的事件。它们在本地状态里维护
了多个聚合时间窗口,用于更新事件,并为开发者提供配置时间窗口大小的能力。当然,时间窗口越大,维护本地状态需要的内存也越大。
Streams API 通常将聚合结果写到主题上。这些主题一般是压缩日志主题,也就是说,它们只保留每个键的最新值。如果一个聚合时间窗口的结果需要被更新为晚到事件的结果,
Streams 会直接为这个聚合时间窗口写入一个新的结果,将前一个结果覆盖掉。
重新处理
最后一个很重要的模式是重新处理事件,该模式有两个变种。
• 我们对流式处理应用进行了改进 , 使用新版本应用处理同一个事件流,生成新的结果,并比较两种版本的结果,然后在某个时间点将客户端切换到新的结果流上。
现有的流式处理应用出现了缺陷,修复缺陷之后,重新处理事件流并重新计算结果。对于第一种情况, Kafka 将事件流长时间地保存在可伸缩的数据存储里。也就是说,要使
用两个版本的流式处理应用来生成结果,只需要满足如下条件:
• 将新版本的应用作为一个新的消费者群组 3
• 让它从输入主题的第 一 个偏移量开始读取数据(这样它就拥有了属于自己的输入流事件副本);
• 检查结果流,在新版本的处理作业赶上进度时,将客户端应用程序切换到新的结果流上 。
第二种情况有一定的挑战性。它要求“重置”应用,让应用回到输入流的起始位置开始处理,同时重置本地状态(这样就不会将两个版本应用的处理结果棍淆起来了),而且还可
能需要清理之前的输出流。虽然 Streams 提供了一个工具用于重置应用的状态,不过如果有条件运行两个应用程序井生成两个结果流,还是建议使用第一种方案。第一种方案更加
安全,多个版本可以来回切换,可以比较不同版本的结果,而且不会造成数据的丢失,也不会在清理过程中引入错误。
Kafka Streams的架构概览
构建拓扑
每个流式应用程序至少会实现和执行一个拓扑。拓扑(在其他流式处理框架里叫作 DAG,即有向无环图)是一个操作和变换的集合,每个事件从输入到输出都会流经它。如 图 所示。
哪怕是一个很简单的应用,都需要一个拓扑。拓扑是由处理器组成的,这些处理器是拓扑图里的节点(用椭圆表示)。
大部分处理器都实现了一个数据操作一一过滤、映射、聚合等。数据源处理器从主题上读取数据,并传给其他组件 , 而数据源处理器从上一个处理器
接收数据,并将它们生成到主题上。拓扑总是从一个或多个数据源处理器开始,并以一个或多个数据池处理器结束。
对拓扑进行伸缩
Streams 通过在单个实例里运行多个线程和在分布式应用实例间进行负载均衡来实现伸缩。用户可以在一台机器上运行 Streams 应用,并开启多个线程,也可以在多台机器上运行
Streams 应用。不管采用何种方式,所有的活动线程将会均衡地处理工作负载。Streams 引擎将拓扑拆分成多个子任务来并行执行。拆分成多少个任务取决于 Streams 引
擎,同时也取决于主题的分区数量。每个任务负责一些分区:任务会订阅这些分区,并从分区读取事件数据 , 在将结果写到数据池之前,在每个事件上执行所有的处理步骤。
这些任务是 Streams 引 擎最基本的并行单元,因为每个任务可以彼此独立地执行,如图所示 。
开发人员可以选择每个应用程序使用的线程数。如果使用了多个线程,每个线程将会执行一部分任务。如果有多个应用实例运行在多个服务器上,每个服务器上的每一个线程都会
执行不同的任务。这就是流式应用的伸缩方式: 主题里有多少分区,就会有多少任务。如果想要处理得更快,就添加更多的线程。如果一台服务器的资源被用光了,就在另一台服
务器上启动应用实例。 Kafka 会自动地协调工作,它为每个任务分配属于它们的分区, 每个任务独自处理自己的分区,并维护与聚合相关的本地状态,如图 所示。
有时候一个步骤需要处理来自多个分区的结果,这样就会在任务之间形成依赖。例如,在点击事件流的例子里对两个流进行了连接,在生成结果之前, 需要
从每一个流的分区里获取数据 。 Streams 将连接操作所涉及的分区全部分配给相同的任务,这样,这个任务就可以从相关的分区读取数据,井独立执行连接操作。这也就是为什么
Stream s 要求同一个连接操作所涉及的主题必须要有相同数目的分区,而且要基于连接所使用的键进行分区。
如果应用程序需要进行重新分区,也会在任务之间形成依赖。例如,在点击事件流的例子里,所有的事件使用用户 ID 作为键。如果想要基于页面或者邮政编码生成统计信息该怎
么办?此时就需要使用邮政编码对数据进行重新分区,并在新分区上运行聚合操作。如果任务 1 处理来自分区 1 的数据,这些数据到达另一个处理器,这个处理器对数据进行重新
分区( groupBy 操作),它需要对数据进行 shuffle ,也就是把数据发送给其他任务进行处 理。 与其他流式处理框架不一样的是, Streams 通过使用新的键和分区将事件写到新的主
题来实现重新分区 ,并启动新的任务从新主题上读取和处理事件。重新分区的步骤是将拓扑拆分成两个子拓扑,每个子拓扑都有自己的任务集,如图 所示。第二个任务集依
赖第一个任务集,因为它们处理的是第一个子拓扑的结果。不过,它们仍然可以独立地并行执行,因为第一个任务集以自己的速率将数据写到一个主题上,而第二个任务集也以自
己的速率从这个主题读取和处理事件。两个任务集之间不需要通信, 也没有共享资源,而且它们也不需要运行在相同的线程里或相同的服务器上 。 这是 Kafka 提供的最有用的特性
之一一一减少管道各个部分之间的依赖。
从故障中存活下来
Streams 的伸缩模型不仅允许伸缩应用,还能优雅地处理故障。
包括本地状态在内的所有数据被保存到有高可用性的 Kafka 上。如果应用程序出现故障需要重启,可以从Kafka 上找到上一次处理的数据在流中的位置,并从这个位置开始继续处理。如果本地状
态丢失( 比如可能需要将服务器替换掉),应用程序可以从保存在 Kafka 上的变更日志重新创建本地状态。
Streams 还利用了消费者的协调机制来实现任务的高可用性。如果一个任务失败,只要还有其他钱程或者应用程序实例可用,就可以使用另一个线程来重启该任务。这类似于消费
者群组的故障处理,如果一个消费者失效,就把分区分配给其他活跃的消费者。
流式处理使用场景
如果想快速处理事件,而不是为每个批次等上几个小时,但又不是真的要求毫秒级的响应,那么流式处理(或者说持续处理)就可以派上用场了 。下面来看一些例子,它们都使用流式处理来解决实际的问题。
客户服务
假设你向一个大型的连锁酒店预订了一个房间,并希望收到邮件确认和票据。在预订了几分钟之后,仍然没有收到确认邮件,于是打电话向客服确认。客服的回复是:“我在
我们的系统里看不到订单,不过从预订系统加载数据的批次作业每天只运行一次,所以请明天再打电话过来。你应该可以在 2-3 个工作日之后收到确认邮件 。”这样的服务有
点糟糕,不过有人已经不止一次地在一家大型连锁酒店遭遇过类似的问题。我们真正需要的是,连锁酒店的每一个系统在预订结束之后的几秒钟或者几分钟之内都能发出通
知,包括客服中心、酒店、发送确认邮件的系统、网站等。有的用户可能还希望客服 中心能够立即获知自己在这家连锁酒店的历史入住数据,前台能够知道他是一个忠实的客
户,从而提供更高级别的服务。如果使用流式处理应用来构建这些系统,就可以实现几近实时的接收和处理这些事件,从而带来更好的用户体验。如果有这样的系统,就可以
在几分钟之内收到邮件确认,信用卡就可以及时扣款,然后发送票据,服务台就可以马上回答有关预订房间的问题了。
物联网
物联网包含很多东西,从用于调节温度和自动添加洗衣剂的家居设备,到制药行业的实时质量监控设备。流式处理在传感器和设备上应用,最为常见的是用于预测l何时该进行
设备维护。这个与应用监控有点相似,不过这次是应用在硬件上,而且应用在很多不同的行业一一制造业、通信(识别故障基站)、有线电视(在用户投诉之前识别出故障机
顶盒)等。每一种场景都有自己的特点,不过目标是一样的 处理大量来自设备的事件,并识别出一些模式,这些模式预示着某些设备需要进行维护,比如交换机数据包的
下降、生产过程中需要更大的力气来拧紧螺丝 , 或者用户频繁重启有线电视的机顶盒。
• 欺诈检测。欺诈检测也被叫作异常检查,是一个非常广泛的领域, 专注于捕捉系统中的“作弊者”或不良分子,比如信用卡欺诈、股票交易欺诈、视频游戏作弊或者
网络安全风险。在这些欺诈行为造成大规模的破坏之前,越早将它们识别出来越好。一个几近实时的系统可以快速地对事件作出响应,停止一个还没有通过审核的交易
要比等待批次作业在 3 天之后才发现它是一个欺诈交易要更容易处理。 这也是一个在大规模事件流里识别模式的问题。
在网络安全领域,有一个被称为发信标( beaconing )的欺诈手泣, 黑客在组织内部放置恶意软件,该软件时不时地连接到外部网络接收命令。一般来说,网络可以抵
挡来自外部的攻击,但难以阻止内部到外部的突围。通过处理大量的网络连接事件流,识别出不正常的通信模式,检测出该主机不经常访问的某些 IP 地址, 在蒙受更
大的损失之前向安全组织发出告警。
如何选择流式处理框架
在比较两个流式处理系统时,要着重考虑使用场景是什么 。 以下是一些需要考虑的应用类别。
摄取
摄取的目的是将数据从一个系统移动到另一个系统,并在传输过程中对数据进行一些修改,使其更适用于目标系统。
低延迟
任何要求立即得到响应的应用。有些欺诈检测l场景就属于这一类。
异步微服务
这些微服务为大型的业务流程执行一些简单操作,比如更新仓储信息。这些应用需要通过维护本地状态缓存来提升性能。
几近实 时的数据分析
这些流式媒体应用程序执行复杂的聚合和连接,以便对数据进行切分,并生成有趣的业务见解。
选择何种流式处理系统取决于要解决什么问题。
• 如果要解决摄取问题,那么需要考虑一下是需要一个流式处理系统还是一个更简单的专注于摄取的系统,比如 Kafka Connect。如果确定需要一个流式处理系统,那就要确保
它拥有可用的连接器,并且要保证目标系统也有高质量的连接器可用 。
• 如果要解决的问题要求毫秒级的延迟,那么就要考虑一下是否一定要用流。 一般来说,请求与响应模式更加适用于这种任务。如果确定需要一个流式处理系统,那就需要选择
一个支持低延迟的模型,而不是基于微批次的模型。
• 如果要构建异步微服务,那么需要一个可以很好地与消息总线(希望是 Kafka ) 集成的流式处理系统。它应该具备变更捕捉能力,这样就可以将上游的变更传递到微服务本地
的缓存里,而且它要支持本地存储,可以作为微服务数据的缓存和物化视图。
• 如果要构建复杂的数据分析引擎,那么也需要一个支持本地存储的流式处理系统,不过这次不是为了本地缓存和物化视图,而是为了支持高级的聚合、时间窗口和连接,因为
如果没有本地存储,就很难实现这些特性。 API 需要支持自定义聚合、基于时间窗口的操作和多类型连接。