1. 为什么要在Java项目里直接调用Kettle
1.1 从拖拽工具到代码内嵌,只差一个需求
先说说我自己的经历。之前做数据平台,业务方丢过来一个需求:每天凌晨2点把业务库的增量数据同步到数仓,这个活儿我用Kettle的图形界面Spoon拖了十几分钟就搞定了一个转换。但没过两个月,需求就变了——要求按天分页拉取第三方API的数据,同步完成后还要调用通知接口、要能断点续传、要失败自动重试。这时候再靠运维去点Spoon按钮已经完全不现实了,最靠谱的方案就是把这套ETL逻辑直接嵌进Java应用里,由代码统一调度、统一监控。
Kettle这个工具,全称是Pentaho Data Integration(也就是Data-Integration),很多老工程师习惯直接叫它Kettle。它的核心能力就是ETL:从各种数据源抽取数据,做清洗、转换、映射,再加载到目标库。社区版免费开源,再加上本身支持的数据源极多,所以在国内很多企业的离线数据交换场景里,它比一堆商业ETL工具都常见。
但大多数教程都停留在“教你用Spoon拖拽组件”的阶段,真正讲Java嵌入式开发的非常少。我刚开始也是两眼一抹黑,翻了半天文档才跑通第一个由Java代码启动的转换。这篇文章把我的完整路径整理出来,从环境搭建到核心对象模型,再到参数传递、Job调度、API分页读取,最后把我踩过的几个坑一起讲清楚。
1.2 哪些场景必须走Java集成而不是纯Spoon
先拉一张表,大家对照自己的项目看看属于哪种情况:
| 场景 | 纯Spoon的痛点 | 用Java集成的优势 |
|---|---|---|
| 定时任务、复杂调度 | Spoon依赖人工触发,或要额外部署调度服务 | 直接嵌入Spring Boot,用Quartz或XXL-Job统一调度 |
| 需要和业务系统联动 | 无法感知业务状态,无法动态传参 | 代码中可以实时设置参数、读取执行结果 |
| 分页循环拉取接口 | 图形界面做循环和条件判断非常别扭 | 用Java代码控制循环,Kettle专注做取数和入库 |
| 多租户/多数据源 | 每来一个新数据源就要重新拖一个转换 | 同一个转换模板,运行时动态替换连接和参数 |
| 需要集成到已有平台 | 流程割裂,维护成本高 | ETL逻辑作为应用的一部分,随应用发布、监控 |
也就是说,如果你的项目只是临时做一次数据迁移,用完就丢,那直接用Spoon拖一版就行。但如果ETL是平台能力的一部分,要长期运行、要响应业务变化,那用Java调用Kettle基本是绕不开的。
1.3 Kettle在Java中的核心对象模型
在写第一行代码之前,先搞懂Kettle运行时涉及的核心对象。这个理解了,后面的代码就是顺水推舟的事。
KettleEnvironment:全局环境入口,任何Kettle操作之前必须先调用KettleEnvironment.init(),它会加载Kettle的插件、变量、资源库配置等。你可以把它类比成Spring容器启动时的ApplicationContext初始化。TransMeta:转换的元数据定义,对应磁盘上的.ktr文件。它描述了这个转换有哪些步骤、步骤之间怎么连接、每个步骤的配置是什么。TransMeta本身不执行逻辑,只描述结构。Trans:真正执行转换的运行时对象。构造时需要传入TransMeta,然后调用execute()启动异步执行,调用waitUntilFinished()等待执行完成。JobMeta/Job:对应.kjb作业文件。Job是用来串联多个转换、执行Shell脚本、发送邮件等动作的编排器。JobMeta是元数据,Job是运行时。StepInterface:每个步骤的运行时实例,可以通过Trans拿到某个步骤的引用,进而读取该步骤的执行行数、错误行数等信息。Result:一个转换或作业执行后产生的结果对象,包含了成功/失败状态、处理行数、错误数、执行时间等指标。
有了上面这些概念,再看代码就不会懵了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建:版本匹配是最容易踩的坑
2.1 Java版本与Kettle版本怎么搭配
很多人在第一步就栽了,拿了最新版JDK 17甚至JDK 21直接跑Kettle,然后遇到各种莫名其妙的ClassNotFoundException或者编译警告。Kettle对Java版本的要求很严格,不是越新越好。
我梳理了一下常见版本的搭配关系:
| Kettle版本 | 推荐Java版本 | 备注 |
|---|---|---|
| Kettle 7.x | Java 7/8 | 老项目还在用,但官方维护已停止 |
| Kettle 8.x | Java 8 | 比较稳定,但功能较老 |
| Kettle 9.x | Java 8/11 | 目前社区版的主力,推荐Java 8或11 |
| Kettle 10.x | Java 11 | 新版本,对Java 8支持不好了 |
我自己现在用的是Kettle 9.4 + Java 11,实测下来是稳定的组合。如果你用的是Spring Boot 2.x,那Java 8或11本来就在范围内,不需要额外折腾。如果非要用Java 17跑Kettle 9.x,也不是完全不行,但后续很可能碰到IllegalAccessError这类模块化相关的报错,不建议新手这么做。
在IDEA的Project Structure里,记得把Project SDK和Modules的Language Level保持一致,否则会出现编译目标版本不匹配的问题。
2.2 Maven依赖引入的正确姿势
Kettle官方提供了Maven仓库,坐标是在org.pentaho.di下。一个最小的依赖引入方式是这样的:
xml复制<dependency>
<groupId>org.pentaho.di</groupId>
<artifactId>pdi-core</artifactId>
<version>9.4.0.0-343</version>
</dependency>
但这里有个坑:pdi-core只是核心模块,很多插件(比如各种数据库方言、JSON组件、HTTP组件)是以插件形式存在的,默认不会被打进pdi-core里。所以在实际项目中,我建议把pdi-engine和核心依赖一起加进来:
xml复制<dependency>
<groupId>org.pentaho.di</groupId>
<artifactId>pdi-core</artifactId>
<version>9.4.0.0-343</version>
</dependency>
<dependency>
<groupId>org.pentaho.di</groupId>
<artifactId>pdi-engine</artifactId>
<version>9.4.0.0-343</version>
</dependency>
如果你的转换里用到了某个具体的数据库连接(比如达梦、PostgreSQL),记得把对应的JDBC驱动也一起加进来,后面我会专门讲达梦的坑。
2.3 下载安装与Spoon初体验
依赖引入之后,我强烈建议先下载一个完整的Kettle客户端,用图形界面把转换和Job画出来,再用Java去加载执行。原因很简单:纯代码写Kettle步骤配置极其繁琐,远不如Spoon拖拽直观,而且Spoon生成的.ktr文件本身就是最好的配置规范。
去官网下载Kettle 9.x的发布包,解压后进入目录,Windows运行Spoon.bat,Mac/Linux运行Spoon.sh。启动时如果内存不足,可以修改Spoon.bat里的PENTAHO_DI_JAVA_OPTIONS,把-Xmx调大,比如-Xmx2048m。
打开Spoon后先新建一个转换,从左侧面板拖入“表输入”和“表输出”两个步骤,连接它们,配置数据库连接,随便跑一个SELECT查询写入目标表。这个流程在Spoon里跑通之后,我们再把它放到Java代码里执行。这一步的目的是排除环境问题——如果在Spoon里都跑不通,那问题多半在驱动或网络,不是Java集成的问题。
3. 第一个Java加Kettle实战:从MySQL到PostgreSQL的转换
3.1 先用Spoon把转换流程画出来
我以一个最常见的场景为例:从MySQL数据库的student表读取学生数据,通过字段映射写入PostgreSQL的student_info表。原始表结构大概是:
sql复制CREATE TABLE student (
id INT PRIMARY KEY,
name VARCHAR(50),
score DECIMAL(5,2),
create_time DATETIME
);
目标表结构:
sql复制CREATE TABLE student_info (
student_id INT PRIMARY KEY,
student_name VARCHAR(50),
score DECIMAL(5,2),
sync_time TIMESTAMP
);
在Spoon里新建一个转换,按下面的步骤配置:
- 拖入“表输入”步骤,配置MySQL连接,SQL写
SELECT id, name, score, create_time FROM student。 - 拖入“字段选择”步骤,把
id改名为student_id,name改名为student_name,并保留score。 - 拖入“表输出”步骤,配置PostgreSQL连接,目标表选
student_info,勾选“批量插入”,“提交大小”设置为1000。 - 用“跳(Hop)”把三个步骤连起来:表输入 → 字段选择 → 表输出。
- 保存为
student_etl.ktr,放到项目的etl目录下。
注意,在“表输出”的“数据库字段”选项卡里,需要手动映射源字段到目标字段,并加上sync_time字段,值可以用SYSDATE函数生成,或者在表输入SQL里直接查NOW()。
3.2 Java代码里如何加载并执行转换
现在开始写Java代码。完整流程分成五步:初始化环境、加载元数据、创建转换实例、设置日志级别、执行并等待结果。
java复制import org.pentaho.di.core.KettleEnvironment;
import org.pentaho.di.core.logging.LogLevel;
import org.pentaho.di.trans.Trans;
import org.pentaho.di.trans.TransMeta;
public class KettleEtlRunner {
public static void main(String[] args) {
try {
// 1. 初始化Kettle环境
KettleEnvironment.init();
// 2. 加载转换元数据
TransMeta transMeta = new TransMeta("etl/student_etl.ktr");
// 3. 创建转换运行时
Trans trans = new Trans(transMeta);
// 4. 设置日志级别
trans.setLogLevel(LogLevel.BASIC);
// 5. 执行并等待完成
trans.execute(null);
trans.waitUntilFinished();
// 6. 判断执行结果
if (trans.getErrors() > 0) {
System.err.println("转换执行失败,错误数: " + trans.getErrors());
System.exit(1);
} else {
System.out.println("转换执行成功");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
KettleEnvironment.shutdown();
}
}
}
这段代码看着简单,但里面有几点容易忽略:
KettleEnvironment.init()必须且只能调用一次。如果Kettle被嵌在Spring Boot里,建议用一个@PostConstruct方法做初始化,或者用一个单例类持有,避免每次执行都重复初始化。new TransMeta("etl/student_etl.ktr")用的是相对路径,实际项目中建议用ClassPathResource或者绝对路径,避免打包成Jar后找不到文件。trans.execute(null)是异步执行,所以必须调用waitUntilFinished()阻塞等待。如果你不等待,主线程跑完了,转换可能还没开始,结果自然不对。- 判读是否成功不能只看异常,还要看
trans.getErrors(),有些步骤内部失败不至于抛异常,但错误数会大于0。
3.3 结果校验与日志输出
很多时候光知道“成功/失败”不够,还需要知道每个步骤处理了多少行。这时候可以获取特定步骤的运行时对象,读取行数指标:
java复制// 获取名为"表输入"的步骤实例
StepInterface inputStep = trans.getStepInterface("表输入", 0);
long linesRead = inputStep.getLinesRead();
long linesWritten = inputStep.getLinesWritten();
System.out.println("输入步骤读取行数: " + linesRead);
System.out.println("输入步骤写出行数: " + linesWritten);
其中第二个参数是副本编号(Copy),大多数步骤是单副本,传0就行。如果使用了“并行执行”且开启了多个副本数,就需要遍历所有副本:
java复制for (int i = 0; i < trans.getSteps().size(); i++) {
StepInterface step = trans.getStepInterface("表输入", i);
System.out.println("Copy " + i + " 读取: " + step.getLinesRead());
}
Kettle的日志输出也有讲究。trans.setLogLevel()可以设置日志级别,有ERROR、MINIMAL、BASIC、DETAILED、DEBUG、ROWLEVEL等。生产环境中建议用BASIC,既能看到关键信息,又不会刷屏。如果用ROWLEVEL,它会打印每一条行数据,数据量一大直接打爆日志文件。
4. 参数传递、动态SQL与API分页读取
4.1 三种参数方式:变量、命名参数、替换参数
实际项目中,几乎没有完全写死的转换。总得有动态的日期范围、动态的表名、动态的过滤条件。Kettle里传参有三种常见手段:
| 方式 | 脚本写法 | Java设置API | 使用场景 |
|---|---|---|---|
| Kettle变量 | ${VAR_NAME} |
trans.setVariable("VAR_NAME", "value") |
全局通用,生命周期长 |
| 命名参数 | ? 或 ${PARAM_NAME} |
trans.setParameterValue("PARAM_NAME", "value") |
转换级别的参数,最常用 |
| 环境变量 | %%VAR_NAME%% |
系统环境变量 | 机器级配置 |
用的最多的是前两种。在Spoon的表输入步骤里,SQL可以直接写:
sql复制SELECT id, name, score
FROM student
WHERE create_time >= '${startDate}'
AND create_time <= '${endDate}'
然后在Java代码中这么传参:
java复制Trans trans = new Trans(transMeta);
trans.setVariable("startDate", "2024-01-01");
trans.setVariable("endDate", "2024-01-31");
trans.execute(null);
trans.waitUntilFinished();
这里有个特别容易踩的坑:setVariable和setParameterValue不是一回事。setVariable设置的是变量,适合${}替换;setParameterValue设置的是命名参数,适合步骤中定义了参数后使用。如果你在Spoon里配的是命名参数,代码里却用setVariable设置,那个参数根本不会生效。我建议统一用setVariable,并且在Spoon里全部写成${参数名},这样代码侧逻辑更统一。
4.2 在Java代码中向转换传参的完整示例
再扩大一点场景:不光要传时间范围,还要传目标表名,甚至要动态切换数据源。表名无法直接通过SQL参数替换,因为JDBC的PreparedStatement是不允许把表名作为占位符的。Kettle里可以用变量拼接表名:
sql复制SELECT id, name, score
FROM student
WHERE create_time >= '${startDate}'
目标表名在“表输出”步骤中配置为${targetTable}。Java代码:
java复制trans.setVariable("startDate", "2024-01-01");
trans.setVariable("endDate", "2024-01-31");
trans.setVariable("targetTable", "student_tmp");
这里要注意一点:如果表名是变量拼接的,Kettle不会自动帮你做表名合法性校验,存在SQL注入风险。所以变量的值必须由可信来源控制,比如后端接口传参时要做白名单校验。
4.3 用Kettle实现API分页数据读取
热搜词里能看到很多人在搜“kettle用post组件获取api分页数据”“kettle循环api读取”,这块确实是刚需。
先给一个思路:Kettle本身提供“HTTP客户端”和“HTTP Post”组件,可以从接口取数据。配合“JSON输入”组件解析返回结果。但如果接口需要分页循环请求,纯图形化Spoon做起来非常别扭。
更好的方案是:用Java控制分页循环,Kettle只负责单页数据的抽取和入库。
具体做法是:
- 在Java里写一个
for循环,从第1页到第N页。 - 每页请求前,把页码和页大小通过
setVariable传给转换。 - 转换内部用“HTTP Post”组件请求API,带上
${pageNum}和${pageSize}参数。 - 拿到JSON后用“JSON输入”组件解析,最终写入目标表或文件。
核心代码:
java复制int pageNum = 1;
int pageSize = 100;
boolean hasNext = true;
while (hasNext) {
TransMeta meta = new TransMeta("etl/api_fetch.ktr");
Trans trans = new Trans(meta);
trans.setVariable("pageNum", String.valueOf(pageNum));
trans.setVariable("pageSize", String.valueOf(pageSize));
trans.execute(null);
trans.waitUntilFinished();
// 通过步骤读取输出的行数,判断是否还有下一页
StepInterface apiStep = trans.getStepInterface("输出", 0);
long written = apiStep.getLinesOutput();
if (written < pageSize) {
hasNext = false;
}
pageNum++;
}
这样做的最大好处是:分页逻辑在Java代码里一目了然,方便加日志、加断点续传、加失败重试;而Kettle只做它最擅长的事——取数据、解析、入库。记住一点:不要在Spoon里做太复杂的业务循环,那是Java的强项。
5. Job作业开发:多个转换串联与调度执行
5.1 Job里可以装哪些节点
单个转换只能完成一条链路的数据处理,但真实的ETL任务往往是一串动作:先清空目标表、再执行转换A、转换B、然后执行Shell脚本、最后发个通知。这些动作需要按顺序编排,并且要处理成功/失败分支,所以Kettle引入了Job。
一个Job中可以包含的节点类型非常丰富:
- 转换:执行指定的
.ktr文件 - 作业:嵌套执行另一个
.kjb文件 - Shell:执行操作系统命令或脚本
- SQL:直接执行一段SQL脚本(注意,这里不能带参数)
- 发送邮件:SMTP发通知
- 成功/失败:定义流程的结束状态
- 条件判断:根据字段值或变量走向不同分支
每个job项执行完毕后,可以配置“跳”的条件:当结果为真(成功)时走哪条线,为假(失败)时走哪条线。这是Kettle作业和转换最大的区别——转换里“跳”是数据流,作业里“跳”是控制流。
用Spoon新建一个Job,拖入“转换”节点,选择之前保存的student_etl.ktr,再拖入一个“SQL”节点执行清理脚本,然后从“转换”节点到“SQL”节点连一条线,条件为“成功”时连接。保存为student_job.kjb。这样一个“先抽数,再清理”的作业就搭好了。
5.2 Java加载并执行Job
加载和执行Job的代码跟Trans几乎是一个套路:
java复制import org.pentaho.di.core.KettleEnvironment;
import org.pentaho.di.job.Job;
import org.pentaho.di.job.JobMeta;
public class KettleJobRunner {
public static void main(String[] args) {
try {
KettleEnvironment.init();
JobMeta jobMeta = new JobMeta("etl/student_job.kjb", null);
Job job = new Job(null, jobMeta);
job.setVariable("startDate", "2024-01-01");
job.setVariable("endDate", "2024-01-31");
job.run();
job.waitUntilFinished();
if (job.getErrors() > 0) {
System.err.println("Job执行失败,错误数: " + job.getErrors());
} else {
System.out.println("Job执行成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
有个细节需要注意:new JobMeta("etl/student_job.kjb", null)的第二个参数是资源库对象,如果传null表示完全从文件系统加载。如果你后续想用Kettle的资源库(Repository)统一管理转换文件,这里就要传Repository对象。新手直接用文件系统最省事,把.ktr和.kjb都放在工程的资源目录下,用getResource()获取绝对路径即可。
5.3 在Java层面实现循环调度
Kettle自带一个Carte模块可以做分布式调度,但对大多数项目来说太重了。更轻量的做法是直接在你的应用里用Java的定时任务框架去触发上面的KettleJobRunner。
比如用Spring的@Scheduled:
java复制@Component
public class EtlScheduler {
@Scheduled(cron = "0 0 2 * * ?")
public void runDailyEtl() {
try {
KettleEnvironment.init();
JobMeta jobMeta = new JobMeta("etl/student_job.kjb", null);
Job job = new Job(null, jobMeta);
job.setVariable("startDate", LocalDate.now().minusDays(1).toString());
job.setVariable("endDate", LocalDate.now().toString());
job.run();
job.waitUntilFinished();
// 记录日志,做告警通知...
} catch (Exception e) {
// 异常处理
}
}
}
用Spring调度比Kettle自带的调度器好用的地方在于:你可以和公司的告警平台、任务管理系统无缝对接,Spring的@Scheduled支持cron表达式,也方便做分布式锁(比如配合Redis)防止多实例重复执行。
6. 踩坑实录:我在Kettle Java开发中遇到的5个典型问题
6.1 “源发行版17需要目标发行版17”警告与Lombok冲突
在JDK 17环境下编译Kettle项目,经常出现:
code复制java: 警告: 源发行版 17 需要目标发行版 17
这说明当前项目编译的source和target不一致。本质是maven-compiler-plugin没有正确配置。解决方式是在pom.xml中显式指定编译参数:
xml复制<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
如果你用的是JDK 17,Kettle跑不起来,可以先降到JDK 11。还有那个热搜里出现频率很高的:
code复制java: you aren't using a compiler supported by lombok, so lombok will not work
这个问题的根因是Lombok版本太旧,不支持较新的JDK。把依赖升级到1.18.30以上,或者在Maven的<annotationProcessorPaths>中指定新版Lombok。
6.2 OutOfMemoryError内存不足
热搜里的java: outofmemoryerror: insufficient memory有相当一部分就是在跑Kettle时出现的。Kettle处理大数据量时默认的JVM堆内存很容易被打满,尤其是一次读取几百万行的场景。
我的经验是三管齐下:
首先,调大JVM内存。如果是在Java代码里嵌入Kettle,在启动脚本的JAVA_OPTS中设置:
bash复制-Xms512m -Xmx2048m
如果是直接跑Spoon,修改Spoon.bat里的PENTAHO_DI_JAVA_OPTIONS变量。
其次,调整Kettle的“行集大小”。行集(RowSet)是步骤之间传输数据的缓冲区,默认大小是10000行。在Spoon的转换设置里可以改,也可以在Java代码里设置:
java复制trans.setSize(5000); // 设置行集大小为5000行
行集越大占用的内存越多,如果内存吃紧,调小反而更安全。
最后,在“表输出”步骤开启“批量插入”,把提交批次调小一点,比如500或1000。这样可以减少缓存的行数,降低内存压力。
6.3 达梦数据库连接配置
话题热度高还有一个原因:国产数据库达梦的适配。Kettle默认不自带达梦驱动,需要手动配置。
步骤是这样的:
- 从达梦官网下载JDBC驱动包(通常叫
DmJdbcDriver18.jar)。 - 把驱动jar放到Kettle的
lib目录下(Kettle 9.x是lib,老版本是libext/JDBC),重启Spoon。 - 在数据库连接里,驱动类填
dm.jdbc.driver.DmDriver,URL填jdbc:dm://IP:5236。 - 在Java项目里,同样要把这个驱动jar引入到依赖中,或者直接放进项目的
lib目录。
如果你把Kettle嵌在Java应用里,还需要注册数据库方言。Kettle使用方言来生成特定数据库的SQL,没有方言会导致部分组件无法正常工作。具体做法是在SimpleJavaTypeInference或Database中加载:
java复制DatabaseInterface databaseInterface = new DmDatabaseInterface();
DatabaseMeta dbMeta = new DatabaseMeta("dm_test", "DM", "JDBC", "127.0.0.1", "5236", "dbname", "username", "password");
Database db = new Database(dbMeta);
如果拿不到达梦的方言支持,退而求其次的做法是:在Spoon里把连接类型选为“Generic database”,手动填入驱动类名、URL、以及方言类。只要驱动类能加载,SQL能下发,大部分同步场景是可以跑的。
6.4 中文乱码问题
ETL最烦的就是数据跑到目标库变成问号。这个问题通常出现在两个环节。
第一是数据库连接URL。MySQL用JDBC连接时,URL里要加字符集参数:
code复制jdbc:mysql://127.0.0.1:3306/dbname?useUnicode=true&characterEncoding=UTF-8
第二是Kettle变量文件或SQL脚本文件的编码。如果.ktr文件是用Spoon在Windows下创建的,默认可能是GBK编码,而Java应用在Linux服务器上读取时按UTF-8解析,SQL里的中文条件就会乱。解决办法:在Spoon里设置“文件编码”为UTF-8,或者在Java代码里显式指定:
java复制System.setProperty("file.encoding", "UTF-8");
另外,在“表输入”步骤中,如果SQL查询条件有中文,建议把SQL写进变量,再通过Java传参,而不是直接写在.ktr文件里。这样能绕开文件编码导致的乱码问题。
6.5 API分页读取时变量不生效的陷阱
用trans.setVariable("pageNum", value)给转换传分页参数时,我遇到过一种情况:第一页正常,第二页开始变量还是第一页的值。排查了半天发现,问题出在“HTTP Post”步骤的请求体是“固定”的,它没有按变量动态替换。
解决方式是在Spoon里配置HTTP Post时,把请求体中的值写成${pageNum}和${pageSize},而不是在Post Body的右键菜单里手动写变量。同时要确认变量的作用域传递到了正确的步骤上。如果还不行,可以在Java代码里先读取变量验证:
java复制String actualPageNum = trans.getVariable("pageNum");
System.out.println("实际页码变量: " + actualPageNum);
如果这里为空,说明变量没有被正确传入转换,要检查TransMeta是否有“参数”定义。在Spoon中,转换本身有一个“参数”页签,需要先声明这个参数的名称。Java端设置的变量,只有和TransMeta中声明的参数名一致时,才会被填充到相关步骤。
7. 生产环境中的性能调优与经验建议
7.1 行集大小、提交批次与并发度
Kettle的性能优化有一套固定的套路。我总结下来,优先级从高到低是:
| 优化项 | 默认值 | 建议值 | 场景说明 |
|---|---|---|---|
| 行集大小 | 10000 | 5000-20000 | 数据量大但内存有限,调低;内存充足可调高 |
| 表输出提交批次 | 1000 | 500-5000 | 目标库写入瓶颈,批次太小性能差,太大内存高 |
| 步骤副本数 | 1 | 2-4 | 只有耗时的步骤(如表输入)才需要增加副本 |
| 日志级别 | BASIC | ERROR/BASIC | 生产环境不要开DETAILED以上 |
关于步骤副本数,在Spoon中右键步骤选择“改变步骤副本数”。开启并行后,数据会被分发到多个副本处理。Java代码里的trans.execute(null)会自动按照Meta中配置的副本数并行执行,不需要额外写并行代码。
但要注意:不是所有步骤都能随意开副本。比如“表输出”开多个副本会向目标库并发写入,如果你的目标库连接池没配好,反而会把数据库压垮。一般来说,在“表输入”侧开副本做并行读取,效果最明显。
7.2 日志治理与错误重试
嵌入到Java应用的Kettle,日志不能只打到控制台。我的做法是自己实现KettleLogLayout并把Kettle日志桥接到SLF4J:
java复制LogWriter logWriter = LogWriter.getInstance();
// 将Kettle日志桥接到自定义Appender
更简单的方案是:只在Java侧做日志记录,把Kettle的LogLevel设为ERROR,正常执行过程用应用日志描述进度:
java复制log.info("开始执行ETL转换,参数 startDate={}", startDate);
trans.execute(null);
trans.waitUntilFinished();
log.info("ETL转换结束,总行数={},错误数={}", totalRows, trans.getErrors());
错误重试的策略上,我建议只在Job级别做重试,不要在Trans级别做。因为Trans重跑会从源头把数据再读一遍,如果源端没有做幂等,容易产生重复数据。Job失败后重试,可以保证整个流程回到初始状态。用Spring Retry或者手写循环都行:
java复制int maxRetry = 3;
for (int i = 0; i < maxRetry; i++) {
try {
job.run();
job.waitUntilFinished();
if (job.getErrors() == 0) break;
} catch (Exception e) {
log.error("第{}次执行失败", i + 1, e);
}
}
7.3 我整理出的Kettle Java开发清单
最后分享一份自己每次做Kettle Java开发都会过的检查清单:
- [ ] Java版本是否在Kettle支持范围内(推荐JDK 8或11)
- [ ] Maven中是否加入了
pdi-core和pdi-engine - [ ] 目标数据库驱动是否在依赖中,连接URL是否带字符集参数
- [ ]
.ktr和.kjb文件是否用UTF-8编码保存 - [ ]
KettleEnvironment.init()是否只调用了一次 - [ ] 变量名与Spoon中定义的参数名是否完全一致
- [ ] 执行后是否调用了
waitUntilFinished()并检查了getErrors() - [ ] 生产环境日志级别是否设置为
BASIC或更低 - [ ] 大批量数据是否开启了批量插入,行集大小是否合理
按照这个流程走下来,Kettle Java开发的基本骨架就搭牢固了。剩下的就是在实际业务中不断填充细节,碰到新坑再补充排查经验。
