ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

macOS上Kettle 9.5 PDI CE:Spoon启动、CSV入库与launchd定时

macOS上Kettle 9.5 PDI CE:Spoon启动、CSV入库与launchd定时

简介:面向数据集成与 ETL 开发者的 Pentaho Kettle 9.5.0.1-261 自编译发行包,覆盖 macOS(含 M1 芯片)、Windows 与 Linux,需 JDK 17 运行,解压即可启动 Spoon、Kitchen、Pan 等组件。整包共 1078 个文件、约 387.49MB,其中 626 个 jar 承载核心依赖与插件,196 个 ktr 转换、19 个 kjb 作业及 80 个 XML 配置可直接参考和二次开发,另有 bat、sh 启动脚本与少量样例数据、界面资源,兼顾运行与学习。已有 2875 人学习下载。读者可获得已编译好的跨平台环境,省去源码编译与依赖排错,并借助内置转换、作业样例理解 Kettle 9.5 的精简包结构与新版特性,适合搭建数据抽取、清洗、迁移流程的初中级工程师作为开箱即用的练习与落地基础。

1. 在 macOS 跑 Kettle 9.5:先确认这套 PDI CE 到底能给谁用

上周帮一个团队收拾散落在 CSV、Excel 和业务库里的对账数据,对方开口第一句是:有没有不用写代码、点几下就能配出同步流程的工具。我丢过去的是 Kettle 9.5 的社区发行包,也就是 PDI CE 9.5.0.1-261 这个构建。它本质是 Pentaho Data Integration 社区版的桌面发行包,里面装着图形化设计器 Spoon、两个命令行执行器 Pan 与 Kitchen,外加一个轻量调度服务 Carte。它解决的问题很具体:跨库抽取、字段清洗、增量同步、把手工导表变成定时任务,而且不用为了一个临时同步需求去写一套调度框架。在 macOS 上用它的多是开发或数据侧工程师,先在本机把流程配通,再挪到服务器跑。下面按 macOS 这条线,从解压一路拆到无人值守。

2. 解压即用之前:JDK 版本、目录结构与启动链路

2.1 为什么 macOS 上没有安装包:zip 分发与 JDK 的前置绑定

PDI 社区版一直只发 zip,不出 pkg 也不签 dmg,原因不复杂:Spoon 是一个基于 SWT 的 Java 桌面程序,官方要维护 Windows、Linux、macOS 三套启动脚本,打包成 zip 最省事。代价就是 macOS 上拿到手的是一个裸目录,双击不管用,得先自己把 JDK 找出来。

启动脚本 spoon.sh 的探测顺序是硬编码的:先看环境变量 PENTAHO_JAVA_HOME,没有就退到 JAVA_HOME,再没有就去 PATH 里找 java。所以「双击打不开」这件事,九成不是包坏了,而是脚本没找到合格版本的 java。macOS 自带的那套运行环境里没有完整的 JDK,命令行敲 java -version 经常会弹一个安装提示框,就是这个原因。

版本上,9.5 这一代被验证过的组合是 Java 8 和 Java 11。Java 17 之后模块系统封装收紧,Spoon 启动时最常见的报错是 javax.xml.bind 相关的 NoClassDefFoundError,以及反射访问被拒的 IllegalAccessError。我在 17 上试过一次,日志刷出几十行之后就退出了,界面根本没起来。

JDK 版本在 9.5 上的表现建议
Java 8能跑,插件兼容性最好,但部分加密与字符集库偏旧老环境可留
Java 11官方验证组合,Spoon、Pan、Kitchen 都稳定首选
Java 17JAXB 缺失、模块反射受限,启动即报错不要用
Java 21同上,且部分第三方插件直接不加载不要用

我的习惯是机器上同时留 8 和 11,用 /usr/libexec/java_home -v 11 精确取路径,不去改系统的默认 JDK。因为很多老项目还绑着 8,改动全局变量属于给自己埋雷。

2.2 解压后先认目录:lib、plugins、simple-jndi 各自的职责

解压出来的># 列出所有已安装的 JDK,输出里带版本号和路径 /usr/libexec/java_home -V # 精确取 JDK 11 的路径,赋给 PDI 专用的环境变量 export PENTAHO_JAVA_HOME=$(/usr/libexec/java_home -v 11) echo "$PENTAHO_JAVA_HOME"

第一行只做查询,不改变系统状态;第二行才是关键,PENTAHO_JAVA_HOME 的优先级高于 JAVA_HOME,专门用来给 PDI 指定版本,不影响其他项目。如果你的机器上压根没有 11,这条命令会直接报错退出,比等到 Spoon 闪退再来排查省事得多。

接下来解压并处理两个 macOS 特有的麻烦:归档权限位丢失,以及从浏览器下载带来的隔离属性。

# 建一个固定位置放工具,避免以后找不到 mkdir -p ~/opt && cd ~/opt # 解压,-d . 表示解到当前目录,会生成>cd ~/opt/data-integration # 先看看自带了哪些作业,文件名以你目录里的实际输出为准 ls samples/jobs | head -20 # 挑一个跑,-level=Basic 只打关键日志 ./kitchen.sh -file=samples/jobs/<上一步看到的作业名>.kjb -level=Basic echo "退出码=$?"

判断标准很简单:日志末尾出现作业执行完成的统计行,且退出码是 0。如果这一步就报 java: command not found,回到 2.3 检查 PENTAHO_JAVA_HOME;如果报找不到类,多半是 JDK 版本过高;如果能跑完但中间有乱码,那是编码问题,留到第 3 章处理。

补一句关于路径的经验:不要把>#!/bin/bash # 保存为 ~/opt/start-spoon.sh,用完 chmod +x # 指定 JDK 11,避免被系统默认版本带偏 export PENTAHO_JAVA_HOME=$(/usr/libexec/java_home -v 11) # JVM 参数:初始堆、最大堆、编码 export PENTAHO_DI_JAVA_OPTIONS="-Xms512m -Xmx4096m -Dfile.encoding=UTF-8" cd ~/opt/data-integration || exit 1 # 把启动输出同时留一份到日志,闪退时有据可查 ./spoon.sh "$@" 2>&1 | tee ~/opt/spoon-launch.log

逻辑上没什么花活:先钉死 JDK,再给 JVM 参数,最后进目录启动。tee 那一步看着多余,但 Spoon 在某些异常下是静默退出的,界面一闪就没,终端也不留东西,这时候 spoon-launch.log 就是唯一的黑匣子。参数上,PENTAHO_DI_JAVA_OPTIONS 会被启动脚本拼接进最终的命令行,它和直接改 spoon.sh 里的默认值是等效的,但用环境变量更干净,升级版本时不用重新改脚本。

3.2 内存怎么给:-Xmx 与 macOS 的物理内存

默认堆大小对演示够用,对真实数据量完全不够。转换跑着跑着 Spoon 界面卡住不动,日志里冒出 OutOfMemoryError: Java heap space,就是堆打满了。这时候第一反应不该是无脑调大 -Xmx,得先看是哪一类内存问题。

参数含义常见取值说明
-Xms初始堆512m和 -Xmx 设成一样可减少扩容抖动
-Xmx最大堆2048m ~ 8192m不超过物理内存的 60% 左右
-XX:MaxMetaspaceSize元空间上限512m插件多时适当放大
-Dfile.encoding默认字符编码UTF-8macOS 上建议显式声明

以 16G 内存的机器为例,给到 4096m 到 6144m 是比较稳的区间,再往上系统本身就开始换页,反而更慢。更关键的是判断 OOM 的成因:如果是「排序记录」「聚合」这类需要把全量数据攒在内存里的步骤撑爆的,加堆只能续命,正确做法是加大「临时文件目录」的使用、调小缓存行数;如果是「数据库查询」一次拉回几十万行,应该改用流式参数或者分页抽取。我在一个对账项目里就吃过这个亏,把 -Xmx 从 2G 一路加到 12G,最后还是炸,后来发现是「表输入」没有勾选「在首次行检索时设置结果集只进」,游标把整个结果集都缓存在客户端了。

3.3 中文、编码与 Retina 缩放

编码问题在 macOS 上有两层,很多人只处理了一层。第一层是 JVM 的默认字符编码,用 -Dfile.encoding=UTF-8 声明;第二层是每一步「文本文件输入」里那个独立的编码下拉框,它跟 JVM 编码没有继承关系,默认值未必是 UTF-8。这两层任意一层不对,中文列名就会变成问号或者方块。

排查顺序建议是反的:先打开那个步骤看编码设置,再看 JVM 参数,最后才怀疑数据库连接串。因为前面两层改起来最快。数据库侧还要补一刀,MySQL 的连接串加上 characterEncoding=utf8 或者直接用 utf8mb4,表的字符集也确认一遍,三层都对齐了才不会出现「Spoon 里显示正常、库里存成乱码」这种半对半错的状态。

至于 Retina 高分屏,Spoon 用的是较老的 SWT 版本,界面元素不会跟着系统缩放走,字会显得偏小,按钮间距也偏挤。这个问题没有干净的解法,能做的只有把系统显示设置调成「更大文字」,或者外接一台非 Retina 的显示器。属于历史包袱,习惯就好,别在上面花时间。

3.4 Apple Silicon:SWT 本地库的架构问题

这一节是 M 系列芯片用户必读。9.5 这个构建发布的年代早于 Apple 芯片普及,包里的 libswt/osx64 目录放的是 x86_64 架构的本地库。用 arm64 的 JDK 去加载它,结果就是 Spoon 启动后白屏、闪退,或者在日志里看到 UnsatisfiedLinkError。

三条路,按成本从低到高排:

# 路线一:装 Rosetta 2,然后用 x86_64 方式启动(需配套 x86 JDK) softwareupdate --install-rosetta --agree-to-license arch -x86_64 ./spoon.sh

路线一是最省事的,代价是界面跑在转译层上,启动慢几秒,日常操作能接受。路线二是换成 arm64 的 SWT 库替换 libswt/osx64,需要自己找匹配版本,版本错一点就报链接错误,不建议新手碰。路线三是干脆不在本地开 Spoon,把 .ktr 文件在远程机器上编辑,本地只用命令行跑 pan 和 kitchen。

这里有个容易被忽略的结论:Pan 和 Kitchen 不加载 SWT,因为它们不需要图形界面。所以在 Apple 芯片上,用 arm64 JDK 执行命令行任务是完全正常的,只有 Spoon 界面需要 Rosetta。这意味着你可以本地用 Rosetta 开着 Spoon 改流程,服务器或容器里用 arm64 JDK 跑批,两边互不影响。搞清楚这一点,能省掉「是不是整台机器都得装 x86 环境」的纠结。

4. 第一个能跑通的转换:CSV 进库的完整链路

4.1 建表与驱动落地

先解决驱动。数据库厂商的 JDBC 驱动不在发行包里,得自己下,然后丢进 lib 目录。以 MySQL 8 为例,驱动包的类名从老的 com.mysql.jdbc.Driver 变成了 com.mysql.cj.jdbc.Driver,这个变化在 Spoon 的连接测试里会直接体现出来。

# 驱动放进 lib,放完必须重启 Spoon,热加载不生效 cp ~/Downloads/mysql-connector-j-8.x.x.jar ~/opt/data-integration/lib/ # 确认文件确实落进去了,顺便看看有没有多版本共存 ls -l ~/opt/data-integration/lib/ | grep -i mysql

第二条命令不是凑数。lib 里同时存在两个版本的 MySQL 驱动是很常见的事,尤其是从别人那里拷了整个目录过来。多版本共存时类加载顺序不确定,表现出来的就是「昨天还能连今天就连不上」这种玄学问题,血泪经验是定期清一遍。

目标表建议一开始就把幂等性考虑进去,主键选业务单号而不是自增 ID,这样重复导入可以直接走「插入/更新」步骤,不用先清表。

CREATE TABLE dw_orders ( order_id VARCHAR(32) NOT NULL COMMENT '业务单号', customer_id VARCHAR(32) NOT NULL COMMENT '客户编号', amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '金额', order_time DATETIME NULL COMMENT '下单时间', load_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', PRIMARY KEY (order_id), KEY idx_customer (customer_id), KEY idx_load_time (load_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细';

load_time 这一列看着可有可无,实际是对账的抓手。重跑之后想知道这批数据是哪次任务写进去的,或者想按批次回滚,没有它就只能靠猜。

4.2 .ktr 文件长什么样:结构与关键节点

.ktr 本质是一个 XML 文件,可以用文本编辑器打开。理解它的结构,出问题时才知道该看哪一段,而不是只能在界面里瞎点。

<!-- 结构示意,节选关键节点,不是完整可执行文件 --> <transformation> <info> <name>csv2mysql</name> <size_rowset>10000</size_rowset> <!-- 每次从输入步骤拉多少行 --> </info> <connection> <name>dw</name> <type>MYSQL</type> <access>Native</access> <server>127.0.0.1</server> <port>3306</port> <database>dw</database> <username>etl</username> </connection> <!-- order 段定义步骤之间的连接线,缺一条 hop 流程就断在这 --> <order> <hop><from>读CSV</from><to>字段清洗</to><enabled>Y</enabled></hop> <hop><from>字段清洗</from><to>写库</to><enabled>Y</enabled></hop> </order> <step><name>读CSV</name><type>TextFileInput</type></step> <step><name>字段清洗</name><type>SelectValues</type></step> <step><name>写库</name><type>TableOutput</type></step> </transformation>

三个要点。第一,步骤的名字在文件里是全局唯一的,两个步骤重名会导致 hop 指向混乱,界面上看起来连接线完整,实际跑起来数据流是错的。第二,order 段里的 hop 才是流程的骨架,删掉一个步骤时对应的 hop 常常残留成孤立节点,这就是「转换跑起来什么都没发生」的常见原因。第三,连接信息里的密码是加密串,不要手动改明文,改了也读不出来。

真正需要在界面里精调的参数不多,但每一个都影响成败:

步骤关键参数我一般设成
文本文件输入分隔符、编码、首行为字段名编码 UTF-8,字段名从首行取
文本文件输入字段类型手工声明,不用全部 String
字段选择去空格、日期格式统一转为 yyyy-MM-dd HH:mm:ss
表输出提交记录数1000
表输出使用批量插入勾选
插入/更新比较用的关键字业务主键字段

字段类型必须手工声明这一条值得单独说。全设成 String,让数据库做隐式转换,看着省事,实际会遇到两种翻车:带千分位的金额 1,234.00 被截断成 1,或者空字符串被转成 0 而不是 NULL。后一种在统计口径上属于事故级别,而且很难事后发现。

4.3 用 Pan 命令行跑一遍并验证

界面里点运行只是验证逻辑,真正要交付的是命令行能力,因为调度最终都落在命令行上。

cd ~/opt/data-integration ./pan.sh \ -file=/Users/me/work/ktr/csv2mysql.ktr \ -level=Detailed \ -logfile=/tmp/csv2mysql_$(date +%Y%m%d%H%M).log echo "退出码=$?"

参数逐个说明:-file 指定转换文件,用绝对路径最稳,相对路径依赖当前工作目录;-level 控制日志粒度,Basic 只打步骤开始结束,Detailed 会打每批的读写行数,排错时用后者;-logfile 把日志写到文件,同时终端也会输出一份。注意 -logfile 的目录必须已经存在,Pan 不会帮你创建,目录不存在时会静默退化成只输出到终端——这个坑我在第一次配调度时就踩了。

退出码是判断结果的第一依据,但不要只信它。Kettle 的某些步骤失败(比如写入时主键冲突)默认可能只记录错误行而整体仍返回 0,取决于步骤的错误处理配置。所以第二道验证一定要落库对账:

-- 行数与时间分布,和源文件对比 SELECT COUNT(*) AS total, MIN(load_time) AS first_load, MAX(load_time) AS last_load FROM dw_orders; -- 抽查金额是否有被截断的痕迹 SELECT order_id, amount FROM dw_orders WHERE amount = 0 LIMIT 20;

4.4 参数化:一份 ktr 跑多环境

同一个转换要在测试库和生产库之间切换,靠复制多份 .ktr 是最糟糕的做法,改一处漏一处,早晚出事。9.5 的命名参数能解决这个问题:在转换属性里定义参数名,命令行用 -param: 覆盖。

./pan.sh \ -file=/Users/me/work/ktr/csv2mysql.ktr \ -param:INPUT_FILE=/data/orders/20240101.csv \ -param:BATCH_TAG=20240101 \ -level=Basic

转换里对应位置写 ${INPUT_FILE} 和 ${BATCH_TAG} 即可。使用上有两个边界要清楚:命名参数的作用域是整个转换,子转换不会自动继承,需要显式往下传;另外命令行参数只在本次执行生效,不会写回 .ktr 文件,所以改完参数再打开界面看还是旧值,这是正常的。如果要放的是跨环境稳定的信息,比如数据库主机名,用 system/kettle.properties 里的全局变量更合适,它随发行包走,不用每次传。

5. 避坑与排查:macOS 上五个高频翻车点

5.1 现象:双击或在终端执行 spoon.sh 提示无法验证开发者

原因:从浏览器下载的归档带着 com.apple.quarantine 扩展属性,系统对未签名的脚本做了一次拦截,和脚本内容本身无关。

解决:递归清掉属性,再补执行权限。

xattr -dr com.apple.quarantine ~/opt/data-integration chmod +x ~/opt/data-integration/*.sh

如果清完还提示权限不足,检查一下是不是解压到了「下载」目录,某些系统策略下那个目录的脚本执行会被额外限制,挪到 ~/opt 就好。

5.2 现象:Spoon 启动几秒后闪退,日志里有 javax.xml.bind 相关报错或 IllegalAccessError

原因:JDK 版本过高。Java 17 起 JAXB 不在标准库里,模块系统也禁止了大部分反射访问,而 9.5 这一代的 jar 是按 Java 8/11 编译的。

解决:切回 JDK 11,用 PENTAHO_JAVA_HOME 精确指定,不要去改系统默认 JDK。改完验证方式很简单:

export PENTAHO_JAVA_HOME=$(/usr/libexec/java_home -v 11) "$PENTAHO_JAVA_HOME/bin/java" -version

输出是 11.x 就对了。如果机器上确实没装 11,装一个再回来,不要试图在 17 上加参数绕过,那是一条走不通的路。

5.3 现象:Apple 芯片机器上 Spoon 白屏、闪退,日志出现 UnsatisfiedLinkError

原因:libswt/osx64 里是 x86_64 的本地库,和 arm64 的 JDK 不匹配。

解决:装 Rosetta 2 并用 x86_64 方式启动,或者只把界面留在本地、任务交给命令行。判断当前 JDK 架构可以用:

file "$(/usr/libexec/java_home -v 11)/bin/java" # 输出含 x86_64 说明是 x86 版,含 arm64 说明是原生版

顺带说一句,Pan 和 Kitchen 不依赖 SWT,所以 arm64 JDK 跑命令行任务没问题,别因为这个坑把整条链路都迁走。

5.4 现象:表输出步骤报 No suitable driver 或 ClassNotFound

原因:三种可能,驱动没放进 lib、放进去没重启 Spoon、驱动类名写的是旧版的 com.mysql.jdbc.Driver。MySQL 8 的驱动已经换成 com.mysql.cj.jdbc.Driver。

解决:按顺序查。先 ls lib 确认 jar 在,再重启 Spoon,最后在连接的高级选项里显式填类名。连接串建议一开始就带上时区和编码:

jdbc:mysql://127.0.0.1:3306/dw?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&rewriteBatchedStatements=true

rewriteBatchedStatements=true 这个参数容易被漏,它让批量插入真正合并成多值语句,几千行级别的导入能差出好几倍时间。

5.5 现象:Spoon 里数据看着正常,进库之后中文变成问号

原因:三层编码没对齐——JVM 默认编码、文本文件输入步骤的编码、数据库连接与表的字符集。任意一层是 ISO-8859-1 或者 latin1,就会出现半对半错。

解决:三层逐个确认。JVM 加 -Dfile.encoding=UTF-8;步骤里把编码下拉改成 UTF-8;连接串加 characterEncoding=utf8,表建的时候用 utf8mb4。排查时可以先用一小段数据跑一遍,直接 SELECT 出来看,别在 Spoon 的预览窗口里判断——预览用的是另一条读取链路,显示正常不代表写进去正常。

6. 从点击到无人值守:Kitchen 加 launchd 的落法与验证

手工跑通只是第一步,真正的价值在于它能在没人看着的时候跑。macOS 上做定时,我更倾向 launchd 而不是 crontab,因为 crontab 在机器休眠唤醒后会漏掉错过的任务,而 launchd 的 StartCalendarInterval 会在唤醒后补跑一次,这对下班关盖子的笔记本很关键。

先写一个包装脚本,把环境变量和日志路径都钉死在脚本里,不要让定时任务去猜环境。

#!/bin/bash # 保存为 ~/work/bin/run_daily_sync.sh export PENTAHO_JAVA_HOME=$(/usr/libexec/java_home -v 11) export PENTAHO_DI_JAVA_OPTIONS="-Xms512m -Xmx4096m -Dfile.encoding=UTF-8" LOG_DIR=/Users/me/work/logs mkdir -p "$LOG_DIR" cd ~/opt/data-integration || exit 1 ./kitchen.sh \ -file=/Users/me/work/kjb/daily_sync.kjb \ -level=Basic \ -logfile="$LOG_DIR/daily_sync_$(date +%Y%m%d).log" echo "exit=$?" >> "$LOG_DIR/daily_sync_$(date +%Y%m%d).log"

包装脚本里为什么要有 mkdir -p:前面说过 -logfile 的目录不存在时 Kitchen 不会报错,只是不写文件。定时任务里这个坑极其隐蔽,第二天看到空日志会以为是任务没启动,实际是跑了但没留痕。

launchd 的配置文件放到 ~/Library/LaunchAgents/ 下,命名用反域名格式。

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key><string>local.etl.dailysync</string> <key>ProgramArguments</key> <array> <string>/bin/bash</string> <string>/Users/me/work/bin/run_daily_sync.sh</string> </array> <key>StartCalendarInterval</key> <dict><key>Hour</key><integer>2</integer><key>Minute</key><integer>30</integer></dict> <key>StandardOutPath</key><string>/Users/me/work/logs/launchd.out</string> <key>StandardErrorPath</key><string>/Users/me/work/logs/launchd.err</string> </dict> </plist>

加载和验证:

launchctl unload ~/Library/LaunchAgents/local.etl.dailysync.plist 2>/dev/null launchctl load ~/Library/LaunchAgents/local.etl.dailysync.plist launchctl list | grep etl # 第一列是最近一次退出码,0 才算成功 # 不想等到凌晨,直接手动触发一次 launchctl start local.etl.dailysync

验证要看三样东西,缺一不可:launchctl list 里的退出码、Kitchen 日志末尾是否有作业执行完成的统计行、库里按 load_time 筛当天的数据量是否对得上源文件。只看第一样会被坑,因为作业里某个转换失败但作业整体没中断时,退出码仍然是 0。

那次之后我给自己定了规矩:任何要进无人值守环境的作业,先在本地用 kitchen.sh 带 -level=Basic 手动跑一遍,确认退出码和日志末尾都干净,再去配 launchd。少这一步,第二天早上就得对着空表猜是任务没起、还是数据没到、还是根本没写日志。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进