☰
DataX 写入瀚高:highgowriter 插件部署、调优与避坑
2026/10/28 11:04:08 网站建设 项目流程

简介:面向 DataX 数据同步场景的瀚高数据库写入插件包,主要服务于需要将数据从异构数据源同步至瀚高数据库的开发与运维人员。原生 DataX 未内置瀚高 writer 支持,该插件补齐写入链路,适合在数据集成、迁移和日常同步任务中使用,技能层级偏向具备一定 DataX 使用经验的开发者。包内共 17 个文件,以 15 个 jar 和 2 个 json 为主,整体约 8.15MB:jar 覆盖 highgowriter 核心实现、DataX 公共与关系型数据库工具、日志与序列化依赖,以及瀚高 JDBC 驱动;json 则对应插件描述与作业模板,便于识别插件和快速组织同步任务。目前已有 626 人学习,说明其在瀚高数据库接入 DataX 的实际场景中具有一定参考价值。读者可获得完整包体、依赖清单与模板配置,减少自行适配瀚高写入接口时的摸索成本,也便于对照排查驱动版本与依赖冲突问题。

1. 这类链路卡住,多半不是数据库的问题

源库能连、目标库也能连,DataX 一跑却报找不到 writer 插件——做国产库替换的项目里,这个场景隔三差五就会冒出来。业务库迁到瀚高(HighGo),同步作业还是原来那套 DataX 配置,可官方插件清单里没有 highgowriter,链路就停在最后一百米。highgowriter.zip 补的正是这个缺口:一个按 DataX 插件规范落地的 writer 实现,放进 plugin/writer 目录就能被框架当原生插件实例化,把 MySQL、Oracle、CSV 的批量数据按批灌进瀚高。适合三类人:正在推数据库替换的迁移工程师、维护离线同步链路的数仓同学,以及手上只有 zip、没有源码,需要先把链路跑通再谈优化的运维。它不是独立程序,也不带调度,别指望解压完双击就能跑。

2. 拆开压缩包:目录骨架、三层对象与驱动位置

拿到一个第三方插件包,先别急着往 DataX 里拷。DataX 的插件加载机制很朴素也很死板——目录名、plugin.json、job.json 三处的名字只要有一处对不上,框架就直接告诉你“找不到插件”,而不会告诉你到底哪一处写错了。这一章把包拆到能看懂的程度:目录该长什么样、Java 侧靠哪三个类串起读写、驱动 jar 为什么不能塞进插件自己的包里。

2.1 plugin.json 是唯一入口,name 和目录名必须对上

DataX 扫描$DATAX_HOME/plugin/writer/下的每个子目录,读目录里的plugin.json,取class字段做反射实例化。所以下面三个值必须完全一致:插件目录名、plugin.json里的name、作业 JSON 里writer.name。少一个字符,报错信息都是同一句Code:[Framework-14], Description:[您提供的数据源插件无法被加载]。

先不解压,直接看包内层级:

# 看压缩包的目录层级,确认解压后不会多套一层 highgowriter/highgowriter/ unzip -l highgowriter.zip | head -30 # 直接读插件描述文件,比解压再翻目录快 unzip -p highgowriter.zip '*/plugin.json'

一份典型的 plugin.json 长这样:

{ "name": "highgowriter", "class": "com.alibaba.datax.plugin.writer.highgowriter.HighGoWriter", "description": "write data to HighGo Database via JDBC", "developer": "third-party" }

name决定目录名和作业里的引用名,改它就得三处一起改;class是 Writer 主类的全限定名,框架拿它Class.forName加newInstance;description和developer只用于日志展示,不影响加载。第三方包最容易出问题的是class——有人拿模板改了包名却没同步改 json,或者 json 里写的是旧版本类名。

核对类名最直接的办法是看 jar 里到底有什么:

# 从 zip 里抽出插件 jar 再列类,注意内部类命名是 Outer$Inner.class unzip -o highgowriter.zip -d /tmp/hg unzip -l /tmp/hg/highgowriter*.jar | grep -Ei 'HighGoWriter'

正常会看到HighGoWriter.class、HighGoWriter$Job.class、HighGoWriter$Task.class三个条目。如果只看到一个主类,说明这个包把 Job/Task 拆到了别的类里,那就以实际的全限定名为准去改 plugin.json,不要照抄任何示例。

2.2 Writer、Job、Task 三层:谁连库,谁攒批

插件主体继承com.alibaba.datax.core.transformer.AbstractTransformer体系的 Writer 抽象类,标配是三个静态内部类,分工很清楚:外层 Writer 是工厂,Job 层做一次性的全局准备(连库、校验表结构、拼列名、切分任务),Task 层才是真正跑在每条 channel 上的执行体(建连接、开事务、攒批、flush)。

public class HighGoWriter extends Writer { @Override public Writer.Job job() { return new HighGoWriter.Job(); } public static class Job extends Writer.Job { @Override public void init() { // 校验 writer.parameter 里的必填项:username/password/column/connection this.originalConfig = super.getPluginJobConf(); } @Override public void prepare() { // 全局只做一次:加载驱动、校验表存在、必要时按 preSql 预处理 } @Override public List<Configuration> split(int mandatoryNumber) { // 返回 mandatoryNumber 份 taskConfig,DataX 按 channel 并发消费 return Collections.nCopies(mandatoryNumber, this.originalConfig); } @Override public void post() { /* 全部 task 结束后执行 postSql */ } @Override public void destroy(){ /* 释放 Job 层资源 */ } } public static class Task extends Writer.Task { @Override public void init() { // 从 taskConfig 取参数,Class.forName 显式加载驱动 } @Override public void prepare() { // 建 Connection、prepareStatement、执行当前 task 的 preSql } @Override public void startWrite(RecordReceiver lineReceiver) { // 循环 receiver.getFromReader(),攒够 batchSize 调 addBatch/executeBatch } @Override public void post() { /* 执行 postSql、提交剩余批次 */ } @Override public void destroy(){ /* 关连接、清缓冲 */ } } }

调用的先后顺序是:Job.init → Job.prepare → Job.split,然后每个 Task 各自 init → prepare → startWrite → post → destroy,最后回到 Job.post。这个顺序决定了三件事:写库的账号权限、表是否存在、preSql要不要清表,都该在Job.prepare里先验一遍,失败了立刻停,而不是等每条 channel 跑到一半再集体报错。

split的返回值直接决定并发度。写库插件通常不需要按主键切片,直接按 channel 数复制配置就行,mandatoryNumber由作业里的setting.speed.channel决定。这也是为什么把 channel 从 1 调到 8,瀚高侧会突然多出 8 个并发会话和新一轮锁竞争——切分是在这里复制出来的。

2.3 驱动放 libs,别塞进插件 jar

DataX 给每个插件目录单独准备一个 URLClassLoader,插件目录下的libs/*.jar和插件主 jar 都在它的搜索路径里。因此驱动 jar 有两种摆放方式:打进插件 fat jar,或者放进libs/。实际维护时我一般选后者——驱动升级只换一个文件,不用重编插件,回滚也简单。

# 目录落位:主 jar、plugin.json、模板放根目录,驱动放 libs cd $DATAX_HOME/plugin/writer/highgowriter cp /path/to/highgowriter-1.0.0.jar . cp /path/to/plugin.json . cp /path/to/plugin_job_template.json . mkdir -p libs && cp /path/to/highgo-jdbc-*.jar libs/ # 落完看一眼,缺哪个后面都会报 ClassNotFound ls -l . libs/

驱动类名和连接串前缀需要以你手上那份驱动为准。瀚高走 PG 协议,常见的驱动类是com.highgo.jdbc.Driver,连接串形如jdbc:highgo://host:5866/dbname;用 PG 原生驱动org.postgresql.Driver配jdbc:postgresql://也能跑通大部分场景,但服务端版本相关的特性会有差异。判断办法只有一个:解开驱动 jar 看META-INF/services/java.sql.Driver里注册的是哪个类名。

注意:DriverManager只会加载调用方 ClassLoader 可见的驱动。插件 ClassLoader 里注册的驱动,在某些 DataX 版本下不会被外层感知,会出现“驱动明明在 libs 里却报 No suitable driver”。稳妥做法是在Task.init里显式Class.forName(driverClassName),再用DriverManager.getConnection之前的实例方式拿连接。

3. 编译与打包:从源码到能加载的插件目录

zip 里如果带了源码,第一件要做的事不是直接mvn package,而是先确认它依赖哪个版本的 DataX 核心包、驱动用什么坐标引入。第三方插件最常见的失败不是代码写错,是编译环境跟运行时环境对不上:编译时依赖 3.x 的 core,运行时装在 2.x 的 DataX 里,类能加载,跑到一半抛NoSuchMethodError,排查起来非常费劲。

3.1 依赖先对齐,驱动装进本地仓库

插件编译需要 DataX 的核心包提供Writer、RecordReceiver、Configuration这些基类。常见做法有两种:把 DataX 源码整体mvn install到本地仓库,或者直接用官方发布的datax-common、datax-core坐标。前者的好处是版本绝对对得上——你装的是哪份 DataX,就用哪份源码编。

# 方式一:在 DataX 源码根目录装核心包到本地仓库 cd datax-src && mvn -q clean install -DskipTests -Dmaven.javadoc.skip=true # 方式二:驱动不走远程仓库,直接 install 到本地 mvn install:install-file \ -Dfile=highgo-jdbc.jar \ -DgroupId=com.highgo \ -DartifactId=highgo-jdbc \ -Dversion=1.0.0 \ -Dpackaging=jar

pom 里对应的依赖块,注意把驱动设成provided——运行时由libs/提供,避免同一个驱动类被两个 ClassLoader 各加载一份:

<dependency> <groupId>com.highgo</groupId> <artifactId>highgo-jdbc</artifactId> <version>1.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.alibaba.datax</groupId> <artifactId>datax-common</artifactId> <version>${datax.version}</version> <scope>provided</scope> </dependency>

datax.version必须和运行环境一致,这一条比什么都重要。拿不准就去运行机上找:ls $DATAX_HOME/lib/ | grep datax能看到核心 jar 的版本号,照着写。

3.2 打包三个开关:跳测试、指定 profile、别打依赖

# 打包:跳测试省时间,插件本身没有单测就别硬跑 mvn -q clean package -DskipTests -Dmaven.test.skip=true # 产物确认:正常只有一个插件 jar,体积通常在几十 KB 量级 ls -lh target/*.jar

如果打出来的 jar 有几十 MB,说明把依赖全塞进去了,需要检查 pom 里有没有误用maven-shade-plugin合并数据源相关包。插件 jar 应该保持干净,只含自己的类,驱动和第三方依赖统一交给libs/管。

组装插件目录这一步建议写成脚本,重装、换机器、回滚都靠它:

#!/usr/bin/env bash set -e DATAX_HOME=${DATAX_HOME:?请先导出 DATAX_HOME} PLUGIN_DIR=$DATAX_HOME/plugin/writer/highgowriter rm -rf "$PLUGIN_DIR" mkdir -p "$PLUGIN_DIR/libs" cp target/highgowriter-*.jar "$PLUGIN_DIR/" cp src/main/resources/plugin.json "$PLUGIN_DIR/" cp src/main/resources/plugin_job_template.json "$PLUGIN_DIR/" cp lib/highgo-jdbc-*.jar "$PLUGIN_DIR/libs/" echo "installed -> $PLUGIN_DIR" ls -R "$PLUGIN_DIR"

脚本里的set -e别省,中间任何一步失败立刻退出,否则会出现“目录建好了、jar 没拷进去”的半成品状态,后面加载报错反而更难查。

3.3 装完先自检:模板回显加最小冒烟作业

插件装好不要直接上生产作业。先用框架自带的模板回显验证插件可被识别:

# 只要不报 Framework-14,就说明 plugin.json、类名、目录名三处对上了 python $DATAX_HOME/bin/datax.py -r streamreader -w highgowriter

回显内容里会带出plugin_job_template.json的字段结构,这一步顺便确认模板里的参数名和你打算写的作业 JSON 是否一致——第三方插件的模板经常和实际解析代码不同步,模板里写着batchSize,代码里读的是batchsize,跑起来就是静默用默认值。

接着用 streamreader 造几条数据打过去,不依赖任何源库:

{ "job": { "setting": { "speed": { "channel": 1 } }, "content": [{ "reader": { "name": "streamreader", "parameter": { "column": [ {"value": "1", "type": "long"}, {"value": "smoke", "type": "string"} ], "sliceRecordCount": 5 } }, "writer": { "name": "highgowriter", "parameter": { "username": "app", "password": "******", "column": ["id", "name"], "writeMode": "insert", "connection": [{ "table": ["public.t_smoke"], "jdbcUrl": "jdbc:highgo://127.0.0.1:5866/demo" }] } } }] } }
python $DATAX_HOME/bin/datax.py job/highgo_smoke.json

跑完去库里select count(*) from public.t_smoke,5 条就是对的。冒烟作业的价值在于把问题关在最小范围里:这一步报错只可能是驱动、连接串、表结构、权限四类问题之一,比拿几百万行的生产作业试错省太多时间。

4. 作业 JSON:从 MySQL 到瀚高的参数逐项拆解

写库插件的配置看着字段多,真正会影响结果的其实就六个:连接串、表名、列顺序、写入模式、批大小、前后置 SQL。剩下的都是锦上添花。这一章给一份能直接改改就用的完整作业,然后把每个参数摊开说清楚改了会发生什么。

4.1 一份可以直接改的 job.json

{ "job": { "setting": { "speed": { "channel": 4, "byte": 1048576 }, "errorLimit": { "record": 10, "percentage": 0.02 } }, "content": [{ "reader": { "name": "mysqlreader", "parameter": { "username": "ro", "password": "******", "column": ["id", "name", "amount", "updated_at"], "splitPk": "id", "connection": [{ "table": ["t_order"], "jdbcUrl": ["jdbc:mysql://10.0.0.11:3306/demo"] }] } }, "writer": { "name": "highgowriter", "parameter": { "username": "app", "password": "******", "column": ["id", "name", "amount", "updated_at"], "writeMode": "insert", "batchSize": 2048, "preSql": ["truncate table ods.t_order_tmp"], "postSql": ["analyze ods.t_order_tmp"], "connection": [{ "table": ["ods.t_order_tmp"], "jdbcUrl": "jdbc:highgo://10.0.0.21:5866/demo" }] } } }] } }

reader 侧的connection[0].jdbcUrl是字符串数组(分库分表用),writer 侧通常是单个字符串。这一点照抄示例就容易出错,配置校验会直接报类型错误。

4.2 writer 参数表:默认值和你该改的值

参数必填常见默认说明与调整建议
username / password是无写库账号,需要目标表的 INSERT 权限,用到 preSql 还要 TRUNCATE 权限
column是无顺序必须与 reader 的 column 一一对应,不按列名匹配,错位就会静默写错数据
connection[0].jdbcUrl是无jdbc:highgo://host:5866/db,需要指定模式时带?currentSchema=ods
connection[0].table是无支持schema.table,不带 schema 时按 search_path 解析
writeMode否insertinsert / replace / update 三选一,语义差异见 4.3
batchSize否1024 左右单次 executeBatch 的记录数,512~4096 之间调,别指望它当并发用
preSql / postSql否无数组,可多条;preSql 在 Task.prepare 执行,postSql 在收尾执行
session否无建连后执行的会话参数,用来调超时、只读、work_mem 之类

几个容易忽略的点。column不做名字匹配,只按位置绑定,reader 多一列或少一列都不会报错,只会把数据写到错误的字段上,这种问题往往要等业务方发现数据不对才暴露。preSql里写truncate要注意它在多 channel 下会被每个 Task 各执行一次,四个并发就是四次 truncate——如果原意只是初始化一次,把它挪到Job.prepare里执行。session里设statement_timeout是个好习惯,避免某条慢 SQL 把连接一直占着。

4.3 写入模式与类型映射:冲突和精度都在这里

writeMode的三个取值对应三种完全不同的行为:

  • insert:纯 INSERT。目标表已有同主键数据会抛唯一约束冲突,作业按errorLimit判断是继续还是终止。适合先清表再灌的 ODS 场景。
  • replace:按主键判断存在则覆盖(常见实现是先删后插或走冲突更新)。前提是目标表必须有主键或唯一索引,否则等价于 insert。
  • update:只更新已存在的行,不插入。适合做增量补数,但新数据会被静默丢弃。

类型映射这块,DataX 内部只有六种类型,落到瀚高要按下面的对应关系建表,别指望插件帮你做隐式转换:

DataX 内部类型瀚高字段类型备注
Longint8 / numeric超过 19 位用 numeric,别硬塞 bigint
Doublefloat8 / numeric金额类一律 numeric,float8 会有尾差
Stringvarchar / text超长截断是常见事故点,先确认目标列长度
Booleanbool源端 0/1 需要在 SQL 或 reader 侧转换
Datetimestamp / date时区问题在跨时区部署时必现
Bytesbytea大字段建议单独走表,别混在宽表里

5. 避坑与排查:五个把任务卡在中途的现场

插件跑起来之后,问题基本都集中在连接层和写入语义两层。下面五条是按出现频率排的,每条都给现象、原因和处置办法,照着对号入座即可。

5.1 连接层的两个死结

第一条:启动即 ClassNotFound / No suitable driver

现象:作业刚提交就抛ClassNotFoundException: com.highgo.jdbc.Driver,或者驱动在 libs 里却报No suitable driver found for jdbc:highgo://...。

原因:前一种是驱动 jar 没进libs/,或者 plugin.json 的class写错导致插件根本没加载;后一种是驱动类没被DriverManager感知,插件 ClassLoader 里注册的驱动在部分 DataX 版本下不会被外层看到。

解决:先ls libs/确认驱动文件在,再在Task.init里显式Class.forName("com.highgo.jdbc.Driver")。若仍旧不行,改成用驱动实例直接建连,绕开DriverManager的服务发现:

// 显式注册,避免 DriverManager 在插件 ClassLoader 下看不到驱动 Class.forName(driverClassName); Connection conn = DriverManager.getConnection(jdbcUrl, user, password);

第二条:relation does not exist,但表明明建了

现象:任务跑起来后报关系 "t_order" 不存在,用客户端连上去select * from t_order又是正常的。

原因:瀚高沿用 PG 的标识符规则,建表时加双引号会保留大小写,"T_Order"和t_order是两张表;另外作业账号的search_path可能和工作账号不同,不带 schema 的表名解析到了别的模式。

解决:作业里的表名一律小写并显式带 schema,如ods.t_order;连接串加?currentSchema=ods兜底。建表脚本里也尽量避免双引号包裹混合大小写的名字。

5.2 写入语义的三个坑

第三条:主键冲突导致整批回滚,日志里一堆脏数据

现象:作业跑到 80% 突然终止,错误信息是唯一约束冲突,errorLimit计数瞬间飙到阈值。

原因:writeMode=insert而目标表里已有历史数据,或者上一次作业中断后重跑,前一半数据已经落库。

解决:重跑场景改用replace,或者用preSql按分区清理再灌;把errorLimit的记录数临时调大观察脏数据分布,确认是幂等性问题还是数据本身重复,不要直接调成无限大把问题盖过去。

第四条:重跑之后数据翻倍,但没有报错

现象:作业显示成功,业务方查数发现金额多了一倍。

原因:preSql写在 writer 参数里,多 channel 下每个 Task 各执行一次;如果 preSql 是delete from t where dt = '...'这类语句,四个并发同时删、同时插,时序不可控。

解决:全局只执行一次的清理逻辑放到Job.prepare;一定要用 writer 的 preSql 时,把 channel 降到 1,或者改成按 channel 分段的条件删除。幂等性最好由目标表的唯一键或分区交换来保证,别依赖执行顺序。

5.3 性能和资源的一条

第五条:大批量时内存上涨、连接被服务端断掉

现象:作业跑十几分钟后 DataX 侧堆内存持续上涨,或者瀚高日志里出现连接被终止,客户端报connection reset。

原因:batchSize设得过大(有人直接写 20000)导致单批对象堆在内存里;同时 channel 数又开得很高,几十个连接各自攒着大批次,服务端内存和锁等待一起被拉满。

解决:batchSize回到 512~2048,靠channel提并发而不是靠单批体积。监控上先看 DataX 的 JVM 参数(-Xms2g -Xmx2g起步),再看瀚高侧的活跃会话数。调参顺序永远是先定 channel,再扫 batchSize,反过来做没有可比性。

6. 提速与验证:一次扫参加一份回归清单

参数调优最忌讳拍脑袋。我一般固定一个中间规模的表(百万级、字段 8~10 个),用循环扫 batchSize,同时记录每个组合的耗时和服务端活跃会话峰值:

#!/usr/bin/env bash # 扫参:channel 固定,batchSize 从 512 扫到 4096 for bs in 512 1024 2048 4096; do sed "s/\"batchSize\": [0-9]*/\"batchSize\": $bs/" job/highgo_tpl.json > job/run_$bs.json start=$(date +%s) python $DATAX_HOME/bin/datax.py job/run_$bs.json > log/run_$bs.log 2>&1 echo "batchSize=$bs cost=$(( $(date +%s) - start ))s" done

关键在sed那行——把模板作业复制一份再改参数,永远不要直接改生产作业文件,扫完参回头找不到基线是常有的事。日志统一落到log/目录,出问题直接按文件名定位那次运行。

记录成表之后,通常能看到一条明显的拐点曲线:batchSize 从 512 涨到 2048 耗时明显下降,再到 4096 收益就很小甚至反弹,反弹点往往对应服务端开始出现临时文件落盘。channel 的拐点更陡,一般到目标库 CPU 或磁盘 IO 打满就停,继续加只会让锁等待时间变长。

验证环节我固定跑这几项,任何一项不过就不往下推:

检查项做法判定标准
行数一致源表与目标表 count 对比完全相等,误差为 0
抽样字段随机取 100 行比对金额、时间字段无精度差、无时区偏移
幂等重跑同一作业连跑两次目标表行数不变
断点续跑中途 kill 后重跑不出现重复主键数据
空值与特殊字符构造含 NULL、换行、单引号的测试行原样落库,无截断

最后说个血泪教训。早几年做一次库替换,我把column顺序按 reader 的字段重新排了一遍“为了好看”,结果 reader 侧有个字段是后加的,两边错位一列,几十万行数据静默写进了错误的字段,直到业务方对账才发现——插件全程没报一个错。从那以后我每次上生产作业前,都强制先跑一遍 streamreader 的冒烟作业加 100 行抽样比对,确认列序、类型、幂等三件事都对,再放开并发。慢这十分钟,比事后回滚一整天划算得多。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询