简介:Kettle 的 Web 化版本资源包,面向数据集成工程师、ETL 开发者及需要远程协作的数据团队,用于在浏览器中完成转换与作业的设计、编辑和运行,弥补传统 Spoon 桌面端在分布式协作和跨设备访问上的不足。压缩包共 1002 个文件、约 157.7MB,以 482 个 png 界面素材、284 个 jar 依赖库和 112 个 class 编译文件为主体,同时包含 svg/gif 图标、xul 界面描述、sh/bat 启动脚本以及 xml、properties 等配置与少量前端资源,构成较完整的 Web 应用目录。已有 1554 人学习,说明其在 webKettle 部署与二次研究场景中具有一定参考价值。包内保留 Web 端所需的依赖、页面资源与脚本,可用于搭建浏览器端 ETL 工作台,支持转换作业管理、多人协作编辑及 REST API 集成等实践,也便于读者研究 Kettle 从桌面工具向 Web 环境迁移时的工程结构、资源组织与运行依赖。
1. 三个 Kettle 版本、两台机器、一堆散落的 .ktr:web 版先解决的是这本糊涂账
数据组最典型的场面:同事 A 笔记本上装着 Kettle 8,服务器上跑着 Kettle 7,某个 .ktr 在 A 那里点一下就出数,挪到服务器报「找不到步骤插件」。再往后,谁改了哪个转换、昨天凌晨那批为什么少两万行、失败之后从哪一步重跑,全靠翻聊天记录。Kettle 的 web 版这类资源,解决的就是这本糊涂账:它把转换和作业的执行、定时、日志、重跑收拢到一个浏览器入口,而转换本身还是那套 .ktr / .kjb,不用重画。适合已经把 Kettle 用顺手、却被单人单机模式卡住的团队;也适合刚接手一堆历史脚本、需要先把现状盘清楚的人。它不替代 Spoon,替代的是散在各人电脑上的 pan.sh 和 crontab。
2. 拆开 Kettle Web 版:控制面上了浏览器,执行引擎还是原来那套
先划清一件事,不然容易抱着不切实际的期待。很多人一听「web 版」以为是浏览器里拖拽画转换,其实主流做法不是这样:转换的设计仍然在 Spoon 里完成,web 端接管的是提交、执行、调度、监控、日志、权限这一层。引擎侧调用的还是 kettle-core / kettle-engine 那套 Java API,跑的还是同一个 Trans / Job 对象。想明白这条边界,后面所有参数和坑都能对号入座。
2.1 Spoon 管设计,Web 版管执行:能力边界划在哪
Spoon 是设计器兼执行器,一个人一台机器的时候它够用;一旦涉及多人、多环境、定时、追溯,它的短板就全暴露出来了——转换文件靠拷贝传递、执行靠命令行、日志在控制台滚完就没了、失败重跑靠人肉判断。Web 版补的正是这几块。
| 能力 | Spoon 客户端 | Carte 独立服务 | Web 版资源包 |
|---|---|---|---|
| 画转换 / 作业 | 支持 | 不支持 | 不支持(上传现成文件) |
| 远程触发执行 | 需手工连 | 支持 HTTP 调用 | 页面点选 + 接口触发 |
| 定时调度 | 无 | 无 | 内置调度器 |
| 运行日志留存 | 控制台 | 文件 | 落库,可按批次查 |
| 多环境参数切换 | 手工改 | 环境变量 | 页面配置注入 |
| 失败重跑 | 人肉 | 手工 | 按批次重跑 |
从这张表就能看出选型逻辑:如果你的转换还在频繁改动、天天要调步骤,那主力工具仍然是 Spoon,web 端只当执行入口用;如果转换已经固化成「每天跑一遍」的流水线,那它就该待在这个框里。
2.2 三层结构:页面上的一次点击,底下走了哪几步
拆开看就三层,出问题的时候按层定位,比漫无目的地翻日志快得多。
页面层负责转换和作业的上传、参数填写、任务列表、运行记录查询。服务层是主体,一个 Java 服务,管任务元数据、调度触发、执行线程池、日志归集、失败重试。引擎层是 Kettle 本身,负责解析 .ktr / .kjb、建立数据库连接、按步骤跑数据流。三层之间是同步调用加异步执行的关系:页面发起请求后服务层把任务丢进线程池就返回,真正的执行在后台,进度靠任务 id 回查。
| 层 | 职责 | 常见实现 | 出问题时的典型症状 |
|---|---|---|---|
| 页面层 | 上传、参数、列表、记录 | 前端静态资源 + REST | 页面能开但列表空、上传报 413 |
| 服务层 | 调度、线程池、日志归集 | Spring Boot + 调度框架 | 任务状态一直「运行中」、调度不触发 |
| 引擎层 | 解析文件、连库、跑数据流 | kettle-core / kettle-engine | 报找不到插件、连不上库、行数不对 |
| 存储层 | 任务配置、运行记录、步骤日志 | 关系库 | 记录写不进去、中文乱码 |
定位口诀:页面打不开看服务层端口和静态资源;任务能建但不跑看调度;跑了没数据看引擎层;跑成功但查不到记录看存储层。这套分法看着朴素,真到半夜排障能省不少时间。
2.3 四种不适合使用它的场景,提前知道才不至于白费功夫
第一,转换逻辑还在高频迭代。页面改不了步骤,每改一次就得回 Spoon 重传,来回折腾的反而不如直接命令行。第二,只有一台机器、一天跑两三个转换、没有协作诉求——crontab 加 shell 脚本十分钟就能搭完,引入一套 web 服务是给自己找维护量。第三,需要实时或准实时同步。Kettle 的定位是批量,用它做分钟级 CDC 会很难受。第四,单次处理数据量特别大、又舍不得给内存。Kettle 的行集在内存里流转,几千万行的大表不走分片就是硬扛,web 端只是把这个问题放大了——它让并发变容易,也就让 OOM 变容易。
3. 本地跑起来:从建库到第一个转换出数
这一章按顺序走一遍,目标是让一个 .ktr 在浏览器里点一下能跑通、能看到日志。路径和库名我用了通用的写法,实际以你解压后 conf 目录里的配置项为准,字段名各版本会有出入,别硬套。
3.1 环境对齐:JDK、驱动、字符集,三样先定死
JDK 版本跟着 Kettle 引擎走,引擎是什么版本 compiled 的,服务就用对应大版本,别跨太多。数据库驱动单独放进 lib,别指望服务里自带的能用——MySQL 8 之后驱动类名换成了com.mysql.cj.jdbc.Driver,老包名已经废弃,配置文件里写错就是启动即报错。字符集在建库那一步就要定成 utf8mb4,后面再改要动表动连接串,成本翻几倍。
# 1. 建库,字符集一次定死 mysql -uroot -p -e "CREATE DATABASE kettle_web DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 2. 导入资源包里的建表脚本,任务表、调度表、日志表一般都在里面 mysql -uroot -p kettle_web < /opt/etl-web/sql/init.sql # 3. 核对时区,增量抽取最容易栽在这 mysql -uroot -p -e "SELECT @@global.time_zone, @@session.time_zone, NOW();" # 4. 把驱动丢进 lib,确认版本 ls -l /opt/etl-web/lib | grep -i mysql第 3 步别跳过。数据库时区和 JVM 时区不一致的时候,NOW()和 Kettle 里「获取系统信息」拿到的值会差几个小时,增量抽取的 where 条件就会漏数据,而且不报错,只是每天少几条,很难被发现。看到SYSTEM就顺手在连接串里显式写上serverTimezone=Asia/Shanghai。
3.2 配置文件与启动:几个参数决定它能不能长期跑
配置文件里真正需要改的没几项,其余保持默认。下面这份是常见形态,字段名对照你解压出来的那份改。
server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/kettle_web?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: kettle password: 换成你自己的 driver-class-name: com.mysql.cj.jdbc.Driver kettle: home: /opt/etl-web/data # KETTLE_HOME,插件和 kettle.properties 从这里找 repository: /opt/etl-web/ktr # .ktr / .kjb 存放根目录,页面上的路径都相对它 log-level: Basic # None / Error / Minimal / Basic / Detailed / Debug / Rowlevel pool-size: 4 # 执行线程池,见 4.3启动命令有三个参数必须带:
cd /opt/etl-web nohup java -Dfile.encoding=UTF-8 \ -Djava.awt.headless=true \ -Xms512m -Xmx2g \ -jar etl-web.jar --spring.config.location=./conf/application.yml \ > logs/stdout.log 2>&1 &-Dfile.encoding=UTF-8管中文路径和中文日志,缺了就是一堆问号;-Djava.awt.headless=true让引擎在没有显示设备的服务器上不去碰图形类,很多启动期报错都是漏了这个;-Xmx后面 4.3 会细说,先给 2g 起步。启动后先tail -f logs/stdout.log看有没有初始化异常,再开浏览器,别一上来就传转换。
3.3 第一次执行:加载 .ktr 并拿到真实结果
页面点「运行」背后干的事,用一段代码就能说清。这段也适合写进自己的调试类里,页面报错时直接跑一遍,能快速区分是文件问题、参数问题还是引擎问题。
// 1. 引擎全局初始化,整个进程只做一次,耗时几秒到十几秒 KettleEnvironment.init(); // 2. 每次执行都新建 TransMeta,不要静态缓存(原因见 5.5) TransMeta transMeta = new TransMeta("/opt/etl-web/ktr/ods_user_sync.ktr"); // 3. 注入变量,对应 ktr 里 ${DB_HOST} 这类占位 transMeta.setVariable("DB_HOST", "10.0.0.12"); transMeta.setVariable("WORK_DIR", "/data/etl/ods_user"); // 4. 命名参数必须在 execute 之前设,少一个直接抛异常 transMeta.setParameterValue("BATCH_DATE", "2024-06-01"); // 5. 执行 Trans trans = new Trans(transMeta); trans.setLogLevel(LogLevel.BASIC); trans.execute(null); // 传 null 表示无命令行参数 trans.waitUntilFinished(); // 阻塞直到结束 // 6. 判断结果:errors 为 0 不代表数据对,还要看写行数 if (trans.getErrors() > 0) { throw new RuntimeException("转换失败,错误数:" + trans.getErrors()); } long written = trans.getTransMeta().getStep("表输出").getStatus() == null ? -1 : -1; // 实际写行数从日志表或 TransListener 取更好 trans.cleanup(); // 释放连接和线程,漏了就是内存泄漏几个点分开说。KettleEnvironment.init()千万别放在每次请求里,它要扫插件目录、注册步骤类型,重复调用会让首次响应慢到十几秒,标准做法是挂在服务启动钩子里预热一次。
setVariable和setParameterValue是两套东西:变量是运行期文本替换,设不设都不影响加载;命名参数在 .ktr 里是有定义的,execute之前缺一个就报「参数未赋值」。写模板的时候,凡是批次日期这类每次必传的,用命名参数;凡是库地址这类环境相关的,用变量。
waitUntilFinished()会阻塞当前线程,所以它一定跑在执行线程池里,不能直接在调度线程上调。最后cleanup()要放在 finally 里,异常路径也要走到。
3.4 变量与参数:把写死的路径挪出去
拿到别人的 .ktr 第一件事不是跑,是搜一遍里面有没有写死的绝对路径和库名。改法是把它们全换成占位符,赋值收口到一处。
| 类型 | 定义位置 | 赋值时机 | 典型用途 | 缺了会怎样 |
|---|---|---|---|---|
| 命名参数 | .ktr 的「命名参数」页签 | execute 之前 | 批次日期、环境标识 | 直接抛异常,跑不起来 |
| 变量 | kettle.properties / 代码 setVariable | 运行期任意时刻 | 库地址、目录前缀 | 占位符原样输出,报文件找不到 |
| 上一步字段 | 数据流内部 | 步骤之间 | 行级处理 | 步骤配置报错 |
优先级从高到低大致是:代码或命令行传入 > 上游作业传入 > kettle.properties > 步骤里写死的默认值。排查「参数没生效」的时候按这个顺序倒着找,一般到第二层就能定位。
4. 调度与监控:把 crontab 那套搬到页面上之后,坑也跟着搬过来了
能手工跑了,下一步就是定时。这块看起来简单——填个 cron 表达式而已——实际上问题最集中,因为它同时牵扯调度框架、时区、并发控制和日志存储四件事。
4.1 调度模型:cron 表达式的段数、时区和并发
先记住一件事:Quartz 系的 cron 是六到七段,第一位是秒,跟 Linux crontab 的五段不是一回事。从 crontab 直接复制粘贴,最常见的后果是解析失败,其次是把「每天 2 点」写成「每 2 分钟」。
| 场景 | 正确写法 | 容易错在哪 |
|---|---|---|
| 每天凌晨 2 点 | 0 0 2 * * ? | 照抄 crontab 少一位,写成0 2 * * *解析报错 |
| 每 10 分钟 | 0 0/10 * * * ? | 写成*/10 * * * *,段数不够 |
| 每月 1 号 6 点 | 0 0 6 1 * ? | 日和周同时写具体值会被判冲突 |
| 工作日 8 点 | 0 0 8 ? * MON-FRI | 周字段写成1-5有的实现不认 |
调度触发只做一件事——把任务丢给执行池,自己立刻返回:
@DisallowConcurrentExecution // 同一任务不允许重叠执行,防上一批没跑完下一批又起来 public class KtrQuartzJob implements Job { @Override public void execute(JobExecutionContext ctx) { JobDataMap data = ctx.getMergedJobDataMap(); String ktrPath = data.getString("ktrPath"); String batchDate = data.getString("batchDate"); // 可支持 ${yyyy-MM-dd} 这类占位 String taskId = data.getString("taskId"); // 关键:不在调度线程里同步跑转换,交给独立执行池 EtlExecutors.submit(taskId, ktrPath, batchDate); } }为什么不在这里直接跑?调度框架自己的工作线程池通常就十几个,转换一跑半小时,几个任务就能把它占满,后面所有任务全部排队饿死,表现是「定时的都没触发」,实际上触发了、只是没线程可用。执行池和调度池分开,是这套结构里最值钱的一条经验。
4.2 日志采集:三张表撑起可追溯
运行记录这张表要自己建,字段不用多,但几个关键的不能省:
CREATE TABLE etl_run_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id VARCHAR(64) NOT NULL COMMENT '任务标识', ktr_name VARCHAR(128) NOT NULL COMMENT '转换文件名', batch_date VARCHAR(16) NOT NULL COMMENT '业务批次日期', status TINYINT NOT NULL COMMENT '0运行中 1成功 2失败', start_time DATETIME NOT NULL, end_time DATETIME NULL, read_rows BIGINT DEFAULT 0, write_rows BIGINT DEFAULT 0, error_rows BIGINT DEFAULT 0, err_msg TEXT NULL, KEY idx_ktr_date (ktr_name, batch_date), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status是重跑判断的依据:重跑前先查这个批次是不是已经成功,避免重复灌数;write_rows是判断「跑成功但没数据」的唯一抓手;err_msg存前 2000 字符就够,完整的还是落文件。引擎侧怎么拿到这些值:日志文本通过KettleLogStore.getAppender()挂一个 appender 抓,行数从步骤度量或转换结果里取。appender 用完必须 remove,否则每执行一次就多挂一个,见 5.5。
还有一类是步骤级日志表,用来定位「到底卡在哪个步骤」。它需要单独的 DDL,通常在初始化脚本里就有,没有的话手工建一次,然后在转换的日志配置里选上。日常排障顺序是:先看 run_log 判断成功失败,再看步骤日志定位到具体步骤,最后看详细日志里的 SQL。三层递进,比一上来就开 Debug 级别强。
4.3 并发、内存与资源参数:调大之前先算一笔账
Kettle 的数据流在内存里以行集形式流转,每个步骤之间都有一个缓冲区,默认一万行。一个二十步的转换、每行一 KB,光缓冲区就是两百 MB 级别,再乘并发数。所以资源参数不是越大越好,是先算峰值再定上限。
| 参数 | 起步值 | 调整依据 | 调大的代价 |
|---|---|---|---|
-Xmx | 2g | 单次峰值 × 并发数 + 30% 余量 | GC 停顿变长,反而拖慢 |
| 行集大小 | 10000 行 | 字段特别宽时降到 1000~2000 | 太小吞吐掉一半 |
| 执行池并发 | 2~4 | CPU 核数的一半左右 | 抢数据库连接,互相等锁 |
| 调度线程数 | 10 | 定时任务条数 | 和执行池混用会互相拖死 |
| 日志级别 | Basic | 排障时临时 Detailed | Rowlevel 会写爆磁盘 |
调并发之前先问一句:瓶颈在数据库还是在本机?如果目标是同一个库的同一张表,并发调大只会让锁等待变长,整体耗时反而上升。真正需要并发的场景是多个互不相干的源库同步,或者分片跑同一张表的不同区间。
5. 排查与避坑:五条踩过的记录,按现象—原因—解决写
这一章的五条,都是「照文档做完全没问题、真跑起来就出事」的类型。每条按现象、原因、解决三步写,方便直接对号。
5.1 启动即报图形类异常,或 NoClassDefFoundError 指向 swt
现象:服务起不来,日志里出现java.awt.HeadlessException,或者某个org.eclipse.swt开头的类找不到。
原因:资源包的 lib 里混进了 kettle-ui 这一层的图形依赖。Spoon 需要图形界面,引擎不需要,但依赖传递的时候容易一起被带进来。服务器上没有显示设备,一碰图形类就炸。
解决:打包时排除 swt 相关 jar,只保留 kettle-core、kettle-engine、kettle-dbdialect 和数据库驱动;启动参数补上-Djava.awt.headless=true。验证办法是启动后看日志里KettleEnvironment.init()有没有正常打印版本信息,打印出来就说明引擎层通了。
5.2 本地跑得好好的转换,服务器报找不到文件
现象:同一份 .ktr,在本地 Spoon 里跑完出数,传到服务器上执行报「Couldn't find file」或者输出到了一个莫名其妙的目录。
原因:两个。一是路径里带 Windows 反斜杠,Linux 下直接当文件名的一部分;二是用了相对路径,而相对路径的基准是进程工作目录,不是你以为的 ktr 所在目录。
解决:所有路径统一正斜杠,并用变量拼:${WORK_DIR}/ods_user/,WORK_DIR在启动时注入;启动脚本里先cd到固定目录再执行 java,这一条能消掉一半的路径问题;文本文件输入这类步骤,把「从字段获取文件名」打开,源目录用变量给,避免写死。
5.3 定时设了每天 2 点,结果没触发或者疯狂重复
现象:要么一整天没动静,要么两分钟跑一次,把库压得很难受。
原因:cron 段数不对是最常见的;其次容器时区是 UTC,页面上显示的「下次执行时间」按 UTC 算,跟业务时间差 8 小时;还有一种情况是服务部署了两个实例,两个调度器都在跑同一批任务。
解决:先在页面上看「下次触发时间」的回显,回显不对就是表达式或时区问题;容器里显式设TZ=Asia/Shanghai并在 JVM 参数里加-Duser.timezone=Asia/Shanghai;多实例部署时要么只让一个实例开调度,要么给任务加分布式锁,配合@DisallowConcurrentExecution双保险。
5.4 任务状态显示成功,但下游数据没更新
现象:运行记录里一片绿,业务方反馈表还是空的,或者昨天的数据。
原因:判定条件只看了错误数。Kettle 里「过滤记录」把全部行过滤掉、或者「表输入」查到零行,都不算错误,错误数依然是 0。另外日志表连接和业务库不是同一个,日志写到了另一个库里去。
解决:判定条件改成「错误数为 0 且关键步骤写行数大于 0」,写行数为 0 直接标失败并告警;在转换末尾加一个校验步骤,把实际写入行数跟期望行数比一下;确认日志表连接的库和页面上配的是同一个,字符集一致。
5.5 跑三五天内存缓慢上涨,最后 OOM
现象:刚重启一切正常,跑几天越来越慢,最终堆溢出,重启后又好了,循环往复。
原因:三个常见泄漏点。一是 TransMeta 被静态缓存后又调setVariable,变量会挂在这个实例上,后面每次执行都带着上次的值;二是KettleLogStore.getAppender()每执行一次加一个 appender,从不 remove,日志全积在内存里;三是Trans结束后没调cleanup(),连接和线程没释放。
解决:缓存的是 .ktr 文件内容或路径,每次执行new TransMeta(...),宁可多花几十毫秒解析;appender 在用完之后finally里 remove;cleanup()一律放 finally。做完这三条,再加个堆内存监控,跑一周看曲线是不是平的,平了就说明真修好了。
6. 进阶:把转换压成模板,上线前过一遍自检表
到这一步服务能跑了,接下来决定它好不好用的是模板化程度。我的习惯是:一个业务域只留一份 .ktr 作为模板,环境差异全部收敛到五个变量里,新需求复制一份改 SQL 就行,不再从零画。
| 变量 / 参数 | 含义 | 谁赋值 | 备注 |
|---|---|---|---|
ETL_ENV | dev / test / prod | 页面按环境配 | 用来切库、切目录 |
DB_HOSTDB_PORT | 源库地址 | 页面注入 | 禁止写进步骤 |
DB_USERDB_PWD | 源库账号 | 配置中心或页面 | 别硬编码在 ktr 里 |
WORK_DIR | 文件落盘根目录 | 启动注入 | 统一正斜杠 |
BATCH_DATE | 业务批次日期 | 命名参数,每次必传 | 增量条件全靠它 |
模板化的判断标准很简单:把这份 .ktr 从测试环境搬到生产,需要改的东西是不是只有变量值。如果还要打开 Spoon 改连接、改路径、改 SQL 里的库名,那就还没到位。我一般会做一次「空白环境演练」——找一个干净的库,只注入变量、不改文件,能一次跑通才算合格。
上线前这张表我每次都过一遍,少一项就要出事:
| 检查项 | 判据 | 不通过的典型表现 |
|---|---|---|
| 路径是否全变量化 | 全文搜/和\\无硬编码 | 换机器就报找不到文件 |
| 参数是否齐 | 空白环境只注变量能跑通 | 报参数未赋值 |
| 失败判定 | 错误数为 0 且写行数 > 0 | 绿着但没数据 |
| 重跑幂等 | 同批次跑两次结果一致 | 数据翻倍 |
| 日志可查 | 按批次能查到起止和行数 | 出问题只能翻 stdout |
| 并发上限 | 同时跑不超过执行池大小 | 数据库连接被打满 |
幂等那条最容易被忽略。做法无非两种:按批次日期先删后插,或者用「插入/更新」步骤按主键合并。前者简单粗暴,后者适合大表。千万别用纯「表输出」直接追加,重跑一次数据就翻一倍,事后清洗的代价远高于当初多配一个步骤。
说个真实教训。早些年我接手一套别人留下的调度,页面上十几个任务全是绿的,业务方却说数据不对。查了两天才发现问题出在判定条件上——只看错误数,而那个转换的过滤条件写错了,所有行都被过滤掉,一步都没报错。从那以后我每次上线新转换,都强制走一遍「先跑一次看行数、再故意跑一次看幂等」这两步,哪怕再赶时间也不省。数据这东西,绿的不一定是对的,有数才算数。
希望帮到你。
本文还有配套的精品资源,点击获取