第一次把Flink作业提交到Yarn上跑Kafka数据的时候,我是在Linux服务器上手动敲的 nohup flink run ...。当时任务确实能拉数据,可第二天早上它挂了没人发现,业务那边已经在群里问“为什么实时数仓没更新”,我还在登录服务器找日志。后来我把这套“DolphinScheduler调度 + Flink消费Kafka”的流程全部收拢到调度系统里,启动、查重、失败提示都有了统一入口,才算真正能用。这篇文章就把整套落地过程写清楚,包括环境选型、Flink作业怎么写、DolphinScheduler工作流怎么配置,以及那些只有跑过才会遇到的坑。适合正在做实时数据同步、实时数仓,或者刚接触这套技术组合的同学参考。
1. 先想清楚:Flink消费Kafka为什么要放到DolphinScheduler里调度
1.1 一个最典型的实时链路长什么样
先说场景。最常见的需求是:业务日志、埋点数据、订单变更落到Kafka某个topic,实时任务负责把这批数据消费出来,经过简单清洗后写入下游,比如Doris、ClickHouse、HBase,或者先转成结构化消息再推给另一个Kafka topic。
这时Flink就是一个标准的数据管道:
Kafka Topic -> Flink Source -> 业务逻辑转换 -> Sink
这样的任务通常7x24小时运行,不像离线任务跑完就退出。所以你的核心诉求并不是“让DolphinScheduler每天跑一次”,而是让调度系统按你设定的频率检查“这个任务还在不在”,不在就重新拉起来,同时在拉起的时候把参数、日志、运行状态管理起来。
我接触过不少项目,一开始都是开发人员自己写个shell,或者手动执行Flink命令。短时间没什么问题,一旦上了多个任务,管理就乱。谁提交的?跑了多久?失败之后有没有通知?对应哪个业务方?全靠脑子记,不靠谱。
1.2 手动提交方式的问题出在哪
手动在Linux服务器上执行Flink任务,最大的问题不是“不会写命令”,而是缺少状态边界。你用 nohup 启动一个Flink任务,进程是挂在操作系统上的;如果提交到Yarn,任务又归Yarn管。一旦机器重启、内存不足触发OOM、或者Yarn把容器回收,你没有一个统一的地方能看到“这个任务该不该在跑”。
DolphinScheduler解决的是任务编排、定时触发、参数透传、失败重跑和告警通知。它能让你把“提交Flink任务”这个动作变成一条工作流里的一个节点,后续还可以在同一个工作流里接数据质量检查、Kafka Topic创建、下游表初始化等步骤。这比写一堆无人维护的shell脚本强很多。
“用DolphinScheduler启动Flink消费Kafka”这个标题,本质上讲的是一个任务托管问题:让Flink任务依照调度规则稳定运行,并把运行过程从“黑盒”变成“可查看、可恢复、可告警”。
1.3 这篇文章里的目标架构
为了便于复现,我以一个相对简单但完整的链路为例:
- Kafka集群有一个topic,名字叫
ods_user_action,里面存的是JSON格式的用户行为日志。 - Flink作业只做两件事:从Kafka消费消息,并把每条消息原样打印到TaskManager日志中。
- DolphinScheduler通过一个定期调度的工作流,确保这个Flink作业在Yarn上正常运行,如果检测到任务不在,就自动重新提交。
这套架构跑通之后,你把打印改成“解析JSON后写入Doris/HBase”就是一个可用的实时同步管道。所以本文的重点不是业务处理逻辑,而是“怎么让整个链路稳定地跑起来”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Linux部署里的版本匹配与跑通前置检查
2.1 版本怎么搭不会埋雷
DolphinScheduler、Flink、Kafka三者之间的版本兼容性,是很多人上线第一天就会踩的雷。我的经验是:不要追求每个组件都用最新版,而是选一组已经被大量生产环境验证过的组合。
下表是我实际使用过、也推荐新手直接照搬的版本搭配:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| 操作系统 | CentOS 7.9 / Ubuntu 20.04 LTS | Linux环境,生产多用CentOS系 |
| Java | JDK 1.8(或JDK 11) | Flink 1.17建议JDK 11,但JDK 8也能跑 |
| DolphinScheduler | 3.1.x | 3.x版本界面和任务类型都比2.x完善 |
| Flink | 1.17.x | FLink 1.15以上KafkaSource是新API,简单 |
| Kafka | 2.8.x 或 3.2.x | Kafka服务端向下兼容旧客户端,但要匹配认证方式 |
| Hadoop/Yarn | 3.x | 如果Flink提交到Yarn,必须有Hadoop环境 |
Flink和Kafka连接器的版本要特别强调:如果你用Flink 1.15之前的版本,Kafka连接器artifactId通常带Scala后缀,例如 flink-connector-kafka_2.12,API也是老的 FlinkKafkaConsumer。Flink 1.15之后推荐用新的 KafkaSource 连机器,artifactId不带Scala后缀,直接叫 flink-connector-kafka,写起来干净很多。
不要自己单独去调pom里的 kafka-clients 版本。Flink连接器打包时已经带了对应版本的Kafka客户端,你手动升级 kafka-clients,表面上看能连上,实际运行时经常出现 NoSuchMethodError 之类的问题,查起来非常痛苦。Kafka服务端如果是2.8或3.x,Flink 1.17自带的客户端足够兼容,没有必要乱升。
2.2 Worker机器上必须有的运行环境
DolphinScheduler有Master和Worker两种核心角色。真正执行Flink任务的是Worker节点,所以Flink环境、Hadoop环境必须预先部署在所有可能执行任务的Worker机器上,只部署在DolphinScheduler服务端所在的机器是没用的。
我在 /etc/profile.d/flink-env.sh 里配置了这些内容:
bash复制export JAVA_HOME=/usr/local/jdk1.8
export FLINK_HOME=/opt/flink
export PATH=$PATH:$JAVA_HOME/bin:$FLINK_HOME/bin
export HADOOP_CONF_DIR=/etc/hadoop/conf
export HADOOP_CLASSPATH=$(hadoop classpath)
这里最容易漏的是 HADOOP_CLASSPATH。新版Flink已经不把Hadoop相关依赖打包进发行版了,提交到Yarn时如果CLASSPATH里找不到Yarn相关的类,任务会在启动阶段直接失败,报 NoClassDefFoundError 或各种和 org.apache.hadoop 有关的异常。
配置好之后,先别急着到DolphinScheduler里操作。直接在Worker机器上手动执行一条最简单的Flink命令,确认能通:
bash复制flink run -m yarn-cluster -p 1 /opt/flink/examples/streaming/SocketWindowWordCount.jar --port 9000
这条命令如果能在Yarn上正常拉起一个作业,说明Worker节点的Flink、Hadoop、Yarn配置都OK。如果这一步就报错,不要往后推,先把环境问题解决,否则后面所有问题都会混在一起。
2.3 租户和Linux用户映射问题
DolphinScheduler里有个概念叫“租户”,租户编码通常对应一个Linux操作系统用户。创建租户时如果你选了 flink 作为系统用户,那么所有Worker机器上都必须存在这个用户。
为什么这么强调?因为DS执行Shell任务时,会以这个操作系统用户的身份去执行命令。如果Worker机器上没有这个用户,任务一提交就会报用户不存在或者权限拒绝。我第一次部署就吃过这个亏:一台Worker有flink用户,另一台没建,结果DS把任务随机分配到第二台,怎么看日志都是权限问题。
还需要保证这个Linux用户对Flink日志目录、HDFS临时目录、Checkpoint目录有读写权限。比如HDFS的 /flink/checkpoints 目录,需要先创建并授权:
bash复制hdfs dfs -mkdir -p /flink/checkpoints
hdfs dfs -chown -R flink:hadoop /flink/checkpoints
这步不做,任务启动时看上去一切正常,等第一个Checkpoint触发时就会一直超时失败,最终导致任务失败重启,现象很迷惑。
3. 把Flink作业写好:KafkaSource、Checkpoint和认证参数一次讲清
3.1 用ParameterTool把控制权交给调度器
Flink作业要接入DolphinScheduler,一个很重要的设计原则是:不要在main方法里硬编码Kafka地址、topic、group.id这类运行参数。否则你每次换环境都要重新改代码、重新打包,调度系统也起不到灵活管理的作用。
我通常用Flink自带的 ParameterTool 来接收参数。示例代码如下:
java复制import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
public class KafkaFlinkDemo {
public static void main(String[] args) throws Exception {
ParameterTool params = ParameterTool.fromArgs(args);
String brokers = params.getRequired("bootstrap.servers");
String topic = params.getRequired("topic");
String groupId = params.getRequired("group.id");
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers(brokers)
.setTopics(topic)
.setGroupId(groupId)
.setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.LATEST))
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
DataStream<String> stream = env.fromSource(
source,
WatermarkStrategy.noWatermarks(),
"kafka-source-demo"
);
stream.print();
env.execute("Flink Kafka ODS Sync Job");
}
}
在DolphinScheduler里配置Shell脚本时,就可以通过 --bootstrap.servers、--topic、--group.id 把参数传进去,同一个jar包可以在多个任务间复用。这是“调度系统和业务代码解耦”的典型做法。
3.2 KafkaSource从哪个位置开始消费
setStartingOffsets 这个设置非常关键,它决定了一个全新的消费组从哪个位置开始读Kafka。目前常用的选项有几种:earliest、latest、committedOffsets。
我推荐使用:
java复制OffsetsInitializer.committedOffsets(OffsetResetStrategy.LATEST)
意思很简单:如果有已经提交的offset,从这个已提交位置继续消费;如果这个消费组从来没有提交过offset,则从latest(最新消息)开始消费。
有人图省事直接写 OffsetsInitializer.earliest(),结果每次任务重建消费组都从头开始读,消息越积越多,任务越跑越慢。还有人写 OffsetsInitializer.latest(),结果程序启动前的那段消息全部丢失。这两个都是生产上的典型事故。
3.3 Checkpoint不配置,任务重启就抓瞎
Flink为了保证状态一致性,会把消费位置作为状态的一部分保存下来。但前提是开启Checkpoint。如果不开Checkpoint,KafkaSource只能靠外部去提交offset,一旦任务重启,消费位置就很不可控。
我建议在作业代码里强制开启:
java复制env.enableCheckpointing(60_000);
CheckpointConfig cpConfig = env.getCheckpointConfig();
cpConfig.setCheckpointStorage("hdfs://nameservice/flink/checkpoints");
cpConfig.setMinPauseBetweenCheckpoints(30_000);
cpConfig.setCheckpointTimeout(120_000);
cpConfig.setExternalizedCheckpointCleanup(
CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION
);
这里有几个细节:
enableCheckpointing(60_000)表示每60秒做一次Checkpoint,按你的业务延迟要求调整。checkpointStorage配成HDFS路径,不能放在本地磁盘。因为Flink任务一旦被调度到另一台Worker,或者容器重启,本地文件就找不到了。RETAIN_ON_CANCELLATION保证任务被手动取消时,Checkpoint不会被自动删除。这样你还能用最近一次成功Checkpoint恢复任务,不至于白跑。
另一个容易混淆的点是:Kafka的 enable.auto.commit 配置。Flink官方连接器的Offset由Checkpoint管理,不应该依赖Kafka客户端自动提交,否则可能出现重复消费或者offset状态和Checkpoint不一致。所以生产代码里应该明确关闭它:
java复制properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
3.4 SASL/PLAIN或Kerberos下的常见参数
很多公司的Kafka不是无认证的,而是通过SASL_PLAINTEXT或Kerberos访问。这时光写 bootstrap.servers 是不够的,任务会一直报连接被关闭或者认证失败。
用Java代码方式,最少需要加这么几项:
java复制properties.setProperty("security.protocol", "SASL_PLAINTEXT");
properties.setProperty("sasl.mechanism", "PLAIN");
properties.setProperty("sasl.jaas.config",
"org.apache.kafka.common.security.plain.PlainLoginModule required " +
"username=\"realtime_user\" password=\"your_password\";");
如果你用的是Kerberos认证,则通常会遇到一个经典报错:
code复制No service name defined in either JAAS or Kafka config
这个报错的意思是:Kafka客户端能找到JAAS配置,但不知道Kafka broker服务端对应的service name是什么,它默认会去配置里找 sasl.kerberos.service.name,找不到就报错。解决办法是显式指定:
java复制properties.setProperty("sasl.kerberos.service.name",
