流式计算之 Kafka Stream

5,691 阅读9分钟

流式计算之 Kafka Stream

目前世面上有很多关于流式计算处理框架,其中就包括基于 Kafka 的 Stream 流计算。由于在项目中使用到了 Kafka Stream 流,所以在此记录一下。

此次记录主要是以 流式计算框架、Kafka Stream优势、 Kafka Stream 各种特性的流程来组织和介绍,最后简单总结一下.

流式计算

流式计算是什么

流式计是一种计算模型,和其他计算模型最大的区别是它在输入上是持续的,处理端会源源不断收到数据,意味着你永远无法拿到全量数据去计算。同时,计算结果也是源源不断输出的。

img

流式计算对实时性要求也比较高,一般是先定义目标计算,然后数据到来之后将计算逻辑应用于数据。同时为了提高计算效率,往往尽可能采用增量计算代替全量计算。

批量处理模型中,一般先有全量数据集,然后定义计算逻辑,并将计算应用于全量数据。特点是全量计算,并且计算结果一次性全量输出。

流式计算框架

目前业界比较成熟的的框架包括: 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 的优点,最终是否选择 Kafka Stream 作为项目流处理工具,还是取决于具体项目场景是否符合。例如,网易云轻舟的 APM 项目,因为是容器化部署,所以轻量化是我们考虑的重要因素之一,加之服务端需要不断接收 trace 进行分类和计算,于是 Kafka Stream 便成为我们首选。在后续中,会介绍我们具体使用 Kafka Stream 的姿势。

Kafka Stream架构


Kafka Stream Architecture

​ 上图展示了数据如何在 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 State存储

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相同。

image-20200311103002587

根据业务需求,两种数据类型可以自行取舍,同时 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");

KStream 和 KTable 相互转换

其他

​ 除此之外,Kafka Stream 还具体一些其他重要特性,如不同种类Window计算等,这些内容还在学习记录中,后续分享出来。

总结

  • Kafka Stream 是一个轻量级的流式处理类库,可以在特定场景帮助开发人员去完成计算任务。
  • 基于节点和拓扑图模式,可以清晰的制定数据流向。
  • 基于状态存储,可以满足在流式计算过程中、过程后的数据查询等服务。另外其具有容错机制,保证应用弹性伸缩时数据不会丢失。

参考文献