做数据接入、清洗、装载的工程师,应该都绕不开ETL这三个字母。ETL工具里,Kettle(现在官方名称叫Pentaho Data Integration,社区版缩写PDI)又是很多团队入门首选。我最早接触Kettle是在做数仓报表的时候,一个月几十张表的同步、清洗、汇总,手工写脚本写到怀疑人生,后来换成Kettle,拖拖拽拽就把流程搭起来了,一个作业跑一年都没出过大乱子。这篇文章我不打算写成一份官方文档翻译,而是把我在项目里实际用Kettle的选型思路、安装细节、常用场景、参数传递、JNDI配置和踩坑记录全部摊开讲一遍,给准备上手或正在被数据清洗折磨的朋友一份能直接照着做的参考。
1. Kettle到底解决了什么问题:从一段长期加班经历说起
1.1 ETL是什么,Kettle在其中扮演的角色
ETL的全称是Extract、Transform、Load,翻译过来就是抽取、转换、加载。不要被这六个字母唬住,它的本质就一句话:把数据从一个地方挪到另一个地方,在挪的过程中顺便做点能想到的加工。比如把MySQL里的订单表抽到数据仓库,抽的时候把“创建时间”从字符串切成标准日期,把“状态码”从0和1翻成“有效”“无效”,最后写进目标表,这一整个过程就是ETL。
Kettle就是干这个活的图形化工具,用户不用一行行写Java或Python,而是把问题拆成“步骤”,然后通过连线把这些步骤串起来。一个步骤负责从库里读数据,另一个步骤负责过滤,再到另一个步骤负责字段转换,最后落到目标表。每个步骤像流水线上的一台设备,数据从上游流进来,加工完再流给下游。Kettle官方名称里的“Data Integration”也说明了它的定位:它不是一个数据库,也不是一个报表平台,它是一套数据集成流水线。想让数据从A系统到B系统,中间还要做清洗、合并、拆分,这套活Kettle能接。
1.2 Kettle的典型使用场景和选型边界
Kettle真正好用的场景集中在几类:第一类是常规的库表同步,从业务库定时同步数据到数仓,频率可以低到每天一次,也可以高到几分钟一次;第二类是文件类数据导入导出,比如把Excel、CSV、JSON文件读进来,清洗后写库,反过来也可以从库导出成文件给第三方;第三类是跨库数据交换,比如老系统MySQL和新系统Oracle之间做数据迁移,两个库类型不一样也能在Kettle里实现异构传输;第四类是标准化的数据清洗流程,比如字段格式统一、非法值过滤、维度表关联、去重排序等,Kettle组件里都有对应步骤。
但Kettle不是万能的。数据量特别大、状态特别复杂的流式计算,它做起来很吃力;需要精确到毫秒级实时同步的场景,它不如专业的数据集成中间件;团队里如果Python很熟,很多人也会选择直接用脚本实现。Kettle的优势是门槛低、上手快、图形化、对不写代码的人特别友好。我见过很多数据分析师靠Kettle自己把准备数据的活干了,不用等数仓排期。我的观点是:如果任务是“每天的报表准备”,Kettle是性价比很高的选择;如果任务是“几十TB的历史数据迁移”,还是老老实实找更专业的方案。
2. Kettle安装与环境准备:版本、Java和目录结构
2.1 最新版本下载和Java版本匹配
Kettle现在的官方包名是Pentaho Data Integration,社区版在官网有下载入口。你搜“Kettle下载”的时候,很多网站会给你一堆旧版本和历史教程,我的建议是直接看官网的社区版下载页,认准带有“pdi-ce”标识的压缩包。为什么要认准社区版?因为商业版收费,社区版功能对绝大多数场景完全够用,没必要花钱。
版本选择上有个必须注意的坑:不同版本的Kettle对Java版本要求不一样。比如PDI 8以前通常用Java 8,PDI 9开始某些版本能支持Java 11,而到PDI 10之后不少新功能又要求在Java 17环境下跑。下载之前先看一下官方发布说明里对应的Java版本要求,别装完一启动直接报class版本错误。我装过一台服务器,机器上默认Java是17,结果跑的还是一个需要Java 8的老作业,运行时报了一堆莫名其妙的UnsupportedClassVersionError,最后老老实实装了多版本Java切换才解决。建议准备一套相对独立的环境,单独放Kettle需要的JDK版本,不要和线上业务系统搅在一起。
2.2 解压即用,环境变量与常用目录
Kettle安装非常“老派”:下载一个tar.gz或zip包,解压就能用。Windows下解压后双击Spoon.bat就能打开图形界面,Linux下运行目录里的spoon.sh。注意图形界面最好在本地开发机上用,生产服务器上如果不需要点界面,可以完全不用启动Spoon,直接用Pan和Kitchen两个命令行工具跑作业。Pan负责跑转换文件(.ktr),Kitchen负责跑作业文件(.kjb),后面调度会用到。
解压之后,有几个目录你最好提前记住。第一个是根目录下的lib,里面放着Kettle运行时需要的第三方jar包,如果你要做一些特殊的数据库连接或者二次开发,以后可能会往这里扔包;第二个是plugins目录,Kettle的步骤插件都在里面,比如有些版本里JSON解析、Kafka插件是需要单独加载的;第三个是>SELECT store_id, order_date, amount FROM ${store_table} WHERE order_date >= ${start_date}
然后在“表输入”步骤的“插入步骤”里把store_table和start_date的值填进来。作业里每循环一次就调整一次变量,就可以用同一个转换处理不同的表。这样比做十几条静态SQL连接干净得多。
另一种场景是多个来源字段不一致,需要先统一字段名再合并。这时候把每个来源读成一组,然后统一用“字段选择”步骤重命名并只保留需要的字段,接下来再用“追加流”或“合并行”步骤把它们接在一起。注意“合并行”需要排序,且要求两条流都按关联字段有序;“追加流”则只是把两条流的数据上下堆叠,字段必须完全一致,顺序也要一致,否则效果和你想象的不一样。我写过一次合并两个不同Excel来源的月度报表,一开始没统一字段顺序,用“追加流”跑出来标题列全错位,排查了很久才发现是两个流的字段先后顺序不一致。
4.2 批量遍历日期查数的作业写法
批量按日期取数是ETL里的常见需求:每天同步昨天的数据容易,要是客户说要补充近90天的历史数据,还能用人工调日期吗?Kettle里完全可以写一个作业,自动从开始日期遍历到结束日期,每天生成一个SQL参数去查数。
具体思路是把日期变量作为驱动,在作业里用“JavaScript代码”作业项计算日期增量,然后循环调用同一个转换。先定义一个变量current_date,初值等于开始日期,在作业里放三个作业项:第一个是“设置变量”,初始化游标;第二个是执行转换,转换里的表输入SQL使用${current_date}作为过滤条件;第三个是JavaScript判断日期是否达到结束日期,如果没有达到,就用脚本把current_date加一天,再跳回执行转换的作业项,形成循环。
我实际项目里写过类似的补数作业,每天跑一次current_date从2024-01-01递增到昨天的日期。关键点有两个:一是日期变量格式要保持统一,建议全程用yyyy-MM-dd,不要有的地方带横杠、有的地方斜杠;二是在执行转换的时候,“传递参数”必须勾选,否则作业里设置的变量不会传到转换里面。这个坑我踩过一次,作业里变量明明有值,转换里却拿到一个空字符串,最后SQL把整张表查了一遍,差点把源库打挂了。
4.3 时间参数在转换里到底怎么传
很多人问“转换里的时间参数在哪里”,其实是没搞清楚“转换参数”和“命名参数”的关系。在Spoon里打开转换,点击菜单的“转换设置”,可以看到“参数”页签,在这里可以定义这个转换对外暴露几个参数名,比如start_date和end_date。定义好之后,转换内部的任何步骤都能用${start_date}这种占位符引用。而作业在调用转换时,通过“转换”作业项的“参数”页签,把作业里的变量值映射给转换的参数名。
还有一种情况是希望每次运行都能自动取当前时间,比如默认取“昨天”。这时可以在“表输入”的SQL里直接写:
WHERE order_date >= date_sub(current_date(), interval 1 day)也可以使用Kettle自带的替代变量,比如${__DATE_YYYYMMDD__}之类的内置变量,但不同版本支持情况有差异,用之前最好在文档或Spoon里确认。我习惯的做法是宁可多写一步,在转换最开始用“生成记录”或“计算器”步骤把时间算成字段,再在后续步骤里引用这个字段。原因很简单:直接写死当前时间会让作业看起来不直观,团队成员接手时不知道流程什么时候自动取晚上还是早上,而且测试的时候也很不方便改动。
5. 参数化、JNDI和运维细节,让项目真正可控
5.1 变量与参数:不同作业用同一套逻辑
Kettle里经常会同时出现“变量”“参数”“命名参数”几个词,刚开始容易混。简单区分:${xxx}和%%xxx%%是变量引用方式,变量值可以在作业里用“设置变量”作业项写,也可以在执行脚本时用JVM参数传递;参数则更像函数的入参,在转换或作业配置的“参数”页签里定义名称和默认值。两者实际运行时都可以通过${xxx}引用,区别主要在“作用域”和“定义位置”。
想做出可复用的作业,我建议把环境相关的东西全部抽成变量。数据库连接地址、用户名、密码、文件路径、日期范围,这些都应该在作业开头用“设置变量”作业项统一赋值,或者放到一个统一的.properties文件里,用“读取属性文件”作业项加载。这样同一套作业在测试环境和生产环境里跑,只需要换一份配置,不用改任何Kettle文件。我亲手维护过60多个转换,一开始大家习惯把库名直接写在“表输入”里,后来环境一迁移,所有转换都要逐个改,改了一个忘记另外一个,最后对账对出一堆差异。后来统一改为变量之后,换环境只改一个配置文件,十分钟全部搞定。
5.2 JNDI配置:把数据库连接从转换里抽出去
JNDI可能听起来很“Java”,但在Kettle里其实就是一套数据库连接的命名规范。它的好处是让你在转换里不直接写“主机名+库名+账号密码”,而是写一个逻辑名称,比如jdbc/mysql_report。真正的连接信息统一配置在simple-jndi目录下的属性文件里,换环境时只需要改这个文件,所有引用该JNDI名的转换全部跟着生效。这个方法在多人协作项目里尤其好用,因为每个人的本地用户名密码可能不同,只要各自改自己的JNDI文件就行,代码和ktr文件完全不用动。
配置JNDI的步骤也不复杂:在Kettle安装目录下找到simple-jndi目录,里面会有一个jdbc.properties文件,增加一条配置,比如:
report_db/type=javax.sql.DataSource report_db/driver=com.mysql.cj.jdbc.Driver report_db/url=jdbc:mysql://192.168.1.100:3306/report?useSSL=false&characterEncoding=utf8 report_db/user=etl_user report_db/password=your_password然后在Spoon的数据库连接对话框里把连接类型选择为“JNDI”,数据源名称填report_db。这样转换文件里记录的就不是具体的连接信息,而是一个取值标识。另外提醒一句,JNDI配置改完以后通常要重启Spoon或Kitchen进程才会重新加载,我踩过改了密码后作业一直报连接失败的坑,其实就是没重启进程,老连接还挂在内存里。
日志和异常处理也不可忽视。每跑一次重要作业,至少要做到两点:一是保留日志文件,二是失败后能收到通知。Kettle在作业项里提供了“日志”页签,可以把作业运行日志输出到文件或数据库中,推荐在作业层设置一个全局日志表,这样以后排查问题就不需要去翻控制台。失败通知可以通过“发送邮件”作业项实现,把异常信息拼到邮件正文里。邮件服务器配置虽然啰嗦,但真到凌晨3点作业挂了,一条邮件比第二天早上才发现数据没更新要强得多。
6. 常见问题排查与性能调优:我的踩坑记录
6.1 内存溢出和连接被断开
Kettle跑大数据量作业最常见的两个问题,一个是OutOfMemoryError,一个是数据库连接超时断开。内存问题通常发生在读取规模几十万行以上、又在转换里做了大量内存聚合的环节。Kettle默认的JVM堆内存可能在几百MB到1GB之间,跑大作业确实不够。解决办法是修改启动脚本里的PENTAHO_DI_JAVA_OPTIONS参数,比如Linux下在spoon.sh或kitchen.sh所在目录的set-pentaho-env.sh里,将堆内存调到-Xmx2048m或更高。注意,堆内存不是越大越好,调太大会把宿主机内存吃光,需要根据机器物理内存和并发任务数来定。
连接超时断开的情况,常见于长时间等待源库返回大查询或者批处理时空闲太久。可以从三方面排查:一是数据库本身有没有wait_timeout设置,二是Kettle的“表输入”步骤里有没有设置fetch size,三是在作业里增加“检测连接是否有效”之类的心跳机制。我遇到过MySQL的wait_timeout是8小时,Kettle作业一旦运行超过8小时,中途再取连接就报“connection has been closed”。后来我在数据库连接属性里使用了autoReconnect=true,并且把耗时长的作业拆成多段,既降低内存压力,也避免单连接长时间占用。
6.2 中文乱码和字符集问题
Kettle里中文乱码几乎都出在连接字符集或文件编码不一致上。写数据库的时候,先确认数据库表是UTF-8还是GBK,然后数据库连接URL里加上characterEncoding=utf8或characterEncoding=gbk,不要指望两边自动转。读取Excel、CSV文件时,CSV文件本身如果是GBK编码,在“文本文件输入”步骤的编码选项里要改成GBK;Excel是老版本xls和新版本xlsx,Kettle处理方式不同,但只要是“Excel输入”组件,一般都能按内容识别,关键是不要中途混用“CSV文件输入”去读xlsx,那样必乱码。
我印象很深的一次故障:从Oracle同步一张客户信息表,Kettle里显示正常,但写入到MySQL后中文全部变成问号。查了很久才发现Oracle的字符集是ZHS16GBK,MySQL表却是utf8mb4,我在表输入步骤里返回的数据已经被转成了UTF-8,但表输出连接串里没写characterEncoding,Kettle默认用了系统环境的编码,结果一写库就乱。最后把URL改成jdbc:mysql://...?useUnicode=true&characterEncoding=utf8,重新跑一遍就正常了。
6.3 性能慢的几个常见原因和优化方向
Kettle作业跑得慢,不要第一时间怪工具本身,大部分瓶颈出在SQL和设计上。首先要看“表输入”里的SQL有没有走索引;其次看是不是在Kettle里做了大量的行级循环,比如用“查询”步骤逐行去数据库匹配,这种方式一旦数据量变大,性能会非常差,应该改用“数据库查询”步骤或把关联放到SQL里join。第三是看看是不是步骤之间的小批量提交导致频繁IO,比如目标表每次写一行就commit一次,可以在“表输出”步骤里设置批量提交大小,比如每次提交5000行,速度会明显提升。
日志里反映的“处理行数”和“耗时”是很好的性能定位工具。在Kettle界面里运行转换时,会显示每个步骤的处理速度,通常单位是“行/秒”。如果某个步骤处理速度突然掉到很低,数据积压在它上游,那瓶颈就在这里。我处理过一个多表合并作业,源表500万行,看起来只是做几个字段类型转换,结果跑了一个小时。后来我用Spoon的运行日志定位到“值映射”步骤慢,点进去一看,映射的对象数量太多,每次都要查一遍字典表,后来改成“流查询”提前加载字典,耗时直接下降到十分钟。类似这种优化不需要高深技巧,肯沉下心分析日志就能找到突破口。
最后再分享一个经营上的经验:Kettle项目一定要把“入口规范”定好。所有转换文件命名清晰、所有数据库连接要么统一用JNDI要么全部走变量、所有日期参数都从作业集中定义,这三条看起来不起眼,但坚持半年之后,你会发现维护一套几十个转换的项目,根本没想象中那么痛苦。我每次接手新环境,花最多时间的往往不是Kettle本身,而是前任留下的“硬编码”和“备份副本”。工具是死的,用法是活的,把Kettle当成一个执行引擎,把规则和配置攥在自己手里,这套ETL体系才能真正跑得稳。