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.commit 和 auto.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的更多相关文章
- 《从0到1学习Flink》—— Flink 写入数据到 Kafka
前言 之前文章 <从0到1学习Flink>-- Flink 写入数据到 ElasticSearch 写了如何将 Kafka 中的数据存储到 ElasticSearch 中,里面其实就已经用 ...
- Flink实战(八) - Streaming Connectors 编程
1 概览 1.1 预定义的源和接收器 Flink内置了一些基本数据源和接收器,并且始终可用.该预定义的数据源包括文件,目录和插socket,并从集合和迭代器摄取数据.该预定义的数据接收器支持写入文件和 ...
- Flink实战| Flink+Redis实时防刷接口作弊
随着人口红利的慢慢削减,互联网产品的厮杀愈加激烈,大家开始看好下沉市场的潜力,拼多多,趣头条等厂商通过拉新奖励,购物优惠等政策率先抢占用户,壮大起来.其他各厂商也紧随其后,纷纷推出自己产品的极速版,如 ...
- 《大数据实时计算引擎 Flink 实战与性能优化》新专栏
基于 Flink 1.9 讲解的专栏,涉及入门.概念.原理.实战.性能调优.系统案例的讲解. 专栏介绍 扫码下面专栏二维码可以订阅该专栏 首发地址:http://www.54tianzhisheng. ...
- flink 根据时间消费kafka
经常遇到这样的场景,13点-14点的时候flink程序发生了故障,或者集群崩溃,导致实时程序挂掉1小时,程序恢复的时候想把程序倒回13点或者更前,重新消费kafka中的数据. 下面的代码就是根据指定时 ...
- flink学习笔记-flink实战
说明:本文为<Flink大数据项目实战>学习笔记,想通过视频系统学习Flink这个最火爆的大数据计算框架的同学,推荐学习课程: Flink大数据项目实战:http://t.cn/EJtKh ...
- Flink实战(六) - Table API & SQL编程
1 意义 1.1 分层的 APIs & 抽象层次 Flink提供三层API. 每个API在简洁性和表达性之间提供不同的权衡,并针对不同的用例. 而且Flink提供不同级别的抽象来开发流/批处理 ...
- Flink 实战:如何解决生产环境中的技术难题?
大数据作为未来技术的基石已成为国家基础性战略资源,挖掘数据无穷潜力,将算力推至极致是整个社会面临的挑战与难题. Apache Flink 作为业界公认为最好的流计算引擎,不仅仅局限于做流处理,而是一套 ...
- Spark Streaming,Flink,Storm,Kafka Streams,Samza:如何选择流处理框架
根据最新的统计显示,仅在过去的两年中,当今世界上90%的数据都是在新产生的,每天创建2.5万亿字节的数据,并且随着新设备,传感器和技术的出现,数据增长速度可能会进一步加快. 从技术上讲,这意味着我们的 ...
- Elasticsearch数据库优化实战:让你的ES飞起来
摘要:ES已经成为了全能型的数据产品,在很多领域越来越受欢迎,本文旨在从数据库领域分析ES的使用. 本文分享自华为云社区<Elasticsearch数据库加速实践>,原文作者:css_bl ...
随机推荐
- 组合逻辑环(Combinational Logic Loop)
组合逻辑电路 组合逻辑电路是数字电子学中一类基本的电路类型,它由一系列逻辑门组成,用于实现特定的逻辑功能.与时序逻辑电路不同,组合逻辑电路的输出完全取决于当前的输入信号,而不受之前输入的影响.换句话说 ...
- CSEC:香港城市大学提出SOTA曝光矫正算法 | CVPR 2024
在光照条件不佳下捕获的图像可能同时包含过曝和欠曝.目前的方法主要集中在调整图像亮度上,这可能会加剧欠曝区域的色调失真,并且无法恢复过曝区域的准确颜色.论文提出通过学习估计和校正这种色调偏移,来增强既有 ...
- Redis实战-session共享之修改登录拦截器
在上一篇中Redis实战之session共享,我们知道了通过Redis实现session共享了,那么token怎么续命呢?怎么刷新用户呢?本来咱们就通过拦截器来实现这两个功能. 登录拦截器优化: 凯哥 ...
- 忘记 mysql 8.0 root 密码 怎么修改
本文copy自 Centos7重置Mysql 8.0.1 root 密码 问题产生背景: 安装完 最新版的 mysql8.0.1后忘记了密码,向重置root密码:找了网上好多资料都不尽相同,根据自己的 ...
- ChatGPT正式登陆iOS平台
6天前,ChatGPT在美区App Store中上架了官方App,累计下载量已经突破 50 万次,OpenAI 的 ChatGPT 应用在上架之后,其热度远超必应聊天等聊天机器人,以及其它使用 GPT ...
- 声明式 Shadow DOM:简化 Web 组件开发的新工具
在现代 Web 开发中,Web 组件已经成为创建模块化.可复用 UI 组件的标准工具.而 Shadow DOM 是 Web 组件技术的核心部分,它允许开发人员封装组件的内部结构和样式,避免组件的样式和 ...
- RxJS 系列 – 实战练习
前言 这篇主要是给一些简单例子, 从中体会 RxJS 在管理上的思路. Slide Down Effect with Dynamic Content 我在这篇 CSS & JS Effect ...
- BFS 马的遍历————洛谷p1443
马的遍历 题目描述 有一个 \(n \times m\) 的棋盘,在某个点 \((x, y)\) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步. 输入格式 输入只有一行四个整数,分别为 ...
- IDEA如何自动导入依赖的jar包
前言 我们在使用IDEA开发时,会引入第三方的jar包,这些第三方的jar包使我们可以快速的使用别人开发好的功能,而不用重复造轮子了. 这大大提高了我们的开发效率. 但是,有时候我们一下子需要导入太多 ...
- Nacos 配置加密
Nacos 配置加密 nacos配置加密官网 官网介绍太简单,而且GitHub 网络受限,随缘访问.Gitee 发现有镜像仓库,同步的最新版本 Gitee nacos 镜像仓库 但是官网中提到的加密插 ...