2. Flink消费Kafka的核心代码与参数配置
2.1 Maven依赖与连接器选择
Flink消费Kafka需要引入连接器依赖,这一步选错版本后面全是坑。以我当时用的Flink 1.17.1为例,需要在pom.xml中加入flink-connector-kafka依赖。这里要特别强调,从Flink 1.15开始,Kafka连接器从flink-connector-kafka-0.x系列改名并统一为flink-connector-kafka,artifactId不再带版本号后缀。网上很多老教程还在用flink-connector-kafka-0.11这种写法,用在新版本项目上直接编译不过。
我实际使用的依赖配置如下:
xml复制<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka</artifactId>
<version>1.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>1.17.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>1.17.1</version>
<scope>provided</scope>
</dependency>
这里有个重要的经验:flink-streaming-java和flink-clients这两个依赖在打包时要设置为provided,因为Flink集群环境里本身就有这些类,打进去反而会因为版本冲突引发各种奇怪问题。但flink-connector-kafka需要打成JAR包,因为默认的Flink发行版里并没有内置Kafka连接器。如果用了其他第三方依赖,比如连接数据库的JDBC驱动,同样需要打进去。
2.2 核心消费端代码实现
用DataStream API写一个消费Kafka、处理数据、再输出的任务,核心代码并不复杂。我以一个常见的实时订单数据清洗场景为例,代码结构大概是这样的:
java复制import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.util.Collector;
public class KafkaToConsoleJob {
public static void main(String[] args) throws Exception {
// 创建执行环境,开启Checkpoint
Configuration config = new Configuration();
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
env.enableCheckpointing(60000);
// 构建Kafka Source
KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers("kafka-1:9092,kafka-2:9092,kafka-3:9092")
.setTopics("ods_order_info")
.setGroupId("flink-order-consumer")
.setStartingOffsets(OffsetsInitializer.latest())
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
DataStream<String> stream = env.fromSource(source,
WatermarkStrategy.noWatermarks(), "kafka-order-source");
stream.process(new ProcessFunction<String, String>() {
@Override
public void processElement(String value, Context ctx, Collector<String> out) {
// 这里写对Kafka消息的具体处理逻辑
// 比如JSON解析、字段清洗、异常数据过滤
out.collect(value);
}
}).name("order-clean-transform")
.print().name("sink-to-console");
env.execute("kafka-order-consume-job");
}
}
看起来很简单,但这个代码里藏了几个在设计时需要想清楚的点。
第一是消费位点策略。我用的是OffsetsInitializer.latest(),也就是说新任务启动后只消费启动之后产生的新数据。对于离线补数或者需要回溯历史数据的场景,要改成earliest(),或者用specificOffsets指定某个时间点、某个偏移量作为起点。这个选择会影响你任务上线后能不能立即看到数据,我见过不少同事第一次跑任务发现没有数据输出,其实就是因为Kafka里没有新数据进来,而任务默认是从latest开始消费的,数据早就被消费完了,当然看不到东西。
第二是Checkpoint周期设置。我设置了60秒,这个值不是随便拍的,要考虑你的业务容忍度。Checkpoint越频繁,故障恢复时丢失的数据越少,但代价是状态存储和同步的开销增大了。对实时性要求不高的离线数仓链路,60秒完全可以;如果是对实时性要求高的场景,比如大促实时大屏,可以调到10到15秒。
第三是ProcessFunction的使用。这里用process而不是map,是因为process可以拿到Context,后续如果想接入侧输出流、定时器这类高级功能,就不用改代码结构了。我习惯把所有处理逻辑都放在ProcessFunction里,方便统一做数据质量校验和异常日志记录。
2.3 反序列化器与消费位点配置
Kafka里的消息默认是字节数组,Flink拿到之后要转成对象才能处理。最简单的方式是用SimpleStringSchema,把每条消息直接当成字符串处理。这种方式适合消息本来就是JSON字符串、后续用Flink SQL或者解析库去处理的场景。
如果消息是Avro或Protobuf序列化的,那就得写自定义反序列化器了。我举个例子,Kafka里存的是Avro格式的订单数据,那么需要实现KafkaRecordDeserializationSchema接口,在deserialize方法里调用Avro解码逻辑:
java复制public class OrderAvroDeserializationSchema implements KafkaRecordDeserializationSchema<OrderInfo> {
@Override
public void deserialize(ConsumerRecord<byte[], byte[]> record, Collector<OrderInfo> out) {
byte[] value = record.value();
// 用Avro Schema解析字节数组
OrderInfo order = avroDecoder.decode(value);
out.collect(order);
}
@Override
public TypeInformation<OrderInfo> getProducedType() {
return TypeInformation.of(OrderInfo.class);
}
}
这里有一个新手常犯的错:Kafka的key和value是分开的,如果业务消息里key带了一些维度信息、value才是真正的数据体,那么用setValueOnlyDeserializer是拿不到key的。这种情况需要同时设置setKeyDeserializer和setValueDeserializer,或者在自定义反序列化器里同时处理record.key()和record.value()。
消费位点的配置我单独提一提。setStartingOffsets本质上是配置了KafkaSourceReader在无状态启动时的初始读取策略。但需要注意,一旦Checkpoint生成成功,Flink会从状态里恢复消费位点,这时候设置earliest还是latest就都不起作用了,一切以保存的state为准。所以如果你改了消费逻辑、想重新消费历史数据,光改代码里的StartingOffsets是不够的,还要重置Kafka消费组的位点,或者换个新的groupId。
2.4 Checkpoint与状态后端配置
很多人跑Flink任务不重视Checkpoint,任务跑了几天突然挂了,恢复的时候要从Kafka最早或者最晚的位点重新消费,整个链路就乱了。我自己的经验是,只要是生产环境任务,Checkpoint必须开,而且要配套设置状态后端和清理策略。
状态后端我推荐用RocksDB。HashMapStateBackend虽然快,但状态一多就全放内存,容易OOM。RocksDB把状态存储在本地磁盘上,支持增量Checkpoint,对大规模状态更友好。配置代码如下:
java复制Configuration config = new Configuration();
config.setString("state.backend.type", "rocksdb");
config.setString("state.checkpoint-storage", "filesystem");
config.setString("state.checkpoints.dir", "hdfs:///flink/checkpoints");
// 也可以写本地路径 file:///data/flink/checkpoints
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
这里要说明一下,如果任务是通过DolphinScheduler的Flink任务节点提交的,那么很多配置可以直接在DolphinScheduler的任务参数里以-Dkey=value的形式传进去,效果等效于在代码里设置。比如我在DolphinScheduler的Flink任务节点里配过这样的选项:
code复制-Dstate.backend.type=rocksdb -Dstate.checkpoint-storage=filesystem -Dstate.checkpoints.dir=file:///data/flink/checkpoints -Dexecution.checkpointing.interval=60000
还有一个容易忽略的地方是Checkpoint超时时间,默认是10分钟。如果Kafka消费速率慢、barrier对齐时间长,Checkpoint很容易超时失败。我一般会调大一点:-Dexecution.checkpointing.timeout=300000,把超时放宽到5分钟。如果连续失败多次(默认是1次),任务就会自动重启。在DolphinScheduler里看到任务反复重启,先查一下是不是Checkpoint持续失败导致的,日志里会有很明显的提示。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
3. DolphinScheduler调度Flink任务实操
3.1 资源中心上传JAR包
DolphinScheduler里所有需要被任务节点引用的外部文件,比如Flink的JAR包、Shell脚本、SQL文件,都统一放在资源中心管理。这一步很有必要先做,如果直接把JAR包放在本地某个目录,然后让任务去读,DolphinScheduler在集群模式下执行任务的Worker节点可能根本找不到那个文件。
资源中心上传的具体流程是这样的:登录DolphinScheduler的Web界面,左侧菜单栏找到资源中心,点击文件管理,然后在右上角选择上传文件,把本地打好的Flink任务JAR包拖进去。上传成功后可以设置文件别名,这个名字在配置任务时会用到。
我后来在实际使用中发现,资源中心还支持创建目录,我现在习惯按业务线建目录,比如/ods、/dwd、/ads,每个目录下面放对应的JAR包,不然项目一多文件全堆在根目录,检索起来非常费劲。
需要注意一个细节:DolphinScheduler的Worker节点需要能访问到资源中心存储的底层文件系统。如果是单机部署,资源文件默认存在本地磁盘,不存在这个问题。但如果是多Worker节点部署,资源中心底层用的是HDFS或者S3的话,每个Worker节点都要有对应的存储访问权限,不然任务提交时找不到资源文件,日志会一直报FileNotFoundException。
3.2 创建工作流与Flink任务节点
DolphinScheduler的核心概念是工作流。一个工作流由一个或多个任务节点组成,任务节点之间可以配置依赖关系。我们要做的是创建一个新的工作流,然后在里面添加一个Flink任务节点。
操作路径是:项目管理,选择对应的项目,进入工作流定义,点击创建工作流。在画布左侧拖动Flink节点到画布区域,然后双击节点进入配置界面。
这里提一个重要心得:DolphinScheduler的Flink任务节点,本质上就是通过执行flink run命令来提交任务。理解了这一点,在排查问题的时候思路会清晰很多。你在本机命令行里怎么跑这个Flink任务,DolphinScheduler节点里就怎么填参数,区别只是它帮你封装成了可视化的表单。
3.3 任务参数配置详解
Flink任务节点的配置项比较多,我逐个说下我在生产环境实际使用的配置值,以及配置背后的原因。
程序类型,我选择Java,也就是通过反射调用主类。如果你的任务是Flink SQL任务,选择SQL类型,然后直接在下面写Flink SQL语句。如果任务是Python脚本,选择Python类型并指定PyFlink脚本路径。
主JAR包,选择之前在资源中心上传的JAR包。这里选的是资源文件,不是本地路径。选好之后,DolphinScheduler会自动拼接任务提交命令。
主类,填写你的Flink程序的入口类,也就是包含main方法的类全限定名,比如com.example.flink.KafkaToConsoleJob。这个类必须存在于你上传的JAR包中,否则提交时会报ClassNotFoundException。
部署方式,在DolphinScheduler 3.x版本里,有per-job和application两种可选。如果你用的是YARN集群模式提交,选择application模式更适合生产环境,因为这种模式下每个任务有独立的JobManager,资源隔离性好。如果是本地单机测试环境,又没有YARN集群,那这里可以不用纠结,DolphinScheduler默认会以local方式提交。
并行度参数,比如设置为4,表示这个任务的并行度为4,也就是Source、Transform、Sink各个算子默认会有4个并行子任务。这个参数要结合你的Kafka分区数来设置,Kafka有6个分区你并行度只设2,那大概率会有数据倾斜,某些分区消息处理不过来。一般建议并行度和Kafka分区数保持一致,或者略大一些。
自定义参数,这里可以添加任务级别的参数,代码里可以通过args或者Configuration读取。比如我把Kafka的topic和bootstrap.servers配置成自定义参数:
properties复制bootstrap.servers=kafka-1:9092,kafka-2:9092,kafka-3:9092
kafka.topic=ods_order_info
group.id=flink-order-consumer
这些参数最终会通过--key value的形式传入主类的args。在代码里用参数解析工具读取,好处是同一个JAR包可以通过在DolphinScheduler配置不同参数来适配不同的环境,不用频繁改代码重新打包。
Flink版本目录,填写你的Flink安装目录在DolphinScheduler所在机器上的路径。DolphinScheduler执行任务时会通过这个目录下的bin/flink命令来提交任务,要确保Worker节点上的这个路径真实存在,并且当前用户有执行权限。这个配置最容易出错,我踩过一次,路径多打了个斜杠,导致命令拼接错误,任务提交直接失败。
其他选项,比如任务优先级、失败重试次数、失败重试间隔、超时告警等,按项目实际情况设置就行。我一般把失败重试次数设为2,间隔1分钟,避免网络抖动导致的任务失败被直接判定为不健康。
3.4 调度设置与日志查看
任务节点配置完成后,保存工作流,然后需要做的两件事是设置定时调度和上线。
设置定时调度的工作流,会在指定时间点自动触发任务执行。DolphinScheduler的定时表达式是Cron风格的,比如每天凌晨2点跑一次:0 0 2 * * ? *。如果任务需要实时性地消费Kafka数据,那DolphinScheduler只负责把Flink任务提交起来,Flink起来后就是一个常驻流任务,定时调度只需执行一次即可。所以这里要清楚一个概念:DolphinScheduler的调度周期和Flink流任务本身的运行周期,是两个不同的维度。
工作流配置好之后,需要从离线状态切换到上线状态,定时调度才会生效。上线操作在DolphinScheduler里是点击工作流定义右侧的上线按钮。另外,工作流定义本身可以做定时调度配置,也可以在任务实例页面手动启动一次。
日志查看是排查问题的关键入口。当工作流触发后,进入工作流实例页面,点击正在运行的任务节点,会看到任务状态、提交日志和运行日志。提交日志里能看到DolphinScheduler实际执行的flink run命令长什么样,这对于排查命令拼接错误非常有帮助。运行日志则是Flink任务本体的输出,比如Print输出、异常堆栈都会打到这里。我排查问题时的习惯是:先看提交日志,确认命令无误;再看运行日志,定位业务层面的错误。
4. 常见问题与排查技巧实录
4.1 Kafka连接失败,一直报Connection refused
这个问题的表现是任务启动后,日志里反复出现org.apache.kafka.common.errors.TimeoutException。
大多数情况下,原因是Worker节点和Kafka集群之间的网络不通。Kafka的bootstrap.servers配置如果写的是内网域名,比如kafka-1:9092,需要确认所在机器的/etc/hosts里有没有这条解析记录。我遇到过好几次,本地连Kafka正常,换台机器就报超时,排查到最后都是hosts文件缺失。
另一个容易被忽略的点是Kafka的advertised.listeners配置。Kafka返回给客户端的不一定是客户端连接时用的那个地址,而是它在server.properties里配置的advertised地址。如果这个地址配置的是某个内网不可达的IP,即使bootstrap.servers写对了,客户端依然连接失败。排查方法很简单,在Worker节点上用kafka-console-consumer或者写个Java测试类连一下同一个地址,看能不能正常消费。
4.2 JAR包在DolphinScheduler中提交成功但任务秒失败
这种问题,看提交日志会显示Executing command...然后很快就结束,日志里如果再往下翻有No main class found或者ClassNotFoundException。
先检查主类名是不是写对了。注意类全限定名里不要带.class后缀,也不要有行尾空格。
再检查JAR包是否把主类编译进去了。在本地Linux服务器上执行:
bash复制jar tf flink-job.jar | grep KafkaToConsoleJob
如果看不到类文件,说明源码没编译进去,返回IDEA检查一下Artifacts配置。还有一个常见操作是把多个模块的项目打成JAR时,主类所在模块没有被包含在最终产物里。我建议用Maven的shade插件统一打包,它可以解决依赖冲突和主类清单问题。
xml复制<plugin>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>3.2.0</version>
</plugin>
等等,这段配置我写错了,应该是Maven shade插件的配置,注意别照抄。正确的shade插件配置是:
xml复制<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.flink.KafkaToConsoleJob</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
打出来的JAR包会自带MANIFEST.MF里面指定了Main-Class,运行时如果没有手动指定主类,Flink也能自动识别。
另外还需要检查Flink版本目录是否配置正确。DolphinScheduler在执行任务时会尝试调用这个目录下的bin/flink脚本,如果路径不存在或者Flink安装不完整,命令执行也会失败。验证方法是在Worker节点上手动执行一下:
bash复制/opt/flink/bin/flink --version
能正常输出版本号,说明路径没问题。
4.3 消费延迟高,Checkpoint超时
任务跑了一个星期后,开始出现Kafka消费延迟,数据越来越多,但是任务没有报错。这时候要重点检查两个地方。
第一是检查是否有反压。在Flink Dashboard或者日志里看任务算子状态,如果某个算子的输入缓冲持续打满,说明下游处理速度跟不上上游。常见原因是把需要聚合的操作写在了KeyedStream上但数据分布不均,或者某个算子里出现了耗时操作比如外部API调用。解决方案是增加并行度,或者把耗时操作改为异步化。具体到我们这套Kafka到Console的任务,如果只是打印输出,理论上不存在反压,但如果你下游接的是写入数据库的Sink,那就要检查数据库写入性能。
第二是调整Checkpoint参数。之前说的执行环境里Checkpoint超时默认是10分钟,如果你的状态很大、barrier对齐成本高,Checkpoint很容易超时。我一般会同时调大超时时间和间隔时间:
bash复制-Dexecution.checkpointing.timeout=300000
-Dexecution.checkpointing.interval=120000
这两者要配合,如果间隔只有30秒,但超时都5分钟了,很容易出现多个Checkpoint同时进行,资源消耗翻倍。我建议间隔至少是超时时间的一半以上。
4.4 Flink与Kafka版本兼容性导致运行时异常
遇到过任务在本地IDEA里跑得好好的,打包放到Linux服务器上提交就报错的情况。大多数是版本兼容问题,集中在flink-connector-kafka和kafka-clients的版本上。
Flink 1.17版本对应的Kafka连接器使用的是Kafka 3.2.0客户端,如果Kafka集群版本是0.11或者2.0这种老版本,协议上一般还能兼容,但如果集群是3.4以上,而连接器自带的kafka-clients版本过旧,可能因为消息格式版本字段无法解析导致失败。
排查方式是看异常信息。如果是UnknownTopicOrPartitionException,大概率是topic写错了;如果是UnsupportedVersionException,那就是协议版本不匹配,需要升级flink-connector-kafka到和Flink版本匹配的最新版。
我踩过最典型的一个坑,是在pom里手动引入了老版本的kafka-clients,结果覆盖了Flink自带的客户端导致连接失败。后来把kafka-clients依赖移除,统一用flink-connector-kafka传递的版本,问题解决。所以给个建议:Kafka相关依赖,尽量不要自己额外指定版本,让连接器自己管理。
5. 实操心得与优化建议
5.1 任务资源规划要提前想清楚
DolphinScheduler提交Flink任务时,默认可能只给JobManager分配很少的内存。我遇到过默认的JobManager堆内存只有几百兆的情况,Kafka消费端一旦堆积大量数据,很容易因为内存不足导致JobManager频繁GC甚至OOM。
我的建议是在DolphinScheduler的Flink任务参数里,显式指定内存配置。比如:
code复制-Djobmanager.memory.process.size=1024mb
-Dtaskmanager.memory.process.size=2048mb
-Dtaskmanager.numberOfTaskSlots=4
这些配置可以直接作为flink run的-D参数传进去。注意单位要写清楚,mb或gb都行,但别省略。如果是在YARN模式,还需要考虑每个TaskManager的资源大小要和YARN的容器配置匹配,否则可能会被YARN拒绝分配。
另外,如果任务实际生产数据量很大,不要盲目把并行度调得很大。并行度增加,意味着更多的TaskManager启动,DolphinScheduler提交时间也会变长,Kafka消费的分区分布也可能被重新平衡,反而引入不必要的开销。
5.2 关于DolphinScheduler和Flink的日志联动
两套系统的日志是割裂的,这是排障时最头疼的部分。DolphinScheduler的运行日志页面能看到Flink提交后返回的Application ID或者Job ID,但Flink内部的算子日志,在DolphinScheduler界面上是看不到的。我一开始经常在这上面浪费很多时间。
后来我的做法是:在Flink任务代码里统一用log4j2打印关键日志,并在Flink的conf目录下配置日志输出到指定的文件,同时把日志输出级别调低。这样不管是DolphinScheduler的提交日志,还是Flink运行态的业务日志,都能在服务器上找到。查看日志的时候直接用:
bash复制tail -f /opt/flink/log/*.log
如果任务提交到了YARN集群,那就需要用flink list和flink log命令去获取日志了,这个可以单独写一篇了。
5.3 额外强调的一点:消费组隔离
在开发环境调试的时候,多个任务不要共用同一个Kafka消费组。尤其当你用的是latest消费策略时,不同任务抢消费组,数据会被分摊到不同的Flink任务里,看起来就是任务没有输出或者输出少量数据。实际上数据被别人消费走了。
一个比较稳妥的做法是groupId按照项目名+任务名+环境名来命名,比如flink-order-clean-prod、flink-order-clean-dev。清理测试环境任务之前,也可以用kafka-consumer-groups命令行工具查看消费组积压情况:
bash复制kafka-consumer-groups.sh --bootstrap-server kafka-1:9092 --describe --group flink-order-clean-dev
看到LAG字段如果持续上涨,说明消费速度跟不上生产速度,这时候就算任务不报错,延迟问题也迟早会爆发。
我在实际使用DolphinScheduler调度Flink任务这套组合时,最大的体会是:DolphinScheduler的核心价值在于把调度入口统一了,不再需要自己写crontab、自己管理依赖,也不需要在每台机器上手动提交Flink任务。而Flink消费Kafka这个环节,决定任务能不能稳定跑下去的关键因素,始终是版本匹配、Checkpoint策略和消费组的规范管理。第一次搭的时候可能确实有点繁琐,但把流程走通一次、把问题记录成一份自己的速查表之后,后面再复制新任务就非常快了。希望这篇内容能帮你把这个链路跑顺。
