Flink 实战系列 —— 从 Kafka 到 ES

【Flink 实战系列】通过一个个简单的例子,图解分析 Flink 的底层原理。

做个数据搬运工

本例的场景非常常见:消费 Kafka 的数据写入到 ES。Kafka 是常见的 Source,ElasticSearch 是常见的 Sink。

Kafka 中的数据格式如下,为简化程序,测试数据做了尽可能的精简:

{
"gid" : "001254500828905",
"timestamp" : 1620981709790
}

版本说明:

组件 版本
Flink 1.17.2
Kafka-Source-Connector 1.17.2
ES-Sink-Connector 3.0.1-1.17
ElasticSearch 7.8.1
Kafka 2.1.0

Maven pom.xml

<properties>
<flink.version>1.17.2</flink.version>
<flink.connector-es-version>3.0.1-1.17</flink.connector-es-version>
<gson.version>2.10.1</gson.version>
</properties> <dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-runtime-web</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-elasticsearch7</artifactId>
<version>${flink.connector-es-version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
</dependencies>

实现:

package org.example.flink.data;

public class Trace {

	private String gid;
private long timestamp; // 省略Getter, Setter
}
package org.example.flink;

import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.connector.base.DeliveryGuarantee;
import org.apache.flink.connector.elasticsearch.sink.Elasticsearch7SinkBuilder;
import org.apache.flink.connector.elasticsearch.sink.FlushBackoffType;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend;
import org.apache.flink.streaming.api.datastream.DataStreamSink;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.http.HttpHost;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.Requests;
import org.elasticsearch.common.xcontent.XContentType;
import org.example.flink.data.Trace; import com.google.gson.Gson; public class FromKafkaToEs { public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
configuration.setString("rest.port", "9091");
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(configuration);
// 一定要开启checkpoint,只有开启了checkpoint才能保证一致性语义
env.enableCheckpointing(2 * 60 * 1000);
// 使用rocksDB作为状态后端
env.setStateBackend(new EmbeddedRocksDBStateBackend()); KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers("127.0.0.1:9092")
.setTopics("trace-2024")
.setGroupId("group-01")
.setStartingOffsets(OffsetsInitializer.committedOffsets())
.setProperty("commit.offsets.on.checkpoint", "true")
.setValueOnlyDeserializer(new SimpleStringSchema())
.build(); DataStreamSource<String> sourceStream = env.fromSource(source, WatermarkStrategy.noWatermarks(),
"Kafka Source");
sourceStream.setParallelism(2); // 设置source算子的并行度为2 SingleOutputStreamOperator<Trace> mapStream = sourceStream.map(new MapFunction<String, Trace>() { private static final long serialVersionUID = 1L; @Override
public Trace map(String value) throws Exception {
Gson gson = new Gson();
Trace trace = gson.fromJson(value, Trace.class);
return trace;
} });
mapStream.setParallelism(1); // 设置map算子的并行度为1 DataStreamSink<Trace> sinkStream = mapStream.sinkTo(
new Elasticsearch7SinkBuilder<Trace>()
.setHosts(new HttpHost("127.0.0.1", 9200, "http"))
.setEmitter(
(element, context, indexer) -> {
IndexRequest indexRequest = Requests.indexRequest()
.index("trace-index")
.source(element.toString(), XContentType.JSON);
indexer.add(indexRequest);
}
)
.setDeliveryGuarantee(DeliveryGuarantee.AT_LEAST_ONCE)
// This enables an exponential backoff retry mechanism,
// with a maximum of 5 retries and an initial delay of 1000 milliseconds
.setBulkFlushBackoffStrategy(FlushBackoffType.CONSTANT, 5, 1000)
.build());
sinkStream.setParallelism(2); // 设置sink算子的并行度为2 // 执行
env.execute("From-Kafka-To-ElasticSearch");
}
}

由于我们引入了 flink-runtime-web,程序启动后,可以在控制台看到 Flink Web 的 endpoint,可以在浏览器中查看任务的运行时状态:

我们设置了 Source 算子的并行度为 2,Map 算子的并行度为 1,Sink 算子的并行度为 2,Job Graph 以及数据在各算子之间的流动状态如下图所示:

数据在 Kafka 中以序列化的方式存储,经由 Map 算子转换为 Object 结构,然后写入 ES 中存储为 doc 格式。

几个关键点

Kafka Source Idleness

如果 Source 算子的并行度 > Kafka Topic 分区数量,意味着会有 Source 算子消费不到数据,在早期版本的 Kafka Source Connector 中,如果 Source 算子的并行度设置不当,会消费不到数据的现象,因为有数据的 Source 算子一直在等待没有数据的 Source 算子的 watermark(目的是取所有算子的 watermark 的最小值作为 Job 的 watermark),但是没有数据对接的那个 Source 算子的 watermark 被初始成了 -1,就导致作业的 watermark 永远不会 advance。解决办法是设置 Source 算子的并行度一定要 ≤ Kafka Topic 分区数量。

本版本中的 Kafka Source Connector 新增了空闲检测的功能,如果某个 Source 算子一段时间内没有数据流动,会被识别为 idle 状态,不会再 hold back 整个作业 watermark 的进度。

Kafka Offset Commiting

Kafka 消费偏移量的提交方式是非常重要的配置。当开启 Checkpoint 时,Kafka 偏移量默认会在 Checkpoint 成功保存 后提交到 Kafka,这样做的目的是为了保证 Flink Checkpoint 中的 State(记录的就是 Kafka 的 offset)和 Kafka broker 中记录的 offset 的一致性。

如果不开启 Checkpoint,Kafka Source 算子就依赖其内部的 kafka consumer 的自动提交逻辑来管理消费偏移量了。这可以通过配置 consumer 的 properties 来实现,具体的配置项是: enable.auto.commitauto.commit.interval.ms

一致性测试

Flink 的 Kafka Source Connector 可以做到 Exactly-Once 保障,即保证每条数据精确消费一次,ES Sink Connector 可以做到 AtLeast-Once 保障,即至少写入一次。真的是这样吗?我们做个实验:

启动程序后,向 Kafka 生产 10, 000 条数据,可以看到 ES 中索引的数据量也是 10, 000;

停止 ES 服务:

继续向 Kafka 中写入 10,000 条数据,观察 Flink 任务的运行状态,此时 Flink 任务状态是 Restarting,因为 es 写入失败触发了 Flink 的重启策略:

半小时后,重新启动 ES 服务;

Flink 在重启过程中,发现 es 状态恢复正常,尝试从 Kafka 的 committed offset 消费数据,成功将 es 停机期间的 10,000 条数据补到了索引中:

如果 Flink 在运行过程中出现故障重启,将尝试从上一个 checkpoint 中进行恢复,本例中将会从上一个 checkpoint 记录的 offset 开始消费数据,因此这部分数据可能在目标端出现重复写入:

图解原理

通过上面的实验可以看到,ES 的宕机没有对数据一致性造成任何影响,在服务恢复后数据仍然一条不差,Flink 确实太强大、太健壮了!Flink 是如何做到一致性保障的呢?

秘密都在 Checkpoint 中,Checkpoint 是 Flink 实现容错机制的核心功能之一,它确保了在分布式系统中,即使发生故障(如节点宕机、网络分区等),作业也能够从上次成功处理的状态继续运行,而不会丢失数据或重复处理,无需人工干预,在生产环境中强烈推荐开启。

Flink 的 Kafka Source Connector 是一个有状态的算子(operator),它集成了 Flink 的检查点机制,它的状态是所有 Kafka 分区的读取偏移量。当一个检查点被触发时,每一个分区的偏移量都被存到了这个检查点中。当一个 Job 的所有算子都成功存储了自己的状态,一个检查点才算完成。检查点的完成代表这个检查点之前的所有数据都完成了端到端的一致性传输,本例中代表从 Kafka 消费到的数据都已经成功落入到了 ES 中。因此,当系统从故障中恢复时,只要从上一个成功保存的检查点处恢复,就可以做到端到端的一致性语义保障。

我用图解的方式详细介绍 Flink 是如何管理 Kafka 消费位点的

第 1 步

如下图所示,一个 Kafka topic 有两个 partition,每个 Kafka Source 算子负责一个 partition 数据的消费。Kafka Source 算子是一个有状态算子,状态中保存的是各自分区的消费偏移量。在初始时刻,消费偏移量是 0。

第 2 步

Flink 的 JobManager 会定时向数据流中注入 Checkpoint barrier,作为 checkpoint 的界限。Checkpoint barrier 跟随数据在算子链中流动,每到达一个算子都会触发将状态快照写入状态后端的动作。

第 3 步

Kafka Source 算子开始消费数据,并在算子的状态中记录各自分区的消费偏移量,图示时刻 Kafka-Source-1 算子的 Offset = 2,Kafka-Source-2 算子的 Offset = 1。注意此时的状态仍保存在内存中,并没有持久化。

图中所示的这个状态还有一个关键之处,就是此刻 Kafka-Source-1 算子收到了 Checkpoint barrier,开启了 Begin alignment,Kafka-Source-1 算子将暂停消费数据,等待其他的 Kafka-Source 算子收到相同的 Checkpoint barrier。

第 4 步

Kafka-Source-2 也收到了 Checkpoint barrier,所有算子都收到了同一版本的 Checkpoint barrier 后,标志着 End alignment。此时 Kafka-Source-1 算子的 Offset = 2,Kafka-Source-2 算子的 Offset = 4。

第 5 步

Checkpoint 的 End alignment 会触发算子状态保存到状态后端的动作,此时 Kafka-Source 算子将状态 { offset1 = 2, offset2 = 4 } 持久化保存到 State backend。只有当所有算子的状态都成功保存到 State backend,一个 Checkpoint 才算保存成功。

第 6 步

Checkpoint barrier 继续沿着算子链向下游流动,Map 算子集齐同一版本的 Checkpoint barrier 后,也将触发将状态保存到 State backend 的动作。

第 7 步

一般来说,Map 算子是无状态的,假设我们为 Map 算子添加了自定义的状态值 Count,用于统计数据流量。下图中 Map 算子的状态也已经保存到 State backend 中。以此类推,直到所有算子的状态全部保存到 State backend 中,一个 Checkpoint 才算保存成功。

故障恢复

在发生故障时(比如,某个 TaskManager 宕机了),所有的 Operator task 会被重启,而他们的状态会被重置到最近一次成功的 Checkpoint。Kafka source 分别从 offset 2 和 4 重新开始读取消息(因为这是完成的 Checkpoint 中存的 offset)。当作业重启后,我们可以期待正常的系统操作,就好像之前没有发生故障一样。如下图所示:

参考

[1] 云邪的博客:Flink 如何管理 Kafka 消费位点

[2] Flink官方文档

Flink 实战之从 Kafka 到 ES的更多相关文章

  1. 《从0到1学习Flink》—— Flink 写入数据到 Kafka

    前言 之前文章 <从0到1学习Flink>-- Flink 写入数据到 ElasticSearch 写了如何将 Kafka 中的数据存储到 ElasticSearch 中,里面其实就已经用 ...

  2. Flink实战(八) - Streaming Connectors 编程

    1 概览 1.1 预定义的源和接收器 Flink内置了一些基本数据源和接收器,并且始终可用.该预定义的数据源包括文件,目录和插socket,并从集合和迭代器摄取数据.该预定义的数据接收器支持写入文件和 ...

  3. Flink实战| Flink+Redis实时防刷接口作弊

    随着人口红利的慢慢削减,互联网产品的厮杀愈加激烈,大家开始看好下沉市场的潜力,拼多多,趣头条等厂商通过拉新奖励,购物优惠等政策率先抢占用户,壮大起来.其他各厂商也紧随其后,纷纷推出自己产品的极速版,如 ...

  4. 《大数据实时计算引擎 Flink 实战与性能优化》新专栏

    基于 Flink 1.9 讲解的专栏,涉及入门.概念.原理.实战.性能调优.系统案例的讲解. 专栏介绍 扫码下面专栏二维码可以订阅该专栏 首发地址:http://www.54tianzhisheng. ...

  5. flink 根据时间消费kafka

    经常遇到这样的场景,13点-14点的时候flink程序发生了故障,或者集群崩溃,导致实时程序挂掉1小时,程序恢复的时候想把程序倒回13点或者更前,重新消费kafka中的数据. 下面的代码就是根据指定时 ...

  6. flink学习笔记-flink实战

    说明:本文为<Flink大数据项目实战>学习笔记,想通过视频系统学习Flink这个最火爆的大数据计算框架的同学,推荐学习课程: Flink大数据项目实战:http://t.cn/EJtKh ...

  7. Flink实战(六) - Table API & SQL编程

    1 意义 1.1 分层的 APIs & 抽象层次 Flink提供三层API. 每个API在简洁性和表达性之间提供不同的权衡,并针对不同的用例. 而且Flink提供不同级别的抽象来开发流/批处理 ...

  8. Flink 实战:如何解决生产环境中的技术难题?

    大数据作为未来技术的基石已成为国家基础性战略资源,挖掘数据无穷潜力,将算力推至极致是整个社会面临的挑战与难题. Apache Flink 作为业界公认为最好的流计算引擎,不仅仅局限于做流处理,而是一套 ...

  9. Spark Streaming,Flink,Storm,Kafka Streams,Samza:如何选择流处理框架

    根据最新的统计显示,仅在过去的两年中,当今世界上90%的数据都是在新产生的,每天创建2.5万亿字节的数据,并且随着新设备,传感器和技术的出现,数据增长速度可能会进一步加快. 从技术上讲,这意味着我们的 ...

  10. Elasticsearch数据库优化实战:让你的ES飞起来

    摘要:ES已经成为了全能型的数据产品,在很多领域越来越受欢迎,本文旨在从数据库领域分析ES的使用. 本文分享自华为云社区<Elasticsearch数据库加速实践>,原文作者:css_bl ...

随机推荐

  1. .net core下使用事件总线

            随着微服务的火热,DDD(领域驱动设计模式)思想风起云涌,冲击着整个软件生态系统.其中,事件总线那是必须知道的了,于是我便抱着一个学习DDD的心态搭建了一个博客网站,目前该网站正在建设 ...

  2. 如何选择 Linux 发行版

    简介 要建立云服务器,首先需要安装操作系统.在现代环境中,几乎所有情况下都是指 Linux 操作系统.从历史上看,Windows 服务器和其他类型的 Unix 在特定的商业环境中都很流行,但现在几乎每 ...

  3. [JS设计模式]:鸭子类型与多态

    鸭子类型 编程语言按照数据类型大体可以分为两类,一类是静态类型语言,另一类是动态类型语言. 动态类型语言对变量类型的宽容给实际编码带来了很大的灵活性.由于无需进行类型检测,我们可以尝试调用任何对象的任 ...

  4. JVM(JAVA Virtual Machine)Java虚拟机

    JVM的跨平台性 一次编写,到处运行 JVM将字节码文件编译成对应操作系统的机器码 JVM的语言无关性 JVM的内存区域 虚拟机栈:在JVM运行过程中存储当前线程运行方法所需的数据,指令.返回地址 本 ...

  5. 一组.NET MAUI绘制的开源控件 - AlohaKit

    前言 今天大姚给大家分享一组.NET MAUI绘制的开源.免费(MIT License)UI控件库:AlohaKit. MAUI介绍 .NET MAUI是一个开源.免费(MIT License)的跨平 ...

  6. 云原生的 WebAssembly 能取代 Docker 吗?

    WebAssembly 是一个可移植.体积小.加载快并且兼容 Web 的全新格式.由于 WebAssembly 具有很高的安全性,可移植性,效率和轻量级功能,因此它是应用程序安全沙箱方案的理想选择.现 ...

  7. 云原生周刊:HashiCorp Vault 1.14 发布 | 2023.6.26

    开源项目推荐 Helmfile Helmfile 是一个开源工具,使用 Helm charts 简化复杂应用程序的部署.它提供了一种声明性的方式来定义 Kubernetes 资源的期望状态,并管理 H ...

  8. 使用 GitLab 账号登陆 KubeSphere

    作者:李帅 介绍 KubeSphere 多租户是实际生产使用中非常需要的一个功能,该功能满足不同用户登陆 KubeSphere 平台的需求.比如开发,运维,测试都需要登陆 KubeSphere 平台, ...

  9. Kubernetes 新型容器逃逸漏洞预警

    作者:米开朗基杨,KubeSphere 布道师,云原生重度感染者 2022 年 1 月 18 日,Linux 维护人员和供应商在 Linux 内核(5.1-rc1+)文件系统上下文功能的 legacy ...

  10. python-mongodb简单封装

    #!/usr/bin/python # -*- coding: UTF-8 -*- '''@auther :mr.qin @IDE:pycharm''' import pymongo from too ...