简介:围绕ETL与Kettle基础制作的PPT讲解材料,面向具备一定编程和数据库基础、工作1—3年的研发人员,用于快速建立数据抽取、转换、装载的整体认知,并熟悉Kettle(PDI 9.2)的安装与使用。压缩包共1个pptx文件,约859KB,以26张幻灯片串联ETL概念、Kettle目录结构、转换与作业、步骤和跳、数据行与元数据、数据库连接及注意事项,同时覆盖数据同步调优与工具不足点。已有1466人学习。读者可借此理解图形化拖拽构建数据流的思路,掌握转换与作业的差异、常见连接配置和调试要点,并结合MySQL、Oracle实践同步流程,为项目中的ETL落地与排错提供参考。
1. 从「拖几个控件就能同步数据」说起:Kettle 把复杂度藏哪了
第一次看别人用 Kettle 做数据同步,很容易得出一个错误结论:这东西就是把几个控件拖到画布上,用箭头连起来,点运行。画布上从上到下的排列,看上去就是执行顺序——先抽取、再清洗、最后写库。这个直觉是错的。在 Kettle 的转换里,所有步骤在启动那一瞬间同时开跑,谁也不等谁,画布上的位置只是排版。真正约束数据先后的不是箭头方向,而是箭头底下那个叫「行集」的缓存。搞懂这件事,你才能理解一个看起来「串行」的 ETL 任务为什么会突然卡住、为什么日志顺序和预想的不一样、为什么一味调大内存没有用。这份 PPT 讲解真正有价值的地方,正是把并发模型和元数据规则摆到了明面上。
2. 行集与线程:为什么转换里的步骤「没有先后顺序」
很多人第一次被 Kettle 教育过,不是因为它难,恰恰是因为它看起来太简单。拖两个步骤、连一条线,点运行,数据跑通了,就以为自己懂了。直到某天做增量同步,任务跑了四十分钟没结束,日志停在某个步骤不动,内存曲线一路往上爬,才发现自己脑子里那套「从上往下依次执行」的模型根本不成立。
要理解 Kettle 的行为,得先把「转换」当成一个由多个线程和若干条有界队列拼起来的系统来处理。
2.1 一个步骤一个线程:转换启动时到底发生了什么
点下启动按钮之后,Kettle 会读取转换文件,然后做三件事:为每一个步骤创建一个执行线程,为每一条跳创建一个行集,把这套关系按连接建立起来。线程一启动就开始干活:有输入行集的,从行集里取行;没有输入行集的(比如「生成记录」「CSV 文件输入」),自己生产数据行;处理完之后往输出行集里写;没有输出行集的(比如「表输出」「Excel 输出」),直接消费掉。
终止条件是自下而上收敛的:当某个步骤的所有输入行集都关闭、并且已经被读空,这个步骤就结束;当转换里所有步骤都结束,整个转换才算跑完。这里面没有任何「等上一个步骤完成再开始下一个」的逻辑,因为压根就没有「上一个」。
注意:日志里各步骤的启动时间戳有先后,那是线程调度顺序,不是逻辑顺序。拿它去推断数据流向,会推导出完全相反的结论。
之所以这么设计,得回到数据仓库这个场景本身。一次同步动辄几百万上千万行,如果整个转换是一条串行流水线,中间结果总得有个地方放着:要么全量堆进内存等着下游来拿,几百万行直接 OOM;要么每两个步骤之间落一次磁盘临时表,IO 放大几倍,速度掉一个数量级。并发加有界队列这个组合,把内存占用从「和总数据量成正比」压成了「行集大小 × 跳数」这么一个常数——不管你今天同步的是十万行还是一亿行,进程占的内存基本是同一个量级。这就是资源里那句「以最小消耗内存的数据流方式处理」背后的真实含义。
代价也很明确:转换这一层没有全局顺序。所以「先清空目标表、再插入新数据」这类需求,你在转换里靠拖拽排列是做不到的,必须上升到作业层编排。刚上手的人经常在这里翻车——把清洗步骤放在加载步骤上面,就以为它一定先跑。
2.2 行集缓存:写满就停、读空就等,ETL 里的背压
跳不是一根抽象管子,它底下是一个有界阻塞队列,容量在转换属性里配置,默认是一万行。行为很朴素:队列满了,往里写的线程挂起;队列空了,从里读的线程挂起。上游跑得再快,也只能跑到「下游没消费完的那一万行」为止。
常见的实现思路大致是这样,转换文件顶层的 info 节点里会有这么几个参数:
<!-- 转换级配置,作用于本转换内所有跳 --> <transformation> <info> <name>sync_chat_to_target</name> <size_rowset>10000</size_rowset> <!-- 每条跳的行集容量,单位:行 --> <sleep_time_empty>0</sleep_time_empty> <!-- 读端遇空行集时的轮询间隔,单位:纳秒 --> <sleep_time_full>0</sleep_time_full> <!-- 写端遇满行集时的轮询间隔,单位:纳秒 --> </info> <!-- 下面是 step 与 hop 的定义,此处省略 --> </transformation>size_rowset是这套模型里最需要谨慎对待的参数。它不决定吞吐上限,只决定缓冲区有多大。写在 10000,意思是上下游之间最多容忍一万行的速度差;超过这个差值,快的一方被强制降速。两个 sleep 参数是给极端场景用的,设成 0 表示用默认的等待策略,一般不要动,动了容易把 CPU 空转打满。
真正执行的时候,用的是无界面入口:
# Pan 是无 UI 执行转换的入口,服务器上跑任务靠它 ./pan.sh \ -file=/opt/etl/trans/sync_chat_to_target.ktr \ -level=Detailed \ -param:BIZ_DATE=2024-05-01 \ -logfile=/opt/etl/logs/sync_chat_20240501.log-file指向转换文件路径;-level控制日志粒度,从 Basic 到 Rowlevel 逐级变细,Rowlevel 会把每一行都打出来,只在定位具体某条坏数据时开;-param:NAME=VALUE往转换里注入参数,转换内用${BIZ_DATE}引用;-logfile把日志落到指定文件,配合后面要说的调度脚本做归档。还有一个常用开关是-norep,表示不连资源库、直接用文件执行,能省掉一次资源库连接,也避免资源库成为单点。
这里有条血泪经验:遇到「任务卡住」,第一反应往往是去调大size_rowset,这多半是错的方向。行集调大只会让上游多跑一会儿,如果瓶颈在下游(目标库写入慢、索引太多、批次太小),缓冲区再大也只是把阻塞的时间往后推。更麻烦的是,行集调大会直接抬高峰值内存,在阻塞型步骤面前,这个操作等于把「慢慢卡住」变成「快速撑爆」。
判断方向的办法很土但有效:开 Detailed 日志,看各步骤的读入行数和写出行数是不是同步在涨。如果某个步骤读入一直涨、写出不涨,瓶颈就在它下游;如果某个步骤读入不涨、上游也不涨,那大概率是它自己在憋着,往下看 2.3 和第五章。
2.3 分发与复制:一条跳连多个目标时的两种语义
一个步骤可以有多条输出跳,这时候要选数据怎么送出去,两种模式语义完全不同。
分发(Distribute)是轮流给:第一行给目标 A,第二行给目标 B,第三行又给 A,如此循环。每个目标拿到的是总量的一个子集,合起来才是全量。适合做负载分摊或者按分片写多个库。
复制(Copy)是每行都给:同一行数据依次写进每一个目标的行集。每个目标拿到的都是全量。适合「一份数据同时走两条处理线」,比如一份落库、一份做抽样校验。
| 维度 | 分发(Distribute) | 复制(Copy) |
|---|---|---|
| 单行去向 | 只进一个目标,轮流分配 | 进所有目标 |
| 单目标数据量 | 总量 ÷ 目标数 | 等于总量 |
| 典型用途 | 并行写多个分片、分摊压力 | 一份数据走多条处理线 |
| 失败影响面 | 只影响该目标分到的那部分 | 任一目标异常都会波及全链路 |
| 性能陷阱 | 轮到的目标慢,会反向拖住上游 | 最慢的那个目标决定整体速度 |
复制模式最容易被误解成「广播出去各跑各的」,实际不是。写入是串行的:同一行先写进 A 的行集,再写进 B 的行集。所以只要有一个目标的行集满了,写端就卡在那里等,其他目标也跟着停。这解释了为什么加了一条「顺便存一份到文件」的复制跳之后,整个任务慢了一倍——那条文件输出跳成了全链路的最慢环节。
分发模式的问题更隐蔽。轮流分配意味着只要轮到一个慢目标,上游就得等它腾出位置,快目标并不能因此多拿数据。所以分发的前提是各目标处理能力大致相当,如果一个是本地库、一个是跨机房的远端库,分发反而会把整体拉到远端的速度。这种时候正确的做法不是换模式,而是把两条线彻底拆成两个转换,用作业并发跑,让阻塞互不影响。
3. 数据行与元数据:字段类型、格式、精度为什么必须显式写死
跑通第一个转换之后,绝大多数人会经历第二个幻觉:数据能看见,就说明同步对了。真正做过一段时间就会发现,Kettle 最危险的失败方式不是报错,而是不报错。数据照样流过去,行数一条不少,只是某些字段悄悄变成了 null,某些金额小数位被抹掉,某些日期整体偏移了八小时。这些问题全部指向同一层东西——数据行的元数据。
3.1 七种数据类型:Kettle 只认这几种,剩下都是它们的组合
Kettle 内部的数据行就是一个字段集合,每个字段的数据类型只有七种。因为它跑在 JVM 上,这七种类型实际上各自对应一个 Java 承载类型,理解这个映射关系,很多「莫名其妙的行为」就有解释了。
| Kettle 类型 | 语义 | JVM 侧承载 | 常见误用 |
|---|---|---|---|
| String | 变长字符数据 | String | 默认按字符集解码,源端编码不对就整列乱码 |
| Number | 双精度浮点数 | double / Double | 拿来存金额,小数位对不上 |
| Integer | 带符号 64 位整型 | long / Long | 误以为是 32 位,做位运算时算错 |
| BigNumber | 任意精度数值 | BigDecimal | 用==比较,或用它做高频算术,慢 |
| Date | 毫秒精度日期时间 | java.util.Date | 跨时区同步时不做显式转换 |
| Boolean | true / false | Boolean | 源端是 0/1 或 Y/N,没做映射 |
| Binary | 字节数组 | byte[] | 存大对象,把行集内存撑爆 |
第一条要记牢的是 Number 是双精度浮点。它的有效数字大约十五六位,0.1 + 0.2不等于0.3这件事在 ETL 里是真实存在的。把金额字段声明成 Number,短期内看不出问题,等到月末对账差了零点零几,排查成本极高。金额、单价、汇率这类必须精确的字段,一律声明 BigNumber,落到 BigDecimal 上,代价是算术运算慢一些、比较大小时要用compareTo而不是==。
第二条是 Integer 是 64 位。这跟很多语言里的 32 位整型不一样,好处是不会莫名其妙溢出,坏处是有人按 32 位去推算范围,写出错误的边界判断。
第三条是 Date 带毫秒但不带时区语义。当源库和目标库不在同一个时区,或者 JVM 默认时区跟数据库会话时区不一致时,同一批数据在两边看到的时刻会差几个小时,而且不报错。同步链路里只要跨了时区,就必须在转换里显式做一次时区归一,别指望工具自动处理。
第四条是 Boolean。源系统里布尔值经常以0/1、Y/N、是/否的形式存在,直接映射到 Boolean 字段,非预期值会被判成 null 而不是抛错。
3.2 元数据八要素:格式、长度、精度、小数点与分组符号
类型只是一半,另一半是描述。每个步骤在输出数据行时,都要带一份字段描述,通常包含名称、数据类型、格式、长度、精度、货币符号、小数点符号、分组符号这八项。名字在转换范围内必须是唯一的,这个不用多说;真正容易出问题的是后面几项。
格式(Format)常被当成纯显示层的东西,这是最大的误解。在「文本文件输出」这类步骤里,格式字符串直接决定序列化结果。你写#,##0.00,输出的就是带千分位、两位小数的字符串;下游如果按无分隔符解析,整列就废了。
小数点符号和分组符号这两项,在跨国项目里是经典的翻车点。同一个格式串#,##0.00,在把逗号当小数点的区域设置下,实际渲染会变成#.##0,00,千分位和小数位整个对调。数值1234.56在这套设置下输出成1.234,56,目标端按点号解析,得到的是 1.234,量级差了三个数量级,而且不会报任何错。
长度和精度这两项,在 String 上是截断开关,在 BigNumber 上是舍入开关。声明长度 20 而实际来了 30 个字符,超出的部分会被直接砍掉——这是「不报错的静默损坏」的另一个主要来源。
提示:字段元数据不是装饰,它是这一步骤的序列化契约。凡是会写出文件的步骤,格式、小数点、分组符号三项必须逐个核对,不要用默认值蒙混。
3.3 元数据错位:不报错但数据已经坏了的那些情况
元数据配错的后果,通常不会以异常形式出现,而是以「数据看起来有点怪」的形式出现。几种高频情况值得单独记一下。
字符串截断最常见。源端某字段是自由文本,实际长度偶尔超过声明值,超出的部分被静默砍掉,从行数上看完全正常,只有拿原始数据逐条比对才能发现。防范办法是在输入步骤之后接一个「数据检验」步骤,把长度超限的行打到错误处理跳上,而不是让它们继续往下走。
日期解析失败返回 null 次之。源端日期列里混进了一个0000-00-00或者一个空串,元数据声明的格式又对不上,Kettle 的处理是给个 null 继续跑。整条链路没有任何提示,直到下游报表发现某天数据为空。这里的取舍是:要么在输入步骤里把格式声明得足够宽松,要么加校验步骤把这些行拦下来人工确认。
精度丢失排第三。源端是NUMBER(18,4),中间被声明成 Number(双精度),再从 Number 写到一个整型列,小数部分就没了。这种损失是不可逆的,重跑也补不回来,只能改元数据配置再全量重刷。
用脚本步骤做格式转换是常见做法,但要注意脚本里改的是「行里的值」,不是元数据本身:
// JavaScript 步骤:把源端的字符串金额规整成数值,并给异常行打标记 // 注意:这里只改 row 里的值;字段的类型/格式要在步骤对话框里声明 var raw = row.amount_raw; // 上游传入的原始字符串 var amount = parseFloat(raw); if (isNaN(amount)) { row.amount = null; // 解析失败先置空 row.err_flag = "AMOUNT_PARSE_FAIL"; // 打标记,交给下游过滤 } else { row.amount = amount; row.err_flag = null; }这段逻辑的关键不在语法,在于它引入了err_flag这个额外字段作为「错误通道」。后续接一个「过滤记录」步骤,把err_flag非空的行分流到错误文件,主链路继续跑。这样既不让坏数据污染目标库,也不让整个任务因为一条脏数据中断。参数上要注意parseFloat对"12,34"这种带千分位的输入会只解析出 12,所以进脚本之前必须保证分隔符已经归一——这就又回到了 3.2 里小数点符号那一项。
4. 转换管数据、作业管顺序:两套引擎和它们的启动入口
新手最容易混淆的不是步骤,而是箭头。转换里画一条箭头,和作业里画一条箭头,长得一模一样,含义完全不同:前者是一条数据通道,后者是一个执行条件。搞清楚这个区别,才算真正分清了转换和作业。
4.1 转换与作业的分工:数据流和控制流不是一回事
转换(Transformation)处理的是数据行,它的基本单位是步骤,跳表示「数据往哪流」,所有步骤并发启动。作业(Job)处理的是执行顺序,它的基本单位是作业项,跳表示「在什么条件下执行下一项」——成功时执行、失败时执行、还是无条件执行。作业项是串行推进的,一项做完才判断下一项走不走。
| 维度 | 转换(Transformation) | 作业(Job) |
|---|---|---|
| 执行单位 | 步骤(Step) | 作业项(Job Entry) |
| 跳的语义 | 行集,一条有界阻塞队列 | 执行条件,成功/失败/无条件 |
| 并发性 | 所有步骤同时启动 | 按条件逐项推进 |
| 能不能形成环 | 可以,数据能绕回来 | 可以,配合条件做循环 |
| 职责 | 搬运、清洗、加工数据行 | 控制顺序、依赖、重试、通知 |
| 命令行入口 | pan.sh | kitchen.sh |
分工的判据其实很简单:这件事需要「按行处理」还是需要「按阶段处理」。读文件、拆字段、算派生值、写库,都是按行的,放转换。先建表、再灌数、再刷索引、失败了发通知,都是按阶段的,放作业。想在转换里实现「先删后插」,或者在作业里做逐行字符串解析,都是在跟工具的设计对着干。
作业项还有个转换没有的能力:可以拿到前一个作业项的执行结果,据此决定往哪走。这让作业能做条件分支、能做失败重试、能把一个失败的分支单独捞出来重跑。
4.2 总作业 + 分作业:把一次同步拆成可重跑的编排
资源里给的那个场景很典型:同步聊天信息到目标系统,总作业里挂两个分作业,具体的数据操作全部落在转换里。这个三层结构不是形式主义,每一层的存在都对应一个具体问题。
总作业负责调度和全局依赖:判断前置条件是否满足、按顺序拉起几个分作业、任何一个失败就中断并告警。分作业负责一段可独立重跑的逻辑,比如「同步会话表」是一个分作业,「同步消息表」是另一个。转换负责真正的数据搬运。
这么切的核心理由是重跑粒度。一次同步跑到一半失败,如果只有一个大转换,你只能从头再来,前面的几百万行白干。切成多个分作业之后,可以从失败的那一段单独重跑,前面已经成功的部分保留。这在数据量大的同步里是刚需,不是优化。
分作业之间还会有依赖关系,比如消息表依赖会话表先落库。这种依赖只能靠作业的跳来表达,转换里没有这个概念。
作业的执行入口是另一套:
# Kitchen 执行作业;总作业再往下拉起分作业和转换 ./kitchen.sh \ -file=/opt/etl/job/sync_chat_main.kjb \ -level=Basic \ -param:BIZ_DATE=$(date -d "yesterday" +%Y-%m-%d) \ -logfile=/opt/etl/logs/job_$(date +%Y%m%d).log-file指向作业文件;-param:BIZ_DATE=...把日期参数注入进去,作业里用${BIZ_DATE}引用,并逐层透传给下游的转换——参数透传这一环经常被漏掉,导致子转换拿不到值,取数条件变成全量或者空;-level=Basic在调度场景里通常够用,出错时再针对性地改成 Detailed 重跑一次定位。
把增量条件做成参数而不是写死在转换里,还有个额外好处:补数的时候不用改文件,命令行传一个历史日期就能重跑那一天。
4.3 目录与脚本:Spoon、Pan、Kitchen、Carte 分别在什么位置
Kettle 是绿色免安装的,解压即用,这也意味着它的能力全摊在目录里,没有注册表和系统服务来帮你兜底。花十分钟认清目录,能省掉后面很多「为什么换个机器就跑不起来」的困惑。
| 目录 / 脚本 | 作用 |
|---|---|
| classes | 生命周期监听、注册表扩展、日志配置 |
| lib / libswt | 依赖 jar 包、图形界面库 jar 包 |
| plugins | 插件目录,步骤、作业项、数据库驱动都在这 |
| samples | 自带示例,学步骤用法比看文档快 |
| simple-jndi | JNDI 连接配置 |
| pwd | 集群相关配置 |
| system / ui / launcher / docs | 系统、界面、启动、文档 |
| spoon.sh / Spoon.bat | 图形界面入口 |
| pan.sh / Pan.bat | 执行转换 |
| kitchen.sh / Kitchen.bat | 执行作业 |
| carte.sh / Carte.bat | 集群与远程执行服务 |
| encr.sh / Encr.bat | 密码加密工具 |
| import.sh / Import.bat | 资源库导入 |
| set-pentaho-env.sh | 设置运行环境变量 |
| spoonDebug.sh | 以调试模式启动界面 |
真正跑在生产上的只有三个:pan、kitchen、carte。spoon 是开发期的工具,生产服务器上不需要图形环境,也不应该装。carte 是把执行能力暴露成一个常驻服务,用浏览器或者别的客户端远程触发,适合多台执行机统一调度。encr 用来把明文密码换成密文,配合配置文件使用,避免密码裸奔在转换文件里。import 用来把文件方式的资源导进资源库。
绿色免安装的另一面是环境一致性全靠自己管。lib 里塞的 jar 包、JVM 版本、字符集、时区,这四项只要有一项跟开发机不一致,就会出现「本地跑得好好的、服务器上连不上」这类问题。用启动脚本把环境变量钉死,是成本最低的防线:
#!/usr/bin/env bash set -euo pipefail # 任一环节失败立即退出,避免半截任务继续往下走 export JAVA_HOME=/opt/jdk # 固定 JVM,避免默认 java 版本漂移 export KETTLE_HOME=/opt/etl/kettle # 固定安装路径,脚本里所有相对路径以此为基准 export LANG=zh_CN.UTF-8 # 字符集固定,防止中文乱码 export TZ=Asia/Shanghai # 时区固定,防止日期整体偏移 BIZ_DATE=$(date -d "yesterday" +%Y-%m-%d) cd "$KETTLE_HOME" ./kitchen.sh -norep \ -file="$KETTLE_HOME/job/sync_chat_main.kjb" \ -param:BIZ_DATE="$BIZ_DATE" \ -level=Basic \ -logfile="$KETTLE_HOME/logs/$(date +%Y%m%d).log"四个环境变量的作用各不相同:JAVA_HOME决定用哪个 JVM;KETTLE_HOME让脚本不依赖当前目录;LANG影响字符串解码,不设或者设错会导致中文列整列乱码;TZ影响 Date 类型的解释,跨时区时这一项不固定,同一批数据每天的落库时间都可能不一样。-norep让执行走文件方式,不连资源库,少一个故障点。
这段脚本丢进 crontab 就能当定时任务用。但要注意 crontab 的环境变量是空的,date、java这些命令的路径在有些发行版上会找不到,所以脚本内部必须用绝对路径或者显式 export,别指望交互式 shell 里的那套环境。
5. 排查与避坑:Kettle 同步任务最常见的几种翻车
跑通和跑稳是两件事。下面这几条是按「现象 → 原因 → 解决」整理出来的高频问题,多数不是配置写错,而是对底层模型的误解。
5.1 配置与结构层面的坑
坑一:任务跑到某个步骤就不动了,内存一路涨。
现象:日志停在某个步骤没有新输出,进程内存持续上升,最后可能被系统 OOM 杀掉。原因:链路里出现了阻塞型步骤,比如排序记录、分组、去重这类必须先读完所有输入才能吐出第一行的步骤。它们跟「行集流式处理」的模型天然冲突,输入行集满了上游就停,而这个步骤又迟迟不输出,整条链路就这么僵住,中间数据全压在自己身上。解决:优先把排序、分组下推到数据库做,用 SQL 完成;确实要在 Kettle 里做的,改成分批处理,用「分组」的流式替代品,或者按业务键切片成多个小批次循环跑。加内存和调大行集都只是延后爆炸时间。
坑二:想在转换里控制执行顺序,结果顺序完全不听话。
现象:把「清空目标表」的步骤放在最上面,实际执行时数据先写进去了,然后才被清掉,最终目标表是空的。原因:转换里所有步骤并发启动,画布位置不代表任何执行顺序,两个没有跳连接的步骤更是完全独立。解决:把需要顺序的动作拆开,用作业串起来——作业项一是「执行 SQL 清空表」,作业项二才是执行数据转换的转换。这是模型层面的约束,不是配置能绕过去的。
坑三:本地跑通,换台机器连不上数据库。
现象:开发机上一切正常,部署到服务器上连接全部失败。原因:连接信息写在用户目录下的配置里,或者配在 simple-jndi 目录里,跟着账号和机器走,换台机器就丢了;也可能是驱动 jar 放在 plugins 里,迁移时只拷了转换文件没拷驱动。解决:连接统一用变量管理,转换里写${DB_HOST}这类占位符,实际值在统一的配置文件或启动脚本里注入;迁移时把 plugins 目录下的驱动一起带上,并核对驱动的版本与目标库版本是否匹配。
5.2 运行时与数据层面的坑
坑四:目标库某个字段全是 null,日志一条错都没有。
现象:同步任务每天成功,行数也对得上,但目标库某个日期列或者数值列整列为空。原因:源端实际值与元数据声明的格式对不上,解析失败时 Kettle 选择给 null 继续跑,不中断流程。常见触发点是源端混入了0000-00-00、空串、或者小数点和千分位符号反了。解决:在输入步骤之后接「数据检验」步骤,对关键字段做非空和格式校验,把不合格的行分流到错误跳并落文件;同时开 Detailed 日志跑一次,看解析告警。校验步骤的成本很低,但能把「静默损坏」变成「显式失败」。
坑五:增量同步偶尔少几条数据,重跑又不一定复现。
现象:每天凌晨按时间戳拉增量,绝大多数时候正常,偶尔少几条,且第二天不会自动补上。原因通常是边界处理:条件用了>而不是>=,恰好落在边界那一秒的数据被跳过;或者抽取时刻早于源库最后一笔事务提交,那笔数据既不在今天的时间窗里,也不在明天的时间窗里。解决:条件改成>=并在下游按主键去重,或者留一个重叠窗口(每次往前多取几分钟),再或者干脆用源库的自增标识列而不是时间戳做增量依据。这道题没有完美解,关键是选一个代价可接受、且丢数据能被发现的方案。
6. 设计取舍:并发换吞吐,显式换确定性
回头看,Kettle 这套设计几乎每一处都在做同一种交换——拿一部分「直觉上的可控性」去换「大数据量下的可行性」。
| 设计选择 | 得到什么 | 付出什么 |
|---|---|---|
| 步骤级线程 + 有界行集 | 内存占用与总数据量解耦 | 步骤间没有执行顺序,阻塞型步骤成隐患 |
| 元数据显式声明 | 类型问题能在步骤内被拦截 | 每个字段都要配格式、长度、精度 |
| 转换与作业分离 | 数据流和控制流各用各的模型 | 一个业务任务要维护两类文件 |
| 绿色免安装 | 部署就是解压,无侵入 | 环境一致性全靠自己管 |
| 插件化架构 | 步骤可以无限扩展 | 版本兼容和 jar 冲突要自己扛 |
值得多说的是第一行。把 OOM 变成阻塞,这个转换看着不起眼,实际影响很大:内存溢出是一个「结果性」故障,出问题的时候进程已经死了,现场没了;而有界行集带来的阻塞是一个「过程性」故障,任务卡着但进程还活着,你能连上去看线程栈、看每个行集里堆了多少行、看哪个步骤的读入写出不匹配。同样是一次故障,一个给你留了现场,一个不留。做数据同步这行久了会格外看重这一点——出问题不可怕,怕的是没法定位。
第三行的取舍也常被低估。转换和作业分成两套模型,学习成本确实上去了,但它换来的是两个维度各自可以独立演进:数据加工逻辑变了,只动转换;调度策略变了,只动作业。如果硬用一套模型同时表达这两件事,最后的产物一定是一个既难调顺序、又难改字段的巨型文件。
验证的方式很朴素:随便挑一个线上跑的同步任务,把它的转换打开,逐个步骤问三个问题——这个步骤会不会阻塞?它的输出行集满了会怎样?它的字段元数据里格式和精度是谁定的?三个问题都能答上来,这条链路基本就是可控的;答不上来的那一环,就是下一个凌晨告警的来源。
提示:判断一个转换稳不稳,不看步骤多少,看有没有阻塞型步骤、看有没有校验环节、看参数是不是外部注入的。
我自己的习惯是,每接一个同步任务,先不打开界面,先在纸上把步骤的并发关系画一遍,标出哪几个是阻塞型的、哪几个会写文件、哪几个字段的元数据必须钉死,然后再回界面去拖。从那以后,我再没在凌晨被「任务卡住但进程还活着」这种电话叫起来过。这套画图加核查的习惯看着笨,但它把排查工作从运行时挪到了设计时,值。
希望帮到你。
本文还有配套的精品资源,点击获取