DataX这个同步工具,用好了确实省心,但写作业配置的时候,十个人里有八个会掉进JSON结构和参数语义的坑里。今天咱们就用大白话把那些容易踩的地方全捋一遍,争取下次写配置一次过。

一、先别急着写JSON,想想你要干什么

DataX是个离线数据同步工具,专门用来在各种数据源之间搬数据。比如把MySQL里的业务表搬到HDFS,或者把Oracle的表同步到另一个Oracle库,再或者把日志文件导入到数据库。它把一次同步任务叫一个job,而job的说明书就是一个JSON文件。JSON里写清楚从哪读、写到哪、并发多少、出错到多少算失败等等。

应用场景很典型:数仓建设、数据迁移、定时备份。优点是门槛低,缺点嘛,配置一旦复杂起来,JSON会变得很长,而且参数名里的“单复数”和“大小写”也容易让人头大。所以咱们先把这个JSON骨架摸清楚。

1.1 DataX JSON的“三层骨架”

一个标准的作业配置,最外层是job,要求很死板。job里面必须有一个content数组,数组里装着真正的同步任务。任务对象里必须同时有readerwriter,各自又包含nameparametername是插件名字,parameter是插件参数。job外面还可以放一个setting,用来控制速度、并发和错误容忍度。

先看一个最小配置,感受一下层级:

// 技术栈:DataX JSON
// 注意:JSON标准不支持注释,这里为了方便讲解才写的,实际跑的时候请删掉
{
  "job": {
    "content": [
      {
        "reader": {
          "name": "mysqlreader",
          "parameter": {
            "username": "root",
            "password": "123456"
          }
        },
        "writer": {
          "name": "hdfswriter",
          "parameter": {
            "fileType": "text"
          }
        }
      }
    ],
    "setting": {
      "speed": {
        "channel": 3
      }
    }
  }
}

别小看这几层,很多同学写的JSON长得像模像样,一运行就报错,原因就是层级搞错了。

二、JSON结构里最隐蔽的坑

2.1 content是数组,不是对象

这个错误我见过至少一百遍。有人把content写成了花括号包裹的对象:

// 技术栈:DataX JSON(错误示例)
{
  "job": {
    "content": {
      "reader": {
        "name": "mysqlreader"
      }
    }
  }
}

DataX严格规定content必须是数组,因为里面可以放多个同步任务(虽然通常只放一个)。正确写法就是用方括号包起来。万一你报错说“content不是数组”,先检查这里。

2.2 connection数组里的门道

readerwriter里的连接信息,很多参数是放在connection数组下的。以mysqlreader为例,connection是一个数组,每个元素是一个连接对象。对象里有jdbcUrltable等。但有人会把column也放进connection里,或者把username放到connection里,这些都是错的。usernamepasswordparameter的直系子级,column也是parameter的直系子级,而jdbcUrltable必须在connection数组里的对象中。

错误的配置就像下面这样:

// 技术栈:DataX JSON(错误示例)
"parameter": {
  "username": "root",
  "password": "123",
  "connection": {
    "jdbcUrl": "jdbc:mysql://localhost:3306/test",
    "table": "user",
    "column": ["id"]
  }
}

这里至少有两个问题:第一,connection应该是数组,不是对象;第二,column放错了地方。正确写法如下:

// 技术栈:DataX JSON(正确示例)
"parameter": {
  "username": "root",
  "password": "123",
  "column": ["id"],
  "connection": [
    {
      "jdbcUrl": ["jdbc:mysql://localhost:3306/test"],
      "table": ["user"]
    }
  ]
}

注意,jdbcUrltable还可以用字符串数组的形式,比如"jdbcUrl": ["jdbc:mysql://..."],千万别写成单个字符串,除非某些插件明确支持。具体看文档。

2.3 列名别“裸奔”

column数组里,如果列名是关键字,比如orderdesc,或者列名有空格,直接裸写会报SQL语法错误。需要你用反引号或者双引号包起来,具体看数据源。比如MySQL用反引号:

// 技术栈:DataX JSON(正确示例)
"column": [
  "`order`",
  "`desc`",
  "user_name"
]

如果列名里有特殊字符,比如user(name)这种,最好用反引号整个包住。不同数据库的转义符不一样,别记混了。

三、参数语义避坑:以MySQL到HDFS为例

参数结构搞对了,剩下的就是理解每个参数到底是干嘛的。拿最常见的MySQL同步HDFS来说。

3.1 reader的splitPk别乱用

splitPk是mysqlreader的一个关键参数,用来指定分片字段,好让数据按多个通道并行读取。这个字段必须是一个数字类型,而且最好分布均匀。如果表里没有合适的字段,就建议不要配置splitPk,让它单通道读,否则会调度失败。有一些人把主键id直接写上,但id并不严格递增(比如删除过记录),也可能导致分片严重不均匀。

示例:

// 技术栈:DataX JSON(推荐配置)
{
  "job": {
    "content": [
      {
        "reader": {
          "name": "mysqlreader",
          "parameter": {
            "username": "root",
            "password": "123456",
            "column": [
              "id",
              "user_name",
              "age"
            ],
            "splitPk": "id",
            "connection": [
              {
                "table": [
                  "test.user"
                ],
                "jdbcUrl": [
                  "jdbc:mysql://10.0.0.1:3306/test?useSSL=false"
                ]
              }
            ]
          }
        },
        "writer": {
          "name": "hdfswriter",
          "parameter": {
            "defaultFS": "hdfs://10.0.0.2:9000",
            "fileType": "text",
            "path": "/datahouse/user",
            "fileName": "user_data_",
            "writeMode": "append",
            "fieldDelimiter": "\t",
            "column": [
              {
                "name": "id",
                "type": "int"
              },
              {
                "name": "user_name",
                "type": "string"
              },
              {
                "name": "age",
                "type": "int"
              }
            ]
          }
        }
      }
    ],
    "setting": {
      "speed": {
        "channel": 4
      },
      "errorLimit": {
        "record": 10,
        "percentage": 0.01
      }
    }
  }
}

注意看这个配置。columnparameter里,不在connection里。writercolumn是数组对象,每个对象必须有nametypetype只能用DataX自己的类型,比如intstringdoublebooleandate。如果你写varchar,它会直接说不认识。

3.2 hdfswriter的writeMode,一个非常刺激的参数

writeMode有两种:truncateappendtruncate的意思是:同步之前,把path指定的目录先删了,再新建一个,然后写数据。如果你把path写成了某个重要目录的根路径,那跑一次,目录里的旧数据全没了。append则是直接追加。生产环境慎用truncate

还有,fileName不是最终文件名,而是前缀。DataX会为每个channel生成一个以这个前缀开头、后面跟编号和解散符的文件。如果你把fileName写成固定的比如user.csv,两个并发通道就会互相抢同一个文件,最后可能报错或者数据错乱。

3.3 HDFS路径别带结尾斜杠

path参数别写成/data/user/这种带斜杠的,DataX有时候会对路径做拼接,容易得到双斜杠或者歪掉的路径。统一写成不带结尾斜杠的。如果目录不存在,DataX不会自动创建多层,至少在某些插件版本里不会。最好提前用HDFS命令建好目录。

四、更典型的场景:读MySQL写MySQL

除了写HDFS,最常见的还有MySQL到MySQL。这种场景下,坑主要集中在主键和写入模式上。

4.1 完整示例

先看配置:

// 技术栈:DataX JSON(读MySQL写MySQL)
{
  "job": {
    "content": [
      {
        "reader": {
          "name": "mysqlreader",
          "parameter": {
            "username": "sync",
            "password": "sync123",
            "column": [
              "id",
              "name",
              "email",
              "create_time"
            ],
            "splitPk": "id",
            "connection": [
              {
                "table": [
                  "source.user"
                ],
                "jdbcUrl": [
                  "jdbc:mysql://192.168.1.10:3306/source?useSSL=false"
                ]
              }
            ]
          }
        },
        "writer": {
          "name": "mysqlwriter",
          "parameter": {
            "username": "sync",
            "password": "sync123",
            "writeMode": "replace",
            "column": [
              "id",
              "name",
              "email",
              "create_time"
            ],
            "connection": [
              {
                "table": [
                  "target.user"
                ],
                "jdbcUrl": "jdbc:mysql://192.168.1.20:3306/target?useSSL=false"
              }
            ]
          }
        }
      }
    ],
    "setting": {
      "speed": {
        "channel": 2
      },
      "errorLimit": {
        "record": 0,
        "percentage": 0.01
      }
    }
  }
}

这里面几个细节要注意。

writerjdbcUrl写成了字符串,而不是数组,这也是可以的,部分插件允许。但为了统一,建议也写成数组,因为有些版本解析字符串会报类型错误。

4.2 writeMode的语义陷阱

writeMode支持insertreplaceinsert就是纯插入,如果目标表有主键冲突,任务直接报错,触发的重试还会插重复数据(如果没主键)。replace是MySQL语法,遇到唯一键冲突时,先删除冲突行,再插入新行。很多人以为replace是“覆盖更新”,实际上它更像是“删了再插”。所以如果你的目标表有外键关联,replace可能会因为有行被删除导致外键约束失败。

另外,如果目标表没有主键,replace就会变成普通insert,白折腾。要想真正实现“按业务键更新”,DataX自带的mysqlwriter做不到,你只能通过preSql先执行一次update,但性能很差,或者换自研插件。

4.3 preSql不是万能药

preSql是在同步之前执行的一批SQL。很多人喜欢用它来清空旧数据,比如delete from target。但要注意,多个channel并发时,preSql可能被执行多次,如果你的SQL是drop table,第二个通道执行就直接报错。最好把破坏性操作放在DataX外面,用调度系统先跑清理脚本,再启动DataX。

五、关联技术:写配置前的自检习惯

配置写完了,别急着提交。这里有几个方法和工具,能帮你少踩很多坑。

5.1 先做JSON语法检查

最简单的,把.json文件拖进VS Code,如果语法有问题会直接标红。或者用命令行工具,但那就是另外的技术栈了,这里不提。总之,确保JSON本身合法是第一道关卡。很多诡异报错,其实就是末尾多了个逗号。

5.2 用DataX的“试跑”功能

DataX有一个-dry参数,可以只建立连接、检查配置,不真正读写数据。具体运行方式你可以在网上查,我这里不敲命令,因为一敲就串技术栈了。重点是这个功能真的有用,能提前发现数据源连不上、表不存在、字段配错等问题。正式同步前,先dry跑一遍,心中不慌。

5.3 日志里藏着真正的线索

如果同步中途失败,别只盯着控制台第一行。DataX的日志会告诉你“第几行配置有问题”或者“哪个字段类型不认识”。而且报错信息里经常包含SQL语句,你可以把SQL复制到数据库客户端里自己跑一下,看看是不是参数名拼错了。

六、DataX的优点、缺点和注意事项

6.1 优点

第一,插件丰富。常见的MySQL、SQLServer、PostgreSQL、HDFS、OSS、HBase都有对应插件,生态比较成熟。第二,配置简单,比写复杂的MapReduce或者Spark程序要省事得多。第三,并发控制还算灵活,可以通过channel数量调整同步速度。第四,自带脏数据容忍机制,可以设置最多容忍多少条错误记录,避免一点点小毛病就让整个任务挂掉。

6.2 缺点

第一,JSON配置维护成本高。同步几十张表,每张表一个JSON文件,写起来像在做数据录入。第二,实时性差,它天生是离线批量工具,不适合做秒级同步。第三,ETL能力弱,它主要是“搬数据”,不是“算数据”,复杂的转换逻辑要么写SQL,要么写UDF,但UDF支持有限。第四,资源占用不好控制,通道开多了,内存和连接数会直线上升,弄不好就把源库压垮。

6.3 注意事项

生产环境使用,有几个铁律要记住。

第一,不要用root用户跑任务,权限太大,万一配置里把目录删错,后悔都来不及。第二,数据库密码不要明文写在配置文件里,尤其当配置文件放在版本仓库中时,至少要用环境变量替换,或者使用配置中心。第三,errorLimit里的参数要设置合理,recordpercentage是“或”的关系,设置太大,任务会在失败的海洋里游泳。第四,大表同步时,splitPk一定要选择高区分度的数字列,比如自增主键,否则会出现数据倾斜。第五,同步完一定要做数据校验,不能只看了DataX的“写入成功”就算完。

七、文章总结

写DataX作业配置,说到底就两件事:搞清楚JSON的层级骨架,搞明白每个参数的实际语义。掉进坑里不可怕,怕的是只改一个标点就开始狂试。建议你先拿最小配置跑通,再一点点加参数,每加一个就验证一次。这样即便出了问题,也知道是刚加的那刀出的血。

最后记住一句话:DataX的文档并不算太友好,但它的报错逻辑还算忠实,很多时候不是它乱报,而是我们的参数确实没写对。把今天说的这些点挨个检查一遍,你的作业配置至少能顺滑一大半。