简介:面向数据集成与ETL学习者的Kettle 9.3获取说明文档,主要服务于需要在本机搭建数据抽取、转换与加载流程的初学者和数据工程师,帮助其绕开官方下载链路不便的问题,快速取得这款开源ETL工具。压缩包仅含1个docx文档,约14KB,文档以百度云分享链接和提取码为核心,并简要介绍Kettle的跨平台运行、Spoon图形化设计、Transformation与Job两类脚本,以及关系型数据库和云存储等数据源支持。已有5743人学习或下载,说明该版本在中文用户中具有一定关注度。读者可据此定位Kettle 9.3安装包,同时借助文档梳理工具定位、核心组件与典型使用场景,为后续搭建ETL流程、理解作业调度和排查环境问题提供入口,适合作为获取与入门参考。
1. 图形化 ETL:Spoon 里点一下运行,真正搬数据的为什么不是它
第一次打开 Spoon 的人几乎都会有个错觉:这张拖着线连起来的图,是它自己在跑。事实正相反。你把 9.3 的压缩包从网盘取下来解压,目录里躺着的 pan、kitchen、carte 才是真正执行任务的入口,Spoon 只是其中之一,而且是唯一一个不接触数据的。它只做两件事:编辑 XML 元数据,再把这份元数据交给引擎。你会用这套工具,却搞不清为什么改了个参数没生效、为什么界面显示执行成功而目标表还是空的,绝大多数原因都能追溯到这层分工没想明白。Kettle 用纯 Java 写成、解压即用,这个“轻”恰恰来自它把界面和执行彻底拆开。
2. Transformation 与 Job:为什么非要拆成两种 XML
同一个工具里存在两种脚本文件,很多人第一反应是设计冗余,其实它们是两套完全不同的执行语义,混在一起反而会出问题。
2.1 行流与控制流:两种“下一步”的含义根本不同
Transformation 描述的是行流(row flow)。里面的 hop 只是管道连接,不是执行顺序。你把三个步骤连成 A→B→C,实际发生的事是:A 产出第一行数据时,B 就被唤醒开始处理这一行,同一时刻 C 也在等 B 的输出。三个步骤的线程是同时活的,数据像水一样流过去,谁也没有“等别人干完”的概念。
Job 描述的是控制流(control flow)。它的执行单元叫 Job Entry,一个个按跳转关系串行执行。这里的 hop 是真正的“先后”,而且带条件:可以配成“上一步成功才走这条”“失败才走这条”“无条件走这条”。所以 Job 里能做 if/else、能重试、能发告警、能循环。
这个差别会直接导致一类常见误解。写惯了 Job 的人去做 Transformation,会默认“A 步骤的所有行处理完才轮到 B”,于是在 B 里做全量统计、做去重、做依赖前序完成的操作,结果 B 拿到的是半截数据。Transformation 里没有隐式的阶段边界,想要边界就得自己放阻塞型步骤(排序、分组、阻塞数据直到步骤完成)去制造屏障。我一般会这么理解:Job 是剧本,Transformation 是流水线,剧本可以写“第三场再上场”,流水线不行,流水线上每个工位同时开工。
2.2 Job 存在的理由:调度、容错和变量传递
既然 Transformation 已经能处理数据,为什么还要 Job?因为有一整类问题不属于数据处理:什么时候跑、跑失败了怎么办、跑完通知谁、这个月的数据处理完再处理下个月。这些是编排问题。
Job 里没有数据行,只有变量。这条限制很关键:Job 能把一个转换的结果集条目、一个文件名、一个统计值存成变量传给下一个转换,但它没法“逐行调用转换”。想让 Job 对一千万行各调一次转换,只能靠“复制记录到结果 + 遍历”,而这种写法一旦数据量上来,就会暴露出调度开销远超数据处理本身的问题。
提示:需要逐行处理千万级数据时,正确做法是在 Transformation 内部用流式步骤解决,不要在 Job 层做遍历调用。
容错也分工明确。Transformation 内部可以配错误处理步骤,把解析失败、类型转换失败的行重定向到一个专门的分支,主流程继续跑;Job 层则负责“这个转换整体失败了要不要重跑、重跑几次、重跑前要不要清理脏数据”。两层各管一段,谁也别越界。
2.3 两种文件的骨架:三要素贯穿始终
不管是 .ktr 还是 .kjb,剥掉图形界面那层外衣后都只有三样东西:连接定义、处理单元、连接关系。下面这份最小化的转换 XML 可以说明问题:
<?xml version="1.0" encoding="UTF-8"?> <transformation> <info> <name>load_orders</name> <!-- 步骤之间的行缓冲队列长度,默认 1000 行 --> <size_rowset>1000</size_rowset> </info> <!-- 连接是具名的,步骤只引用名字,不内嵌地址 --> <connection> <name>SRC_DB</name> <server>${SRC_HOST}</server> <port>${SRC_PORT}</port> <username>${SRC_USER}</username> <password>Encrypted 2be98afc86aa7f2e4...</password> <type>MYSQL</type> </connection> <order> <hop><from>读订单</from><to>过滤无效行</to><enabled>Y</enabled></hop> <hop><from>过滤无效行</from><to>写目标表</to><enabled>Y</enabled></hop> </order> <step> <name>读订单</name> <type>TableInput</type> <connection>SRC_DB</connection> <sql>SELECT id, amount, created_at FROM orders WHERE created_at >= ?</sql> </step> <step> <name>过滤无效行</name> <type>FilterRows</type> <send_true_to>写目标表</send_true_to> <send_false_to>记录异常</send_false_to> </step> </transformation>三个细节值得盯住。第一,connection只有一个name是常量,地址、账号全在运行期由变量填充,这是后面做多环境切换的基础。第二,order里的hop有enabled字段——界面上的线是可以“禁用”而不是删除的,禁用后线还在,但引擎不会建立这条行通道,这是排查“看着连上了却没数据”的第一现场。第三,密码字段前缀是Encrypted没错,但它是可逆的混淆而不是真正的加密,密钥逻辑内置在客户端里。
Job 的骨架与此同构,只是step换成entry,hop换成带条件的hop(evaluation 为 Y/N 或 unconditional),再加一个entries的排序列表。理解到这一层,你就会明白 Spoon 里做的每一次拖拽,最终都只是往这几个标签里填值。
3. 行流引擎:每个步骤一个线程,数据靠有界队列推着走
知道“引擎在跑”还不够,真正决定性能和行为的是引擎内部那套生产者—消费者模型。
3.1 线程模型:一百个步骤就是一百个线程
一个步骤在引擎里是一个插件,插件由三个接口组成:StepMetaInterface管元数据(配置项、序列化、输入输出字段定义),StepDataInterface管运行时状态(缓冲区、计数器、数据库连接),StepInterface管实际处理逻辑(初始化、逐行处理、收尾)。引擎启动转换时,会为每一个启用的 step 起一个线程,线程内部是个循环:从输入队列取一行 → 交给processRow处理 → 把结果写给下游队列 → 取下一行。
这带来两个直接后果。
一是步骤之间天然并行。读库慢不会阻塞写库,只要中间队列没满。整条链的吞吐由最慢的那一环决定,和流水线的瓶颈工位是一个道理。
二是每个步骤可以开多份副本。右键步骤里可以设置“副本数”,引擎会为这个步骤起多个线程,输入数据按轮询分摊。写目标表打不动时,把副本数从 1 调到 4,往往比换机器有效。但副本一开,输出顺序就不再有保证,涉及自增序列、行号、全局去重的场景要慎用,否则就会出现主键冲突或者行号重复的问题。
3.2 RowSet 与背压:慢步骤会自己拖住快步骤
步骤之间的那条“线”,实现上是一个有界队列,长度由<size_rowset>控制,默认 1000 行。生产者往里放,消费者从里取,放满了生产者就阻塞等待,取空了消费者就等待。
这个设计解决的是一个非常实际的问题:如果队列无界,读库线程能以远超下游的速度往内存里塞数据,几分钟就把堆撑爆。有界队列把“快慢差”换算成了“阻塞”:下游慢,上游就自动降速,内存占用被限制在步骤数 × 队列长度 × 单行大小这个量级里。所以 Kettle 处理千万级表时内存平稳,靠的不是什么高科技,就是这条背压线。
注意:队列长度调大能缓解抖动,但它是按“行数”算的,宽表(几百个字段)时 1000 行可能就是几十兆,几个步骤串起来堆占用就很可观。
背压有个前提:步骤必须是流式的。排序、分组、去重、阻塞数据直到步骤完成这几类步骤,必须先吃掉全部输入才能吐第一行,它们的实现要么把数据全放内存,要么溢写到临时目录。所以一条链上只要出现排序步骤,前面所有的流式优势都被打断,内存曲线会在那一步陡增。
| 步骤类型 | 内存行为 | 大表下的建议 |
|---|---|---|
| 表输入 / 表输出 / 字段选择 | 流式,逐行 | 队列长度默认即可 |
| 过滤 / 计算 / 值映射 | 流式,逐行 | 可开副本提升吞吐 |
| 排序 / 分组 / 去重 | 阻塞,全量或溢写 | 设临时目录,尽量前置过滤 |
| 流查询 / 数据库查询 | 全量加载参照表到内存 | 参照表超十万行改用流查询或关联 |
3.3 提交批次:为什么“执行成功”了数据却没进库
表输出步骤有个不明显但影响巨大的参数:提交记录数(commit size),默认 1000。意思是每写满 1000 行才发一次 commit。转换跑完时,最后不足 1000 行的尾巴会被补一次提交,所以正常结束不会丢。
但异常结束就完全不同了。如果转换在第 3500 行因为某条脏数据抛错,前面 3000 行早就 commit 了,回滚不了;后面 500 行没有。于是你得到一个“跑了一半”的表,重跑一次那 3000 行又进去一遍,账就翻倍了。这不是工具的缺陷,是所有批量写入的共同性质,只是很多人第一次遇到时完全没意识到自己踩着的是一个事务边界问题。
命令行跑转换的方式如下,注意参数是外置的:
#!/usr/bin/env bash # 用命名参数把环境差异挡在文件外面,ktr 本身进版本库 export JAVA_HOME=/opt/jdk-11 ./pan.sh \ -file=/data/etl/load_orders.ktr \ -param:SRC_HOST=10.0.0.12 \ -param:SRC_PORT=3306 \ -param:BATCH_DATE=2024-06-01 \ -level=Basic \ -logfile=/data/etl/logs/load_orders_$(date +%F).log-file指定转换文件,-param:传入命名参数(运行期替换${}占位),-level控制日志粒度(Basic 只记步骤级摘要,Rowlevel 会逐行打日志,生产上千万别开),-logfile落盘。Job 对应的是kitchen.sh,参数形式一致。
4. 元数据驱动:XML 里存了什么,一次设计怎么在多个环境跑
Kettle 真正区别于“写脚本做 ETL”的地方,是它把流程本身变成了数据。
4.1 连接具名化:切换环境只改一处
前面那份 XML 里,步骤引用数据库靠的是<connection>SRC_DB</connection>这个名字。这意味着整个转换里没有任何一处硬编码地址。要迁到另一个环境,只改connection那一块,剩下几十个步骤都不动。
这里有个需要提前说明的问题:<password>那一串Encrypted ...是可逆混淆,不是加密。任何拿到 ktr 文件的人都能还原出明文密码。所以转换文件进版本库之前,密码字段必须清空或改成变量引用,由运行环境的配置文件提供。这件事每年都有人出问题,而且通常是在共享目录里放了半年之后才被发现。
4.2 变量解析发生在运行期,不是保存期
${SRC_HOST}这种写法,Spoon 保存文件时原样写入,不做替换。真正替换发生在引擎启动转换的那一刻,取值来源有三类,优先级从高到低大致是:命令行命名参数、kettle.properties文件、上游步骤设置的变量。
这里有两个容易出错的地方。第一,SQL 里的变量替换依赖步骤上的“替换 SQL 语句里的变量”勾选项,不勾就是一串字面量${TABLE}直接发给数据库,报错信息通常很难看懂。第二,SQL 里混用两种占位方式会失效——${}是文本替换(运行前拼进语句),?是预编译参数(运行期绑定),两者可以共存,但?的取值必须来自上一步的字段,不能在界面里手填。
字段类型的处理同理。Kettle 内部的“行”是带元数据的对象数组,每个字段有名字、类型、格式、长度。表输入读出来的字段类型由 JDBC 驱动决定,和下游步骤期望的类型未必一致,字段选择步骤里把它显式转一次,比在下游到处做隐式转换可靠得多。
4.3 用脚本批量改写:把环境差异从文件里剥干净
当转换文件多到几十个,手工在 Spoon 里逐个改连接就变成了不可靠的重复劳动。既然它只是 XML,就可以用脚本处理:
import shutil, sys import xml.etree.ElementTree as ET from pathlib import Path def rewrite(ktr_path: Path, out_dir: Path, conn_map: dict, dry_run=True): """把 ktr 中引用的连接名按 conn_map 映射后另存,原文件不动""" tree = ET.parse(ktr_path) root = tree.getroot() changed = 0 # step 节点里的 <connection> 是连接名引用,按映射替换 for step in root.iter("step"): conn = step.find("connection") if conn is not None and conn.text in conn_map: conn.text = conn_map[conn.text] changed += 1 # connection 节点里的地址字段统一改成变量占位,避免带出真实地址 for conn in root.iter("connection"): for tag in ("server", "port", "username"): node = conn.find(tag) if node is not None: node.text = "${%s_%s}" % (conn.findtext("name"), tag.upper()) if dry_run: print(f"{ktr_path.name}: 将修改 {changed} 处引用") return out_dir.mkdir(parents=True, exist_ok=True) tree.write(out_dir / ktr_path.name, encoding="UTF-8", xml_declaration=True) print(f"{ktr_path.name}: 已输出到 {out_dir},来源引用 {changed} 处") if __name__ == "__main__": mapping = {"SRC_DB": "SRC_DB_TEST"} for p in Path(sys.argv[1]).glob("*.ktr"): shutil.copy2(p, p.with_suffix(".ktr.bak")) # 先留原始副本,出问题能退回 rewrite(p, Path(sys.argv[2]), mapping, dry_run=False)这段脚本做两件事:按映射改连接名,把连接节点里的地址字段统一替换成${连接名_字段名}形式的变量占位。dry_run参数让你先看改动数量再决定是否执行,shutil.copy2保证有可回退的副本。实际使用时映射表应该从外部配置读,不要写死在脚本里,否则只是把硬编码从 XML 挪到了 py 文件。
提示:改写前务必确认目标步骤里没有用连接名做字符串拼接逻辑(例如 SQL 里拼了库名前缀),这类引用脚本识别不到。
5. 避坑与排查:界面显示完成,为什么数据没到
下面这几条是实际运维里反复出现的,每条都按现象、原因、解决排列。
现象一:转换显示执行完成,日志里所有步骤都是 Finished,但目标表一行数据没有。原因通常是表输出步骤的“ truncate table ”选项和提交批次共同作用——如果这个转换在 Job 里被执行了两次而中间失败了,第二次的 truncate 把第一次的数据清掉后自己又没写成功。也有可能是步骤被禁用:界面上右键“禁用步骤”之后,步骤依然画在画布上,只是不参与执行,而 hop 的enabled字段也是 Y,肉眼看不出差别。解决方式是对着日志看每个步骤的“输出行数”统计,为 0 的步骤就是断点;同时检查步骤图标是否有禁用标记。
现象二:路径或 SQL 里的${}原样进了日志,报“表名 ${TABLE} 不存在”。原因是变量没被解析,可能是没勾选变量替换,也可能是变量根本没被定义——命令行拼错参数名不会有任何提示,引擎只是找不到这个键,然后保留原样。解决方式是先用一个写日志步骤把关键变量打出来确认,再逐步向下游排查。这条几乎每次换环境都会踩。
现象三:升级 JDK 之后,JavaScript 步骤集体报错。原因很明确:JavaScript 步骤依赖的脚本引擎在较新的 JDK 里已被移除,9.3 搭配的 JDK 版本里它还在,一旦把运行环境整成新版本,所有含 JS 步骤的转换都会在初始化阶段失败。解决方式是统一运行环境的 JDK 版本,或者把 JS 步骤的逻辑迁到计算步骤、值映射、用户自定义 Java 类里。这件事的教训是:升级运行环境之前,先统计一下有多少转换依赖了脚本引擎。
现象四:处理大表时内存缓慢上涨直到 OOM。常见原因不是队列太小而是太大,或者链路上有排序、分组、去重这类阻塞步骤。解决方式是先定位内存拐点对应哪个步骤(日志里步骤级统计能看到行数积压),把过滤和字段裁剪尽量前移,减少进入阻塞步骤的列数和行数;排序步骤配置临时目录和“压缩临时文件”,让它溢写到磁盘而不是死扛内存。
现象五:中文写进去变问号。原因在多数字符集链路,而不是 Kettle 本身:连接 JDBC URL 没带字符集参数、目标表建表时用的不是 utf8、或者转换文件本身被以错误编码打开又保存了一次。解决方式是从连接定义开始逐段验证,用一个只写一行的最小转换确认链路通,再回到业务转换上排查。
6. 从 9.3 这个版本说开:图形化 ETL 的取舍到底落在哪
上游把社区版的公开分发停在了 9.3,所以现在网上流传的安装包,绝大多数都是这个版本;配上合适的 JDK,它反而成了一个可控的基线。但版本本身不是重点,重点是这类工具的设计取舍。
图形化 ETL 把“流程”从代码变成了数据。好处很直接:XML 可以 diff、可以被脚本改写、可以把环境差异全部外置,一台机器上一份转换文件,靠参数就能跑在测试和生产两套环境里;新人接手时不用读五百行 Python,看画布就知道数据从哪来到哪去。代价同样明显:图里藏逻辑。一个转换真正的行为,取决于二十个步骤里每一个的几十个复选项,而这些配置在画布上不可见,只看得到步骤名。再加上行流本身是全并行、无阶段边界的,同一条链在不同的数据分布下可能表现出完全不同的行为。
| 取舍点 | 图形化的收益 | 需要额外付出的成本 |
|---|---|---|
| 流程即文件 | 可版本化、可脚本生成 | 配置的细节不可见,需要参数外置规范 |
| 行流全并行 | 吞吐高、天然多核 | 无阶段边界,依赖顺序必须显式加屏障 |
| 步骤插件化 | 扩展容易,生态丰富 | 插件版本与运行环境绑定,升级需回归 |
| 连接具名 | 一处切换全链生效 | 密码是混淆不是加密,必须外置 |
我自己的做法是把转换当代码管:ktr 进版本库、连接和路径全部变量化、密码永远不进文件、写入一律做成可重复执行(先按批次删再插,而不是纯 append),跑之前先拿一份小数据集做回归,比对行数和关键字段的校验和。这套流程不复杂,但少任何一步都会在某个深夜出事。
早年我把一份连着正式库的转换直接放到了共享目录,账号密码就在 XML 里明晃晃地摆着,换环境时手工改连接改到第十几处时才发现改漏了一个。从那以后我每次动转换文件,都强制先跑一遍变量替换检查,确认文件里不存在任何硬编码的地址和明文口令。这份 9.3 的包和配套资料,能帮你把整条链跑通;真正让这条链稳定下来的,还是上面那些看起来不聪明的习惯。希望帮到你。
本文还有配套的精品资源,点击获取