哪个跑过 Kettle(现在叫 Pentaho Data Integration)的人,没在“表输入”和“表输出”这两个步骤上遇到过卡死?我印象很深的一次,某数据平台跑夜间同步任务,作业一启动就卡在表输出上,界面进度条半天不动,数据库连接占着一大堆,后台看 CPU 也不高,任务就这么挂着,直到手工强制结束。这种问题不像语法报错那样给你明确的红字,它更像是整个流程突然“没了呼吸”,你只能从日志、数据库状态、内存表现这些侧面的线索去判断到底卡在哪。
这篇文章我就把这类问题彻底掰开讲一遍。我会从“卡死”的现象分型讲起,再到驱动与连接参数的隐性坑、表输出的事务尺度、内存层面的假死,最后给出一套可以直接照着做的排查流程。内容主要是面向天天跟 Kettle 打交道的开发、运维和数据工程师,同时也兼顾刚接触 ETL 不久的新手朋友——你不需要一开始就理解全部原理,照着排查步骤走一遍,大概率能把问题定位到某一个层面。
1. 先分清“卡死”的类型,再去修
1.1 症状是“整个界面都死了”还是“进度条不动了”
很多人一上来就说 Kettle 卡死了,但“卡死”其实分好几种情况,处理方向完全不同。我一般会先问三个问题:是 Spoon 界面整个没响应、鼠标转圈?还是界面还能点,但转换的步骤状态一直停在某个百分比不动?又或者是命令行的kitchen/pan进程还在,但日志长时间没有新输出?
第一种情况多半是 Spoon 本身的 JVM 出了问题,比如内存被撑爆、GC 卡死,跟转换逻辑关系不大。第二种情况才是真正的 ETL 执行卡住,通常是数据库连接等待、锁冲突或者 SQL 本身没跑完。第三种情况需要看是哪个步骤的输出停住了,以及数据库侧有没有对应的活动会话。
这里我建议你养成一个习惯:遇到“卡死”不要急着 kill 进程,先记录当时的现场。主要是三样东西:Kettle 的日志文件(或 spoon 控制台输出)、数据库当前的活动会话、操作系统层面的 CPU/内存/IO 状态。有了这三样,后面排查才有依据。否则你直接把任务杀掉,重启一跑又是同样位置卡住,等于白折腾。
1.2 日志级别与日志文件,是第一个突破口
Kettle 的日志级别默认是“基本日志”,但很多人一出问题就把级别调到“行级日志”,想看得更细,结果反而更卡。因为行级日志会把每一条数据处理记录都刷出来,数据量一大,日志本身就成了新的瓶颈。
我的建议是,排查阶段用“详细日志”,它会打印步骤初始化、连接获取、SQL 执行等关键信息,但不会为每条数据都输出一行。日志文件通常在 Kettle 安装目录的logs文件夹下,或者通过-logfile参数指定。跑生产作业时一定要把日志写到文件里,不然控制台滚动输出也会拖慢执行,甚至导致信息丢失。
看日志时重点找两类痕迹:一类是“Starting ...”之后长时间没有下一个动作,说明卡在该步骤的初始化或连接阶段;另一类是“Finished”和“Next step”之间隔了很久,说明某个步骤已经把数据处理完了,但数据在步骤间的传递过程中出了问题。前者偏连接,后者偏内存或队列堵塞。
1.3 四层定位法:从连接层一路查到 SQL 执行层
在自己的项目里排查多了,我慢慢总结出一个套路,叫“四层定位法”。任何表输入、表输出卡死,无非落在下面这四层:
- 连接层:数据库连接建立不起来、连接参数不对、网络超时、连接池耗尽。
- 锁与事务层:表被锁、长事务未提交、锁等待超时。
- 内存层:JVM 堆内存不足、GC 频繁、步骤间数据缓存积压。
- SQL 执行层:SQL 本身跑得慢、缺索引、驱动没有开启批量写入。
排查时按这个顺序走,每层都有明确的检查方法。比如先看数据库里有没有锁等待,再看 JVM 内存,最后才去分析 SQL。按层排查的好处是不会像无头苍蝇一样乱试参数,改一个地方就重新跑一次,既浪费时间还可能掩盖真正的问题。
2. 连接层和驱动,卡死最常见的隐性元凶
2.1 驱动版本和数据库版本不匹配,表现就是假死
连接层的问题是最隐蔽的。因为 Kettle 界面不会给你弹错误框,一旦数据库驱动跟服务端版本不兼容,常见表现就是“建立连接时卡死”,或者跑一段时间后连接被服务端静默断开,然后新请求全部排队等待。
举个我遇到的典型场景:某 MySQL 8.0 数据库,Kettle 里还挂着老版本的 MySQL 驱动(5.1.x),连接时用的是老版认证协议,服务端不买账,但驱动也不报错,就那么干等。最后是换了对应 8.x 的驱动,几秒钟就恢复了。
所以拿到一个卡死问题,先看一眼驱动版本。每种数据库都有自己的对应关系,比如 MySQL 8 推荐mysql-connector-java8.0.x,Oracle 19c 推荐ojdbc8及以上版本。不要拿一个驱动通吃所有环境,数据库版本升级之后驱动一定同步升。
2.2 JDBC URL 参数里藏着吞吐量的钥匙
很多工程师配置数据库连接时,只填主机、端口、库名和账号密码,JDBC URL 里那一串参数完全没管。这在做小数据量同步时没差别,一旦数据量上来,参数写没写对就是“分钟级完成”和“卡死到天荒地老”的区别。
以 MySQL 为例,我常用的 JDBC URL 是这样的:
jdbc:mysql://192.168.1.10:3306/etl_db?useSSL=false&rewriteBatchedStatements=true&useServerPrepStmts=false&connectTimeout=5000&socketTimeout=60000这里每个参数都有讲究。useSSL=false是避免加密握手带来的额外开销和潜在等待;rewriteBatchedStatements=true是关键中的关键,它让 JDBC 驱动把多次 INSERT 语句合并成批量执行,否则 Kettle 的表输出即使设置了批量提交,驱动也可能每条语句单独走一次,性能差距是数量级的;useServerPrepStmts=false是让语句在客户端预处理,避免服务端预处理语句长期占用资源;connectTimeout=5000和socketTimeout=60000则是给连接和读操作加上超时,避免网络异常时无限等待。
对于 PostgreSQL,常用参数是reWriteBatchedInserts=true,作用和 MySQL 的rewriteBatchedStatements类似,也是把批量插入真正落到数据库端。这些参数不必背,但要知道:当 Kettle 表输出写入慢得离谱时,第一反应就应该是检查 URL 里有没有启用批量重写参数。
2.3 连接池参数:让“空闲连接”不拖后腿
Kettle 的数据库连接选项里,高级页签下有一个连接池配置区域,很多人直接忽略。默认情况下连接池可能不生效,或者参数不合理,导致高并发步骤反复创建连接,连接数被数据库端限制后,新的请求就进入等待状态。
我一般会设置这三个参数:
- 初始连接池大小:2
- 最大连接池大小:20
- 连接池空闲连接测试周期:60 秒
连接池大小不是越大越好。如果同一个转换里有多路表输入和表输出,并且都指向同一个数据库,最大连接数该算算总账——Kettle 每个步骤可能同时拿多个连接(实际取决于“数据库连接对象的并发数”设置),连接数加起来超过了数据库的max_connections,后发起的请求就在那里排队,看起来就是卡死。排查时可以在数据库侧执行show status where variable_name in ('Threads_connected','Threads_running')看看连接占用情况。
2.4 给操作加上超时:宁可失败重试,不可无限等待
这个建议是我踩过坑之后才加上的。以前有个作业在网络抖动时卡了一个多小时,原因就是连接参数没有设置超时,Kettle 一直等着数据库响应。后来我在所有关键数据库连接上统一加了connectTimeout和socketTimeout,卡死的上限就被“锁死”在了一个可控范围,比如 60 秒。超时之后作业直接报错,人工介入或者重跑,都比无休止等待强得多。
这个思路可以用一句话概括:卡死不可怕,可怕的是没有时间上限的卡死。给连接和读写都设上超时,相当于给整个作业装了一个熔断器。数据库连接选项里“超时”相关的字段也填上数值,不要留空。
3. 表输出的事务尺度与锁等待
3.1 提交大小不是越大越好
表输出步骤有几个关键参数:插入大小、提交大小、是否启用批量插入。新手常见的操作是把“提交大小”调成 10000 甚至 100000,认为一次提交越多越快。数据量小的时候确实如此,但当单行数据较大或目标表有索引时,大事务会持有大量行锁,长时间不提交,其他会话的写入就会被堵住,最终呈现为 Kettle 卡死。
我实际测下来,比较稳妥的提交大小在 500~2000 行之间。单行数据小、字段少的时候可以往 5000 靠,单行数据大或者目标表有多个索引时,宁可使用 500 行一批。这样提交频率高了,但每个事务的持有时间很短,锁冲突的概率大幅下降,整体吞吐反而更稳定。对于表输入,则可以通过设置“每一行都执行一次”之类的选项避免一次拉全量。
还有一点容易忽略:如果目标表是主从复制架构,大事务还会带来主从延迟,延迟上来之后,从库的读请求也可能积压。所以表输出的提交大小不是一个单纯调优参数,它直接关系到整个数据库集群的稳定性。
3.2 用数据库视图当场抓出锁等待
表输出卡死时,很大概率是目标表上有锁。此时在数据库侧跑一条查询,能立刻看到是谁锁了谁。以 MySQL 为例:
SELECT * FROM information_schema.innodb_trx\G这条语句会列出当前所有未提交的事务、执行时间、锁定状态。重点看trx_started较早且还没结束的事务,那就是潜在的阻塞源。再看information_schema.innodb_lock_waits,可以知道锁等待的关系:
SELECT * FROM information_schema.innodb_lock_waits\G如果是 PostgreSQL,对应查询是:
SELECT pid, state, wait_event_type, wait_event, query FROM pg_stat_activity WHERE state = 'active' AND wait_event_type IS NOT NULL;看到结果后,通常有两种情况:一种是另一个应用或 SQL 拿着目标表的锁不释放,Kettle 在等待;另一种是 Kettle 自己的前一个事务没有提交,和后续事务产生了自锁。前者需要通知相关方提交或杀掉阻塞会话,后者多半是提交大小设置不合理,或者表输出之前的步骤有异常导致数据没有正常流转。
3.3 表输入输出用同一批连接时,要留余量
一个转换里同时有表输入和表输出,并且都连同一个库时,容易陷入“连接饥饿”状态。表输入拿了一堆连接去读,表输出再申请连接时已经没了;或者反过来,表输出一口气占用了所有连接,表输入只能排队。两边互相等待,整体表现就是进度条不动。
解决方法是设置“数据库连接对象”的并发数上限,以及连接池最大连接数,保证读写两侧都能拿到连接。还有一个思路是表输入和表输出使用不同的连接配置(比如账号权限区分),这样即便一边的连接全被占满,至少另一边不会被拖累。排查时如果发现数据库侧Threads_running很高但 Kettle 侧进度不动,优先查连接分配。
3.4 一条 SQL 命令确认是不是“行锁打架”
曾经有个数据量并不大的场景,表输出愣是卡了十几分钟。我用innodb_trx一看,发现事务列表里有两个老事务一直没提交,其中一个就是 Kettle 之前跑失败残留的事务。也就是说,前一次失败的作业没有正常回滚,事务持有的锁一直留在那里,后一次作业再跑,直接就撞上了。
处理办法是手工结束残留事务或者重启数据库会话。在 MySQL 里可以KILL <trx_mysql_thread_id>,但前提是你确认这个事务没有正在执行的业务。更稳妥的做法是找到持有锁的源头,等它结束,或者让对应程序把事务提交。这也解释了为什么我建议所有要写数据的表输出步骤,都把“提交大小”设置得适度:事务持有时间越短,残留锁的概率越低。
4. 内存假死:看上去一切都停了,其实 GC 在挣扎
4.1 界面上进度条不动,先看 CPU 和内存
有一类卡死和数据库完全无关,问题出在 Kettle 本身的内存管理上。表现在外的症状也很像“卡死”:进度条不动、界面无响应、日志长时间不刷。但这时候你去数据库侧看,发现所有 SQL 早就执行完了,活动会话也很少。问题其实在 JVM 内部——堆内存大量堆积,垃圾回收器一遍一遍 Full GC,整个进程基本处于“假死”状态。
判断方法很简单:开着任务管理器或者top看进程 CPU。如果 CPU 一直在跳但任务不推进,大概率是 GC 在空转;如果 CPU 几乎为 0,则偏向数据库等待或网络等待。Kettle 这种基于 Java 的 ETL 工具,内存问题在跑大数据量同步时特别常见,尤其是表输入一次性读入几十万行、且行宽很大时,内存会迅速被中间数据填满。
4.2 调整 Spoon 的 JVM 堆内存,不要只调界面字体
很多人遇到 Spoon 卡顿,第一反应是加内存条,其实先看启动脚本里的 JVM 参数更直接。Kettle 的启动脚本(Windows 上是Spoon.bat,Linux 上是spoon.sh)里可以设置堆内存。以spoon.sh为例,找到PENTAHO_DI_JAVA_OPTIONS那一行,改成类似:
PENTAHO_DI_JAVA_OPTIONS="-Xms1024m -Xmx4096m -XX:MaxMetaspaceSize=512m"-Xms是初始堆大小,-Xmx是最大堆大小。我一般将其设置到物理内存的一半左右,比如机器 8G 就给 4G,但要留出操作系统和其他进程的内存余量,不然 Kettle 没卡死,机器先被 swap 拖死了。
改完脚本后重启 Spoon 才能生效。这个调优对命令行执行作业同样有效,因为pan.sh/kitchen.sh也会读取相同的 JVM 参数。还有一点要注意:Kettle 8/9 版本基于 Java 8,Metaspace 满了也会引发奇怪的问题,设置MaxMetaspaceSize是为了防止无限加载类导致假死。
4.3 日志级别调低,别让日志拖垮大数据量
内存假死还有一个隐形帮凶:日志输出。当数据行是几十万级别时,如果把日志级别设成“行级日志”,Kettle 会为每一条数据处理生成日志记录,这些日志对象同样占用堆内存,而且输出到界面上还会触发界面刷新,双重消耗资源。即便你用的是“详细日志”,日志量也是“基本日志”的好几倍。
我的实操经验是:平时跑作业一律用“基本日志”,只有要定位具体问题时才临时改成“详细日志”,定位完马上改回来。生产环境里作业调度调用命令行时,建议用-level=Basic参数显式指定。另外日志文件建议单独放在一个磁盘空间充足的目录,避免日志盘满之后进程异常退出。
4.4 大查询拆为分页查询,避免内存一次性炸掉
表输入一次把所有数据读进 Kettle,本身就是一个高风险设计。数据量大、行宽大时,内存假死的概率非常高。正确做法是把一次大查询拆成多批小查询,每批只读一小段,边读边处理,内存占用就平稳了。
在 Kettle 的表输入步骤里,可以用“SQL 脚本”配合变量实现分页。比如有一个自增主键id,可以设置两个变量start_id和end_id,SQL 写成:
SELECT * FROM src_table WHERE id > ? AND id <= ? ORDER BY id然后通过“设置变量”步骤或者作业参数控制每一批的范围。也可以用数据库自带的分页写法,比如 MySQL 里的LIMIT ?, ?,但要注意排序字段必须有索引,否则分页越到后面越慢。表输出的批量插入也要和这种分页读取配合,每次只处理有限行数,两边内存压力都小。
5. 实操排查流程与避坑速查
5.1 一套可复用的排查顺序
工具、参数、原理都讲完了,这里给出一套可以直接照着走的排查流程。它是我在处理了多个 Kettle 卡死问题之后整理出来的,顺序很重要——先保证能拿到现场,再做针对性修复,最后通过最小化试跑验证。
第一步,记录现场。打开日志文件或者控制台,确认卡在哪个作业项、哪个步骤。同时看一眼数据库活动会话和系统内存。如果界面完全没响应,先判断是不是 Spoon 进程本身假死,必要时用jstack导一份线程快照。
第二步,查数据库状态。用上一章提到的innodb_trx、pg_stat_activity等查询,确认有没有长事务、锁等待、连接占用异常。如果发现目标表被锁,优先处理锁源。
第三步,验连接配置。检查数据库驱动版本是否匹配,JDBC URL 参数是否合理,连接池大小是否够用。这一步即使没发现问题,也建议统一加上超时参数,给卡死设一个上限。
第四步,查 JVM 与日志级别。确认堆内存充足、日志文件可写、日志级别不是行级。若内存紧张,调堆大小或者拆大查询。
第五步,做一次“最小化试跑”。这是我自己每次必做的环节:把转换复制一份,数据量缩小到十分之一,或者直接连一个只有几百行数据的测试表,看同样的逻辑能不能跑通。如果小数据量也卡死,那就是逻辑或配置问题;如果小数据量正常,大数据量卡死,基本就锁定在内存、事务规模或批量参数上。
5.2 常见卡死现场与对应处理方案速查表
| 现象描述 | 最可能原因 | 处理方向 |
|---|---|---|
| 日志停在“连接数据库”不动 | 驱动不匹配、网络不通、连接池耗尽 | 换驱动版本,检查 JDBC URL 参数,增加超时设置 |
| 表输出进度到 90% 左右不动 | 最后一批事务提交被锁阻塞 | 查锁等待,检查提交大小是否过大 |
| 表输入读入数据后界面卡死 | 内存不足或日志级别过高 | 调 JVM 堆内存,降低日志级别,拆分大查询 |
| 整个 Spoon 窗口无响应 | JVM GC 假死 | 用 jstack 看线程,重启,调整 -Xmx |
| 任务跑几十秒后所有步骤停滞 | 数据库连接数占满 | 调整连接池最大连接数,错开读与写的连接池 |
| 第一次跑正常,再跑同一转换就卡住 | 残留事务未回滚 | 查 trx 列表,KILL 残留会话或等待其释放 |
这张表我真实工作里经常拿来当备忘录。有些问题看着复杂,一对照就发现只是驱动版本没跟上,或者提交大小太激进了而已。
5.3 更稳的 ETL 设计:增量、分批、最小化试跑
排查和修复固然重要,但说到底,卡死问题最好的解决方式是“在设计阶段就不给它卡死的机会”。多次实践下来,有几个设计习惯强烈推荐:
第一,用增量同步代替全量同步。每次拿到全量数据再写入目标表,数据量和事务规模都会越来越大,总有一天会到达临界点。改成按时间或者按 id 分批增量同步以后,每次作业的数据量可控,表输出的事务大小也就可控了。
第二,批与批之间加间隔。有些定时作业习惯每分钟跑一次,可是单次要处理几万行。这种场景我建议改成每 5 分钟或 10 分钟跑一次,每次把范围拉大,这样数据库的锁竞争压力会小很多。当然这是业务维度权衡,但值得留意。
第三,任何大改动项目,先做最小化验证。你可以在正式跑大表之前,先用一个小表验证转换是否有连接、字段映射、类型兼容这类基础问题。基础问题不解决就直接跑大表,撞上了你根本分不清是环境问题还是逻辑问题。
5.4 必要时的进程级处置与预防
如果作业已经卡了很久,确认可以中断,再考虑强杀。Spoon 界面可以点击“停止”按钮,但有时按钮点了也没反应。此时可以在命令行用pan/kitchen执行时加-maxloglines之类参数控制日志量,或者通过系统进程管理器结束任务。我不建议在日常工作中轻易用强杀,因为残留事务会在数据库端留下锁,后续作业再跑仍然会卡。
预防这块,我的做法是在所有数据库连接配置里设好超时参数,在调度脚本里加上超时检测,比如超过 N 分钟就发告警,并尽量把转换拆成多个小作业,让失败影响面变小。设置告警非常关键——很多卡死问题你并不是没有能力解决,而是发现得太晚,导致整条数据链路堵塞,修复后还要补数据。有了告警,卡死出现的第一时间就能介入,通常几分钟就能定位到原因。
最后的体会
我处理这类问题的感受是:Kettle 卡死在绝大多数情况下不是 Kettle 自己“坏了”,而是它和数据库、操作系统之间某个环节没有匹配好。驱动版本对不对、批量参数有没有开、事务尺度是否合理、内存是否够用,这四件事做好,百分之八十的表输入表输出卡死都能避免。排查时一定不要靠着性子瞎试参数,先抓住现场,再按连接层、锁与事务层、内存层、SQL 层的顺序推进,基本每次都能快速找到答案。
如果你现在手上正好有这样一个卡死问题,我的建议是先从数据库侧查看有没有锁等待,再去检查 JDBC URL 里有没有rewriteBatchedStatements=true。这两个位置解决了大量“假死”现象。剩下的事情,照着上面的流程一步步来,问题大概率逃不出你手里。