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. 使用 crontab 设置 Homebrew 自动更新

    本人有强迫症,希望自己电脑上安装的软件永远是最新的.App Store 有自动更新功能,然而 Homebrew 则没有自动更新的选项.每次手动更新的话时间长了又感觉麻烦.后来发现可以使用 cronta ...

  3. CMake构建学习笔记11-minizip库的构建

    准确来说,minizip其实是zlib提供的辅助工具,位于zlib库的contrib文件夹内.minizip提供了更为高级一点的接口,能直接操作文件进行压缩.不过,有点麻烦的是这个工具并没有提供CMa ...

  4. qumu虚拟机启动后无法远程连接

    通过 virsh 在启动 qemu 虚拟机,可以通过 VNC 访问虚拟机,但无法通过设置的 SSH 的外部映射端口登录.首先在宿主机上查看虚拟机(csv\tpm\name)的网络配置,可以看到端口映射 ...

  5. ARM汇编: B、BL 与R14(LR)、R15(PC)

    1. b与bl指令的作用是什么? b与bl指令的作用:实现程序跳转,也就是调用子程序. 2. b与bl指令的区别是什么? b与bl指令的区别: b指令:简单的程序跳转,跳转到到目标标号处执行. bl指 ...

  6. LeetCode 407. 接雨水 II (优先队列)

    参考 优先队列的思路解决接雨水II,逐行解释 从最外圈开始不断向内遍历,如果内部的高度小于外部的高度,则证明该位置可以蓄水,否则不能,水会顺着该外圈流出去. 每次都处理外圈高度最小的那个位置 a,遍历 ...

  7. Android dex、odex、oat、vdex、art区别

    1.dex java程序编译成class后,dx工具将所有class文件合成一个dex文件,dex文件是jar文件大小的50%左右. 2.odex(Android5.0之前)全称:Optimized ...

  8. CAS存在的问题及在Java中的解决方式

    CAS 介绍 CAS 可以保证对共享变量操作的原子性 CAS全称Compare And Swap,比较与交换,是乐观锁的主要实现方式.CAS在不使用锁的情况下实现多线程之间的变量同步.Reentran ...

  9. Java日期时间API系列36-----Jdk8中java.time包中的新的日期时间API类应用,使用LocalTime计算十二时辰。

    十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰.二十四小时和十二时辰对照表:     时辰 时间 24时制 子时 深夜 11:00 - 凌晨 01:00 23:00 - 01 : ...

  10. 2022年7月中国数据库排行榜:墨天轮榜单榜眼易主,PolarDB得分涨幅最大

    信创元年,后起之秀,大有可为.2022年7月的 墨天轮中国数据库流行度排行榜 风起云涌,本月排行榜共有232个数据库参与排名,相比上月,新增 ShuangzhaoDB 数据库.榜单前十名的唯一变化是达 ...