流式计算之 Kafka Stream
目前世面上有很多关于流式计算处理框架,其中就包括基于 Kafka 的 Stream 流计算。由于在项目中使用到了 Kafka Stream 流,所以在此记录一下。
此次记录主要是以 流式计算框架、Kafka Stream优势、 Kafka Stream 各种特性的流程来组织和介绍,最后简单总结一下.
流式计算
流式计算是什么
流式计是一种计算模型,和其他计算模型最大的区别是它在输入上是持续的,处理端会源源不断收到数据,意味着你永远无法拿到全量数据去计算。同时,计算结果也是源源不断输出的。
流式计算对实时性要求也比较高,一般是先定义目标计算,然后数据到来之后将计算逻辑应用于数据。同时为了提高计算效率,往往尽可能采用增量计算代替全量计算。
批量处理模型中,一般先有全量数据集,然后定义计算逻辑,并将计算应用于全量数据。特点是全量计算,并且计算结果一次性全量输出。
流式计算框架
目前业界比较成熟的的框架包括: Apache Spark、Storm、Flink等。
这些框架的特点是拥有强大的计算能力,例如 Spark Streaming 上已经包含 Graph Compute,MLLib 等适合迭代计算库,在特定场景中非常好用。
然而,这些框架也有通用的一个缺点:使用复杂、集群资源消耗较大。
Kafka Stream
在 Kafka v0.10版本中,引入了 Stream 概念。其设计目标就是流式计算能力的轻量级流式计算库。那么它和其他流式处理优点有哪些呢?
- 相较于流式处理框架,而 Kafka Stream 提供的是一个基于 Kafka 的流式处理类库。总所周知,框架是要求开发者遵循框架特定的方式去开发逻辑部分,供框架调用。开发者很难了解框架的具体运行方式,从而使得学习、开发、调试、运维成本提高,使用受限。而 Kafka Stream 作为一个流式处理类库,直接提供给使用者去调用,整个流式处理流程全由开发者控制,使用和调试难度大大降低。
- 就流式处理系统而言,基本都支持Kafka作为数据源。例如Storm具有专门的 kafka-spout,而Spark也提供专门的 spark-streaming-kafka 模块。事实上,Kafka基本上是主流的流式处理系统的标准数据源。换言之,大部分流式系统中都已部署了Kafka,此时使用 Kafka Stream 的成本非常低
- 使用Storm或Spark Streaming时,需要为框架本身的进程预留资源,如Storm的supervisor和Spark on YARN的node manager。即使对于应用实例而言,框架本身也会占用部分资源,如Spark Streaming需要为shuffle和storage预留内存
- 由于Kafka本身提供数据持久化,因此Kafka Stream提供滚动部署和滚动升级以及重新计算的能力
- 由于Kafka Consumer Rebalance机制,Kafka Stream可以在线动态调整并行度
上述是总结下来的 Kafka Stream 的优点,最终是否选择 Kafka Stream 作为项目流处理工具,还是取决于具体项目场景是否符合。例如,网易云轻舟的 APM 项目,因为是容器化部署,所以轻量化是我们考虑的重要因素之一,加之服务端需要不断接收 trace 进行分类和计算,于是 Kafka Stream 便成为我们首选。在后续中,会介绍我们具体使用 Kafka Stream 的姿势。
Kafka Stream架构
上图展示了数据如何在 Kafka Stream 中传递计算的,从 Input topic 中取得数据,通过 stream 中一系列业务处理和计算,再将结果输出到 Output topic。在 stream 中, Kafka Stream 提供多种特性功能供开发人员使用,其中就包括:自定义处理器 Processor、状态存储 StateStore 、 时间窗口 TimeWindow、 KStream & KTable 等。
Processor Topology
Kafka Stream 提供了 Processor 和 Topology 这两种概念,这二者通常结合使用,构成了流处理中的数据流向和节点处理的关系。
Processor: 一个数据处理节点,允许开发者在其中定义业务数据计算逻辑、计算处理间隔、结果输出规则等。
Topology : Stream 数据流处理逻辑图,数据流从何处输入、何处处理、何处输出均由 Topology 事先定义,保证了数据流的有向性。
如上,Processor 和 Topology 构成了点和图的关系,在官网的 WordCount 示例中,一个点、一条处理流就构成最简单的流处理关系。 但是在实际业务场景中,数据有可能不止做一次计算处理,而且某些计算的数据源是二次计算得来,最后统计最终结果。
在网易云监控项目中,我们需要对监控数据做基于主维度的聚合操作,再基于主维度做更高维度的聚合。例如:收到监控数据为一台主机的 load ,那么我们可以基于该监控项的主维度(主机uuid)进行一次聚合操作,统计出一天内该主机的 load 情况,其次我们可以在这之前完成更高维度的聚合(如环境维度),这样我们就能得知一个环境的整体 load 情况。以下为项目构建 topology 示例,供参考:
public Topology buildTopology(List<ProcessorSupplier<String, MetricDataBO>> suppliers) {
Topology build = new Topology();
build.addSource("Source", metricDataTopic)
.addProcessor("Process0", suppliers.get(0), "Source")
.addStateStore(
getWindowsStore(STATE_STORE_NAME + "0")
,"Process0");
for (int i = 1; i < suppliers.size(); i++) {
build.addProcessor("Process" + i, suppliers.get(i), "Process" + (i - 1))
.addStateStore(
getWindowsStore(STATE_STORE_NAME + i)
,"Process" + i);
}
build.addSink("Sink", alarmProcTopic, "Process" + (suppliers.size() - 1), "Process0");
return build;
}
State Store
状态存储。在 Kafka Stream 中,数据计算分为有状态数据处理和无状态数据处理。
其中无状态数据,可以通过 Fliter 、 Map 、Joins 类似操作,这些数据只要流过一遍即可,不依赖前后状态。
有状态数据计算,则主要是基于时间进行聚合操作。 如要计算一段时间范围内的TopK问题,当数据达到计算节点时需要根据内存中状态计算出数值。具体示例也同样可以参考上述 Topology 的代码片段,其实现方式主要是通过在 Processor 计算节点后增加 StateStore 来实现。
Kafka Stream 可以为每个流任务嵌入一个或多个本地状态存储,并且允许开发者通过API的方式进行访问、查询所需要处理的数据。这些状态数据是底层是基于 RocksDB 数据库实现的,本质其实是在内存的一个 hashmap。但我在实际使用过程中,发现使用 RocksDB 默认配置存在一些坑点,其默认的写缓冲区大小、块大小不满足业务场景,遂进行参数优化配置,这里就不再赘述,欢迎私下沟通。
Store的容错机制
为了能够让 State Store 具有容错能力,并允许开发者在迁移状态存储时不丢失数据,State Store在迁移过程中会不断将数据备份至一个 topic 中,这个 topic 可以理解为该状态存储的 changelog ,并且该 topic 会经过一次压缩。压缩的目的是为了避免该 topic 内数据无限期增长、减少在 Kafka 集群中的存储消耗,并且在最短时间内从 changelog 中恢复数据。
开启 Fault Tolerance 代码示例:
import org.apache.kafka.streams.state.StoreBuilder;
import org.apache.kafka.streams.state.Stores;
StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = Stores.keyValueStoreBuilder(
Stores.persistentKeyValueStore("Counts"),
Serdes.String(),
Serdes.Long())
.withLoggingEnabled();// enable backing up the store to a changelog topic
KTable vs. KStream
KTable和KStream是Kafka Stream中非常重要的两个概念,它们是Kafka实现各种语义的基础。因此这里有必要分析下二者的区别。
KStream是一个数据流,可以认为所有记录都通过Insert only的方式插入进这个数据流里。而KTable代表一个完整的数据集,可以理解为数据库中的表。由于每条记录都是Key-Value对,这里可以将Key理解为数据库中的Primary Key,而Value可以理解为一行记录。可以认为KTable中的数据都是通过Update only的方式进入的。也就意味着,如果KTable对应的Topic中新进入的数据的Key已经存在,那么从KTable只会取出同一Key对应的最后一条数据,相当于新的数据更新了旧的数据。
以下图为例,假设有一个KStream和KTable,基于同一个Topic创建,并且该Topic中包含如下图所示5条数据。此时遍历KStream将得到与Topic内数据完全一样的所有5条数据,且顺序不变。而此时遍历KTable时,因为这5条记录中有3个不同的Key,所以将得到3条记录,每个Key对应最新的值,并且这三条数据之间的顺序与原来在Topic中的顺序保持一致。这一点与Kafka的日志compact相同。
根据业务需求,两种数据类型可以自行取舍,同时 Kafka Stream 也支持二者之间相互转换(虽然相互转换)。在 Kafka 2.5 之后的版本,可以通过 KStream#ToTable() 方式将 KStream 转换为 KTable。 而在 2.5 版本之前,虽然没有办法直接函数去转换,但可以使用groupkey的方式间接转换。具体代码示例如:
KStreamBuilder builder = new KStreamBuilder();
KStream<String, Long> stream = ...; // some computation that creates the derived KStream
KTable<String, Long> table = stream.groupByKey().reduce(
new Reducer<Long>() {
@Override
public Long apply(Long aggValue, Long newValue) {
return newValue;
}
},
"dummy-aggregation-store");
其他
除此之外,Kafka Stream 还具体一些其他重要特性,如不同种类Window计算等,这些内容还在学习记录中,后续分享出来。
总结
- Kafka Stream 是一个轻量级的流式处理类库,可以在特定场景帮助开发人员去完成计算任务。
- 基于节点和拓扑图模式,可以清晰的制定数据流向。
- 基于状态存储,可以满足在流式计算过程中、过程后的数据查询等服务。另外其具有容错机制,保证应用弹性伸缩时数据不会丢失。
参考文献