简介:包含两个即用型DataX任务配置文件:import_mysql_to_hdfs.实现MySQL表全量导出为文本格式存入HDFS,export_hive_to_mysql.支持将Hive分区表或外部表数据写回MySQL。已预处理Null值兼容问题——Hive中以\N标识的空值,在导入MySQL时自动转为标准NULL,核心通过”nullFormat”: “\N”参数生效。配置规避了Hive源表为空导致任务失败的常见异常,建议执行前校验源数据存在性。所有配置基于DataX 3.0+版本实测通过,路径需按实际环境调整(如/opt/module/datax/),运行命令统一为python /opt/module/datax/bin/datax.py /opt/module/datax/job/xxx.。支持字段映射、列筛选、并发数设置等基础ETL能力,不依赖HiveServer2,直连HDFS文件系统或MySQL JDBC。配套提供run_etl.py脚本用于快速触发任务,以及requirements.txt说明依赖环境。适用于离线数仓建设中MySQL与Hive/HDFS之间的周期性数据同步、历史数据迁移、ODS层接入等典型场景。
1. 项目概述:为什么在离线数仓里,DataX仍是跨引擎搬运的“稳态选择”
在我们团队搭建第三代离线数仓的两年多时间里,从MySQL业务库往Hive数仓层同步订单、用户、商品等核心表,再到把清洗后的宽表回写到MySQL供BI报表直连,几乎每天都在和数据搬运打交道。你可能听过Flink CDC、Spark Structured Streaming这些实时方案,也见过Airflow调度Doris或StarRocks的时髦组合——但当你真正面对一个需要稳定跑三年、月均处理20TB+历史数据、且DBA只允许走离线通道的生产环境时,DataX依然是我第一个打开的配置文件夹。
它不是最炫的,但足够“不惹事”。不依赖HiveServer2意味着不用跟YARN资源队列抢内存,不走Thrift协议就避开了HS2连接池超时、Metastore锁表这类玄学故障;直读HDFS文件+JDBC写入MySQL的架构,让整个链路像一根结实的麻绳——没有中间件、没有状态服务、没有心跳检测,只有输入路径、输出SQL、字段映射和一行python datax.py job.json。这次整理的两个配置模板——import_mysql_to_hdfs.json和export_hive_to_mysql.json,就是我们在线上跑了47个调度周期、覆盖12个业务系统的“最小可行搬运单元”。
关键词里的DataX、MySQL同步、Hive同步、HDFS同步、数据搬运,不是泛泛而谈的技术标签,而是每个字都对应着真实踩过的坑:比如nullFormat: “\N”这个参数,表面看只是个字符串,实则牵扯Hive文本序列化格式(TextFile)、MySQL JDBC驱动对NULL的识别逻辑、以及DataX Reader/Writer插件间的数据类型桥接机制;再比如“规避Hive源表为空导致任务失败”,背后是我们某次凌晨三点被告警叫醒,发现一个分区目录下只有_SUCCESS文件、没有实际数据块,DataX默认报java.lang.NullPointerException: null而非优雅退出——这种细节,文档不会写,但线上会炸。
这两个模板适用于三类典型场景:第一是ODS层建设初期,把MySQL全量快照导成Hive外部表,作为数仓底座;第二是T+1调度中,将Hive清洗后的轻度聚合结果(如日活宽表)回写MySQL,供运营后台查询;第三是历史数据迁移,比如把三年前的老订单表从MySQL归档到HDFS冷存储,同时保留MySQL中最新三个月热数据。它们不解决实时性,但把“搬得准、搬得稳、搬得清”这件事做到了极致。接下来,我会带你一层层拆开这两个JSON配置的骨架,告诉你每个字段为什么这么写、不这么写会出什么问题、以及那些藏在注释里没说透的实操心法。
2. 整体设计思路与方案选型解析
2.1 为什么坚持用DataX而不是Spark或Sqoop?
很多人一上来就问:“现在都2024年了,为啥不用Spark SQL直接INSERT OVERWRITE TABLE hive_db.t1 SELECT * FROM mysql_db.t1?”这个问题我被问过至少37次,答案很实在:可控性、可追溯性、可中断性。
Spark作业一旦提交,就交给YARN调度器,你无法精确控制它读取MySQL时的fetchSize、无法指定HDFS写入时的block大小、更没法在写入第500万行时暂停检查中间状态。而DataX是纯内存管道模型:Reader从MySQL拉一批数据(比如10000行),经Transformer(此处为空)后,Writer批量刷入HDFS。每一步都可配置、可监控、可重试。更重要的是,它的JSON配置本身就是一份自解释的ETL契约——谁都能看懂这个任务干了什么,不需要翻源码或查Spark UI。
至于Sqoop,它在Hive 3.x之后对ORC/Parquet格式支持乏力,且必须依赖Hive Metastore服务。我们曾在一个测试环境尝试用Sqoop导入分区表,结果因为Metastore版本不兼容,生成的分区路径在Hive里根本SHOW PARTITIONS不出来。而DataX的hdfswriter直接操作HDFS API,写完文件立刻可用,连ALTER TABLE ADD PARTITION都不用手动执行。
提示:DataX 3.0+版本对Hadoop 3.x和Hive 3.x兼容性已大幅改善,但务必避开3.0.0-beta版本——该版本存在
hdfswriter在Kerberos环境下认证失败的Bug,我们实测升级到3.0.3后问题消失。
2.2 双向同步为何要拆成两个独立作业?
看到标题里“MySQL↔Hive/HDFS双向同步”,你可能会想:“能不能写一个配置,自动判断方向?”技术上当然可以(通过变量注入),但生产环境坚决不这么做。原因有三:
第一,职责分离。import_mysql_to_hdfs本质是“抽取+落地”,关注点在MySQL连接稳定性、大字段截断风险、时间字段时区转换;而export_hive_to_mysql本质是“加载+写入”,核心是Hive分区裁剪效率、MySQL主键冲突处理、批量插入的事务粒度。混在一起会让配置臃肿,调试时定位问题成本翻倍。
第二,调度解耦。MySQL到HDFS通常是T+1全量,凌晨2点跑;而Hive回写MySQL可能是T+1增量(比如只写当天新产生的订单宽表),凌晨4点跑。如果强行合并,调度系统要么重复执行无意义逻辑,要么增加复杂的条件分支,违背“简单即可靠”原则。
第三,权限收敛。DBA通常会给ETL账号分配最小权限:对MySQL只开放SELECT,对HDFS只开放目标目录的rwx。如果一个作业既读MySQL又写MySQL,权限申请流程会变长,且审计时难以追踪数据流向。
所以我们的设计是:两个JSON文件物理隔离、语义清晰、命名直白。import_mysql_to_hdfs.json永远只做一件事——把MySQL表变成HDFS上的文本文件;export_hive_to_mysql.json也只做一件事——把HDFS上某个路径的文件灌进MySQL表。这种“单职责”设计,让新人接手时,5分钟就能看懂任务逻辑。
2.3 Null值处理:\N背后的三重适配逻辑
Hive中空值显示为\N,这是TextFile格式的约定俗成,但MySQL原生不认这个符号。很多团队早期直接用sed 's/\\N/NULL/g'做预处理,结果发现时间字段2024-01-01 00:00:00里的空格也被替换了。而DataX的nullFormat参数,其实是打通了三个环节的桥梁:
- Reader端(mysqlreader):它不处理Null,只是忠实地把MySQL的NULL值传给下游;
- Transformer端(此处未启用):DataX默认无Transformer,数据原样透传;
- Writer端(hdfswriter):当遇到Java中的
null对象时,根据nullFormat配置,将其序列化为字符串\N写入HDFS文件; - 反向流程(export_hive_to_mysql):
hdfsreader读取文件时,碰到\N字符串,会主动将其转为Javanull对象;mysqlwriter再把这个null对象,通过JDBC的PreparedStatement.setNull()方法,正确写入MySQL的NULL。
这看似简单的参数,实则是DataX插件体系对“空值语义一致性”的深度封装。我们曾对比过三种方案:
- 方案A:不设nullFormat,HDFS文件里存空字符串"" → MySQL写入后变成'',非NULL;
- 方案B:设nullFormat为"NULL"(大写)→ Hive侧能识别,但MySQL JDBC驱动会把它当字符串插入;
- 方案C:设nullFormat: “\N” → 全链路闭环,零歧义。
注意:
\N必须用双引号包裹,且反斜杠需转义。在JSON里写作"nullFormat": "\\N",否则会被解析为字面量N。我们最初就栽在这儿——配置里写了"\N",但Python的json.loads()自动去掉了转义,最终生效的是"N",导致所有空值变成字母N。
2.4 为什么绕过HiveServer2?直连HDFS和JDBC的底层逻辑
文档里强调“不依赖HiveServer2”,这不是为了标新立异,而是源于一次血泪教训:某次集群升级Hive版本,HS2服务重启耗时18分钟,期间所有DataX任务排队等待JDBC连接,积压了62个作业。后来我们彻底转向直连模式,收益立竿见影:
-
HDFS直读:
hdfsreader使用org.apache.hadoop.fs.FileSystemAPI,通过core-site.xml和hdfs-site.xml配置访问NameNode,完全绕过Hive Metastore。它只关心“路径是否存在、是否有读权限、文件是否可分割”,不解析表结构、不校验分区元数据。这意味着即使Hive服务宕机,只要HDFS活着,数据就能搬。 -
MySQL直写:
mysqlwriter基于Druid连接池,支持rewriteBatchedStatements=true参数,能把1000条INSERT合并成一条批量语句发送,吞吐量提升5倍以上。而HS2的JDBC接口本质是Thrift RPC,每次写入都要走网络序列化,延迟高、不可控。
这种设计牺牲了一点“SQL抽象能力”(比如不能写SELECT * FROM db.t1 WHERE dt='20240101'),但换来了极致的稳定性和可观测性。你可以用hadoop fs -ls /data/hive/ods/mysql_orders/dt=20240101/直接确认数据落地,用mysql -e "SELECT COUNT(*) FROM ods_orders"验证写入结果,全程无需任何中间服务。
3. 核心配置细节与实操要点拆解
3.1 import_mysql_to_hdfs.json:MySQL全量导出到HDFS的完整配置
这个配置文件的目标很明确:把MySQL一张表的所有数据,按指定格式导出为HDFS上的文本文件,供后续Hive建外部表使用。我们以orders表为例,其结构为:
CREATE TABLE `orders` (
`order_id` bigint(20) NOT NULL,
`user_id` bigint(20) DEFAULT NULL,
`amount` decimal(10,2) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
对应的import_mysql_to_hdfs.json核心片段如下(已脱敏,关键参数加粗):
{
"job": {
"content": [
{
"reader": {
"name": "mysqlreader",
"parameter": {
"username": "etl_user",
"password": "etl_pass",
"connection": [
{
"jdbcUrl": ["jdbc:mysql://mysql-prod:3306/oms?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"],
"table": ["orders"]
}
],
"column": ["order_id", "user_id", "amount", "create_time", "update_time"],
"where": "",
"splitPk": "order_id",
"fetchSize": 10000,
"querySql": ""
}
},
"writer": {
"name": "hdfswriter",
"parameter": {
"defaultFS": "hdfs://mycluster",
"fileType": "text",
"path": "/data/hive/ods/mysql_orders/dt=${bdp.system.bizdate}",
"fileName": "orders",
"column": [
{"name": "order_id", "type": "long"},
{"name": "user_id", "type": "long"},
{"name": "amount", "type": "double"},
{"name": "create_time", "type": "string"},
{"name": "update_time", "type": "string"}
],
"writeMode": "append",
"fieldDelimiter": "\u0001",
"compress": "GZIP",
"nullFormat": "\\N",
"haveKerberos": false,
"kerberosKeytabFilePath": "",
"kerberosPrincipal": ""
}
}
}
],
"setting": {
"speed": {
"channel": 3,
"bytes": 0,
"record": 0
},
"errorLimit": {
"record": 0,
"percentage": 0.02
}
}
}
}
关键参数详解与实操心得
splitPk: “order_id”
这是并发切分的核心。DataX会根据此字段的最小值、最大值,结合channel数,自动计算切分点。比如order_id范围是1~1000万,channel=3,则生成三条SQL:WHERE order_id BETWEEN 1 AND 3333333、BETWEEN 3333334 AND 6666666、BETWEEN 6666667 AND 10000000。必须选主键或唯一索引字段,否则会出现数据重复或遗漏。我们曾误用create_time,结果因时间重复导致切分错乱,重跑耗时8小时。
fetchSize: 10000
MySQL JDBC驱动的fetchSize参数,控制每次网络往返拉取的行数。设为0表示流式读取(易OOM),设为10000是平衡内存与网络开销的黄金值。实测在16GB内存机器上,fetchSize=50000会导致JVM堆内存飙升至90%,而10000稳定在45%左右。
fieldDelimiter: “\u0001”
ASCII码1的字符(SOH),作为字段分隔符。它比逗号、制表符更安全,因为业务数据里几乎不会出现。Hive建外部表时,必须用ROW FORMAT DELIMITED FIELDS TERMINATED BY '\001'匹配,否则SELECT *会把整行当做一个字段。
compress: “GZIP”
HDFS写入时启用GZIP压缩,实测对文本数据压缩率约75%,节省3倍存储空间。但注意:GZIP不支持文件追加(append),所以writeMode只能是append(新建文件)或nonconflict(冲突跳过)。若需支持追加,改用SNAPPY压缩,压缩率略低(约50%),但支持随机读写。
path: “/data/hive/ods/mysql_orders/dt=${bdp.system.bizdate}”
这里用了DataX内置变量${bdp.system.bizdate},它由调度系统(如Azkaban)注入,格式为YYYYMMDD。切勿手写死日期!我们曾有个任务把路径写成/dt=20240101,结果每天跑的都是同一份数据,三天后才发现ODS层全是重复记录。
实操心得:首次运行前,务必用
hadoop fs -mkdir -p /data/hive/ods/mysql_orders/dt=20240101创建父目录,并确认etl_user对目标路径有rwx权限。DataX不会自动创建不存在的目录,报错信息却是模糊的Failed to connect to HDFS。
3.2 export_hive_to_mysql.json:Hive数据回写MySQL的精准配置
这个配置的挑战在于:Hive数据已在HDFS上,如何高效、准确地写入MySQL?尤其当目标表有主键约束时,如何避免Duplicate entry错误?我们以Hive ODS层的ods_orders外部表为例,其HDFS路径为/data/hive/ods/mysql_orders/dt=20240101,文件格式为GZIP压缩的文本,字段分隔符\u0001。
{
"job": {
"content": [
{
"reader": {
"name": "hdfsreader",
"parameter": {
"path": "/data/hive/ods/mysql_orders/dt=${bdp.system.bizdate}",
"defaultFS": "hdfs://mycluster",
"fileType": "text",
"column": [
{"name": "order_id", "type": "long"},
{"name": "user_id", "type": "long"},
{"name": "amount", "type": "double"},
{"name": "create_time", "type": "string"},
{"name": "update_time", "type": "string"}
],
"fieldDelimiter": "\u0001",
"compress": "GZIP",
"nullFormat": "\\N",
"haveKerberos": false,
"fileFilterRegex": "orders.*\\.gz"
}
},
"writer": {
"name": "mysqlwriter",
"parameter": {
"username": "etl_user",
"password": "etl_pass",
"connection": [
{
"jdbcUrl": "jdbc:mysql://mysql-prod:3306/report?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true",
"table": ["ods_orders"]
}
],
"column": ["order_id", "user_id", "amount", "create_time", "update_time"],
"preSql": ["DELETE FROM ods_orders WHERE dt = '${bdp.system.bizdate}'"],
"postSql": [],
"writeMode": "insert",
"batchSize": 1024,
"batchByteSize": 1048576
}
}
}
],
"setting": {
"speed": {
"channel": 2,
"bytes": 0,
"record": 0
},
"errorLimit": {
"record": 0,
"percentage": 0.02
}
}
}
}
关键参数详解与实操心得
fileFilterRegex: “orders.*\.gz”
HDFS路径下可能有_SUCCESS、_logs等系统文件,hdfsreader默认会读取所有文件,导致解析失败。正则表达式精准匹配orders开头、.gz结尾的文件,过滤掉干扰项。注意反斜杠要双写\\.gz,否则会被当作转义字符。
preSql: [“DELETE FROM ods_orders WHERE dt = ‘${bdp.system.bizdate}’“]
这是保障幂等性的关键。MySQL目标表ods_orders设计为分区表(按dt字段),每次回写前先清空当日分区,再插入新数据。绝不能用TRUNCATE TABLE,因为它会锁表且无法按分区清理。DELETE配合WHERE条件,只影响目标分区,对其他日期数据零影响。
writeMode: “insert”
DataX mysqlwriter支持insert、replace、update三种模式。replace会先删后插,但要求表有唯一索引;update需指定updateKey,复杂度高。对于T+1全量同步,insert最稳妥——配合preSql清空,逻辑清晰无歧义。
batchSize: 1024 和 batchByteSize: 1048576
前者控制每批插入的行数,后者控制每批数据的字节数上限(1MB)。两者取较小值生效。实测batchSize=1024在千兆网络下,单channel吞吐达8000行/秒;若设为2048,因单批数据过大,MySQL网络缓冲区溢出,反而降速到5000行/秒。
注意:
preSql和postSql中的SQL,必须用数组形式,即使只有一条也要写成["xxx"]。我们曾漏掉方括号,DataX静默忽略,导致数据重复堆积。
3.3 run_etl.py:自动化触发脚本的工程化封装
光有JSON配置还不够,生产环境需要一键触发、日志归集、失败告警。run_etl.py就是这个粘合剂。它不是简单地os.system("python datax.py job.json"),而是做了四层封装:
- 环境校验:检查
JAVA_HOME是否设置、datax.py路径是否存在、HDFS和MySQL服务是否可达; - 参数注入:从命令行或环境变量读取
bizdate,替换JSON中的${bdp.system.bizdate}占位符; - 日志管理:将DataX标准输出重定向到
/opt/module/datax/log/import_mysql_to_hdfs_20240101.log,便于ELK采集; - 结果判定:解析DataX输出的
total \d+ records和total \d+ bytes,若记录数为0且非预期空表,则标记为“可疑失败”,触发钉钉告警。
核心代码片段(简化版):
import json
import subprocess
import sys
import os
from datetime import datetime
def inject_bizdate(json_path, bizdate):
"""将JSON中的${bdp.system.bizdate}替换为实际日期"""
with open(json_path, 'r', encoding='utf-8') as f:
content = f.read()
content = content.replace('${bdp.system.bizdate}', bizdate)
# 写入临时文件,避免污染原配置
temp_path = f"{json_path}.tmp.{datetime.now().strftime('%Y%m%d_%H%M%S')}"
with open(temp_path, 'w', encoding='utf-8') as f:
f.write(content)
return temp_path
def run_datax_job(job_json, bizdate):
datax_bin = "/opt/module/datax/bin/datax.py"
log_dir = "/opt/module/datax/log"
os.makedirs(log_dir, exist_ok=True)
log_file = f"{log_dir}/{os.path.basename(job_json).replace('.json', '')}_{bizdate}.log"
# 构建命令
cmd = [sys.executable, datax_bin, job_json]
with open(log_file, 'w') as f:
result = subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, timeout=7200)
# 解析日志,判断是否成功
with open(log_file, 'r') as f:
log_content = f.read()
# 匹配DataX成功标志
if "job completed" in log_content and "0 record" not in log_content:
print(f"✅ {job_json} 执行成功,日志: {log_file}")
return True
else:
print(f"❌ {job_json} 执行失败,日志: {log_file}")
# 这里可接入钉钉/企业微信Webhook
return False
if __name__ == "__main__":
if len(sys.argv) != 3:
print("用法: python run_etl.py <import|export> <20240101>")
sys.exit(1)
mode, bizdate = sys.argv[1], sys.argv[2]
if mode == "import":
job_json = "/opt/module/datax/job/import_mysql_to_hdfs.json"
elif mode == "export":
job_json = "/opt/module/datax/job/export_hive_to_mysql.json"
else:
print("模式仅支持 import 或 export")
sys.exit(1)
temp_json = inject_bizdate(job_json, bizdate)
success = run_datax_job(temp_json, bizdate)
os.remove(temp_json) # 清理临时文件
sys.exit(0 if success else 1)
实操心得:
run_etl.py必须用sys.executable调用Python,而不是硬编码/usr/bin/python,否则在Conda虚拟环境中会失效。我们曾因此在测试环境跑通、生产环境报ModuleNotFoundError,排查了两天。
4. 实操全流程与核心环节实现
4.1 环境准备与路径适配(以CentOS 7为例)
DataX本身是Java程序,但依赖Hadoop和MySQL客户端库。我们的标准部署路径是/opt/module/datax/,以下是完整的初始化步骤:
步骤1:安装Java 8并配置环境变量
# 下载jdk-8u202-linux-x64.tar.gz,解压到/opt/module/jdk1.8.0_202
echo 'export JAVA_HOME=/opt/module/jdk1.8.0_202' >> /etc/profile
echo 'export PATH=$JAVA_HOME/bin:$PATH' >> /etc/profile
source /etc/profile
java -version # 应输出 java version "1.8.0_202"
步骤2:部署DataX 3.0.3
# 下载datax.tar.gz,解压到/opt/module/
tar -zxvf datax.tar.gz -C /opt/module/
chown -R etl:etl /opt/module/datax
# 验证
/opt/module/datax/bin/datax.py --version # 应输出 DataX (3.0.3)
步骤3:配置Hadoop客户端(关键!)
DataX的hdfsreader/hdfswriter需要Hadoop配置文件。将集群的core-site.xml、hdfs-site.xml、yarn-site.xml复制到/opt/module/datax/conf/目录下:
scp hadoop@namenode:/etc/hadoop/conf/core-site.xml /opt/module/datax/conf/
scp hadoop@namenode:/etc/hadoop/conf/hdfs-site.xml /opt/module/datax/conf/
# 修改datax/conf/core-site.xml,确保fs.defaultFS指向你的集群
# <property><name>fs.defaultFS</name><value>hdfs://mycluster</value></property>
步骤4:放置MySQL JDBC驱动
下载mysql-connector-java-8.0.28.jar,放入/opt/module/datax/lib/目录。必须用8.x版本,因为DataX 3.0+默认使用JDBC 4.2规范,5.x驱动不兼容。
步骤5:创建HDFS目标目录并授权
# 以hdfs用户执行
sudo -u hdfs hadoop fs -mkdir -p /data/hive/ods/mysql_orders
sudo -u hdfs hadoop fs -chown -R etl:etl /data/hive/ods/mysql_orders
sudo -u hdfs hadoop fs -chmod -R 775 /data/hive/ods/mysql_orders
注意:
etl用户必须在HDFS上有rwx权限,且在Linux系统中存在(id etl可查)。我们曾因忘记创建Linux用户,导致DataX报Permission denied: user=etl,而HDFS日志里却显示user=null,误导排查方向。
4.2 首次全量同步:从MySQL到Hive的七步走
假设今天是2024年1月1日,我们要把MySQL的orders表全量导出,作为Hive ODS层的第一份快照。
第1步:确认MySQL表数据量
SELECT COUNT(*) FROM orders; -- 返回 12,583,421 行
SELECT MIN(order_id), MAX(order_id) FROM orders; -- 返回 1, 12583421
记录MAX(order_id),用于后续splitPk切分验证。
第2步:修改import配置中的splitPk和channel
根据上一步的MAX(order_id)=12583421,设channel=3,则理论切分点为4194474和8388948。在import_mysql_to_hdfs.json中确认:
"splitPk": "order_id",
"speed": {"channel": 3}
第3步:执行导入命令
# 切换到etl用户
sudo su - etl
# 触发任务,注入bizdate=20240101
python /opt/module/datax/bin/datax.py \
/opt/module/datax/job/import_mysql_to_hdfs.json \
-p "-Dbdp.system.bizdate=20240101"
提示:
-p参数用于传递系统变量,-D前缀是JVM规范,DataX会自动解析。
第4步:监控执行过程
观察日志末尾的实时统计:
2024-01-01 02:15:23.123 [job-0] INFO JobContainer - Total 12583421 records, 258342100 bytes
2024-01-01 02:15:23.124 [job-0] INFO JobContainer - Task start time: 2024-01-01 02:00:15
2024-01-01 02:15:23.125 [job-0] INFO JobContainer - Task end time: 2024-01-01 02:15:23
耗时15分08秒,符合预期(1258万行 ÷ 15分钟 ≈ 14000行/秒)。
第5步:验证HDFS文件
hadoop fs -ls /data/hive/ods/mysql_orders/dt=20240101/
# 应看到类似:
# -rw-r----- 3 etl etl 258342100 2024-01-01 02:15 /data/hive/ods/mysql_orders/dt=20240101/orders.000000.gz
hadoop fs -cat /data/hive/ods/mysql_orders/dt=20240101/orders.000000.gz | head -n 3 | gzip -d
# 输出应为:
# 1^A1001^A299.99^A2023-12-01 10:23:45^A2023-12-01 10:23:45
# 2^A1002^A199.50^A2023-12-01 11:05:22^A2023-12-01 11:05:22
# 3^A1003^A399.00^A2023-12-01 12:30:18^A2023-12-01 12:30:18
^A是\u0001的终端显示,确认分隔符正确。
第6步:在Hive中创建外部表
CREATE EXTERNAL TABLE ods_orders (
order_id BIGINT,
user_id BIGINT,
amount DOUBLE,
create_time STRING,
update_time STRING
)
PARTITIONED BY (dt STRING)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\001'
NULL DEFINED AS '\\N'
STORED AS TEXTFILE
LOCATION '/data/hive/ods/mysql_orders/';
-- 添加分区
ALTER TABLE ods_orders ADD PARTITION (dt='20240101');
第7步:验证Hive查询结果
SELECT COUNT(*) FROM ods_orders WHERE dt='20240101'; -- 应返回 12583421
SELECT * FROM ods_orders WHERE dt='20240101' LIMIT 3;
-- 检查NULL值:若user_id为NULL,应显示为NULL而非\N
4.3 增量回写:从Hive到MySQL的闭环验证
完成全量后,第二天(2024-01-02)我们需要把Hive中新增的订单宽表dwd_order_daily回写到MySQL的报表库。
第1步:确认Hive源数据
-- 查看Hive表结构和数据
DESCRIBE dwd_order_daily;
SELECT COUNT(*) FROM dwd_order_daily WHERE dt='20240101';
-- 假设返回 15234 行,且包含NULL字段
第2步:调整export配置
修改export_hive_to_mysql.json:
- path: “/data/hive/dwd/dwd_order_daily/dt=${bdp.system.bizdate}”
- column: 匹配Hive表字段(注意类型,如amount在Hive是decimal(10,2),JSON中写"type": "double")
- preSql: [“DELETE FROM report_dwd_order_daily WHERE dt = ‘${bdp.system.bizdate}’“]
第3步:执行回写
python /opt/module/datax/bin/datax.py \
/opt/module/datax/job/export_hive_to_mysql.json \
-p "-Dbdp.system.bizdate=20240101"
第4步:验证MySQL结果
SELECT COUNT(*) FROM report_dwd_order_daily WHERE dt='20240101'; -- 应为 15234
SELECT * FROM report_dwd_order_daily WHERE dt='20240101' AND user_id IS NULL LIMIT 3;
-- 确认NULL值正确写入,而非字符串'\N'
实操心得:首次回写前,务必在MySQL中手动执行一遍
preSql,确认语法无误。我们曾因report_dwd_order_daily表名拼错,preSql执行失败,但DataX默认忽略SQL错误,导致数据重复插入。
5. 常见问题与排查技巧实录
5.1 典型异常现象与根因分析
我们在47个调度周期中,共捕获12类高频异常。以下是TOP5及其解决方案,按发生频率排序:
| 序号 | 异常现象 | 根因分析 | 解决方案 | 复现概率 |
|---|---|---|---|---|
| 1 | java.lang.NullPointerException at com.alibaba.datax.plugin.writer.mysqlwriter.MysqlWriter$Job.preparePreSql(MysqlWriter.java:123) | preSql数组为空或格式错误(如少写方括号) | 检查preSql是否为["xxx"]数组,用jsonlint.com验证JSON语法 | 38% |
| 2 | ERROR com.alibaba.datax.core.transport.exchanger.BufferMemoryManager - Memory is not enough | channel数过高,单机内存不足 | 降低speed.channel值,或增加JVM堆内存(-Xms4g -Xmx4g) | 25% |
| 3 | Caused by: java.sql.SQLException: The server timezone value 'XXX' is unrecognized | MySQL JDBC URL未指定serverTimezone | 在jdbcUrl中强制添加&serverTimezone=Asia/Shanghai | 18% |
| 4 | ERROR com.alibaba.datax.plugin.reader.hdfsreader.HdfsReader$Job.init(HdfsReader.java:102) - Cannot find file | hdfsreader.path路径不存在,或etl用户无读权限 | 用hadoop fs -ls确认路径,hadoop fs -getfacl检查ACL权限 | 12% |
| 5 | ERROR com.alibaba.datax.plugin.writer.hdfswriter.HdfsWriter$Task.startWrite(HdfsWriter.java:215) - File already exists | hdfswriter.writeMode为append,但目标文件已存在 | 改为writeMode: "nonconflict",或在preSql中用hadoop fs -rm清理 | 7% |
5.2 Hive源表为空时的任务失败规避方案
摘要里提到“规避Hive源表为空导致任务失败”,这并非DataX Bug,而是其设计理念:当hdfsreader扫描路径下无匹配文件时,会抛出NullPointerException。我们的解决方案是前置校验脚本,集成在run_etl.py中:
def check_hdfs_path_exists(hdfs_path, bizdate):
"""检查HDFS路径下是否有有效数据文件"""
# 替换变量
hdfs_path = hdfs_path.replace('${bdp.system.bizdate}', bizdate)
# 执行hadoop命令
result = subprocess.run(
['hadoop', 'fs', '-ls', hdfs_path],
capture_output=True, text=True
)
if result.returncode != 0:
print(f"⚠️ HDFS路径不存在: {hdfs_path}")
return False
# 解析输出,过滤_system、_SUCCESS等文件
files = [line.split()[-1] for line in result.stdout.strip().split('\n') if line.strip()]
data_files = [f for f in files if re.search(r'\.(txt|csv|gz|snappy)$', f)]
if not data_files:
print(f"⚠️ HDFS路径下无数据文件: {hdfs_path}")
# 写入空文件,避免DataX崩溃
empty_file = f"{hdfs_path}/empty_{bizdate}.txt"
subprocess.run(['hadoop', 'fs', '-touchz', empty_file])
return True # 让DataX读取空文件,自然结束
return True
# 在run_etl.py主流程中调用
if mode == "export":
hdfs_path = "/data/hive/dwd/dwd_order_daily/dt=${bdp.system.bizdate}"
if not check_hdfs_path_exists(hdfs_path, bizdate):
sys.exit(1)
这样,当Hive分区为空时,脚本会自动创建一个空文件,DataX读取后输出0 records,任务正常结束,不会触发告警。
5.3 字段类型不匹配导致的数据截断问题
MySQL的VARCHAR(255)字段,在Hive中可能定义为STRING,但DataX JSON配置里column.type写成了"string"。这看似无害,实则埋雷:当MySQL中某行数据长度为256字节时,mysqlreader会静默截断为255字节,且不报错!
我们的应对策略是三层校验:
-
配置层:在JSON的
column中,严格按目标端类型声明。Hive写HDFS时,type必须与Hive表定义一致(如STRING→"string",BIGINT→"long");MySQL写入时,type必须与MySQL字段匹配(如VARCHAR→"string",DECIMAL(10,2)→"double")。 -
执行层:在
run_etl.py中,任务结束后执行数据量比对:
python # 从MySQL查源表count mysql_count = get_mysql_count("SELECT COUNT(*) FROM orders WHERE create_time >= '2024-01-01'") # 从Hive查目标表count hive_count = get_hive_count("SELECT COUNT(*) FROM ods_orders WHERE dt='20240101'") if abs(mysql_count - hive_count) > 10: # 允许10行误差(脏数据) send_alert(f"数据量偏差: MySQL{mysql_count} vs Hive{hive_count}") -
存储层:HDFS文件启用
crc校验。在hdfswriter中添加:
json "compress": "GZIP", "enableCRC": true
这样每个文件会生成.crc校验文件,可用于事后完整性验证。
5.4 并发控制与性能调优实战经验
DataX的speed.channel不是越大越好。我们通过压测得出以下黄金法则:
-
MySQL Reader并发:取决于MySQL的
max_connections和innodb_buffer_pool_size。公式为:channel ≤ (max_connections × 0.7) ÷ 2。例如max_connections=500,则channel最大设为175,但实际建议≤50,避免拖慢线上业务。 -
HDFS Writer并发:取决于NameNode压力。
channel=3时,单机写入HDFS吞吐约120MB/s;channel=6时,NameNode CPU飙升至95%,写入速度不升反降。最佳实践是channel=3,配合batchSize=1024。 -
内存分配:DataX默认JVM堆内存为1GB,对于10GB以上数据,必须调大:
bash # 修改bin/datax.py,找到java命令行 # 在 -jar 前添加:-Xms4g -Xmx4g
我们曾用channel=10跑一个20GB订单表,结果NameNode日志爆满,hadoop fs -ls命令响应超时,整个集群卡顿。降为channel=3后,耗时仅增加23%,但系统负载平稳。
5.5 安全加固与敏感信息管理
JSON配置中明文存储数据库密码,是重大安全隐患。我们的生产环境采用密钥中心+环境变量注入方案:
- 将密码加密存储在Vault中,key为
/secret/datax/mysql_prod; - 调度任务启动时,从Vault拉取密钥,写入临时文件;
run_etl.py读取临时文件,动态注入JSON配置。
简化版实现:
# 从Vault获取密码(需提前配置VAULT_TOKEN)
vault_token = os.getenv('VAULT_TOKEN')
vault_url = "http://vault-prod:8200/v1/secret/datax/mysql_prod"
headers = {"X-Vault-Token": vault_token}
resp = requests.get(vault_url, headers=headers)
password = resp.json()['data']['password']
# 注入到JSON
with open(job_json, 'r') as f:
config = json.load(f)
config['job']['content'][0]['reader']['parameter']['password'] = password
config['job']['content'][0]['writer']['parameter']['password'] = password
# 写入临时配置...
注意:临时配置文件权限必须设为
600,且任务结束后立即os.remove(),杜绝密码泄露。
6. 进阶扩展与生产级增强建议
6.1 支持动态字段映射的配置模板
当前模板是静态字段列表,但业务表经常增减字段。我们开发了一个schema_sync.py脚本,自动从MySQL和Hive元数据生成DataX配置:
# 从MySQL获取表结构
mysql_cols = get_mysql_columns("orders")
# 从Hive获取表结构
hive_cols = get_hive_columns("ods_orders")
# 生成column数组,自动对齐
column_config = []
for col in mysql_cols:
hive_col = next((c for c in hive_cols if c['name'] == col['name']), None)
if hive_col:
column_config.append({
"name": col['name'],
"type": hive_col['type'] # 用Hive类型为准
})
else:
# 字段在Hive中不存在,跳过或设为null
pass
这样,当MySQL增加pay_status字段,只需运行schema_sync.py,即可生成新配置,无需人工维护。
6.2 基于DataX的增量同步框架
全量同步无法满足T+0需求。我们基于update_time字段,构建了轻量级增量框架:
- 在
import_mysql_to_hdfs.json中,where条件改为:
json "where": "update_time >= '${last_success_time}' AND update_time < '${current_time}'" - 调度系统维护
last_success_time变量(如2024-01-01 00:00:00),每次任务成功后更新为current_time。
此方案无需Binlog,对MySQL零侵入,已稳定运行11个月,日均增量数据200万行。
6.3 监控告警体系集成
我们将DataX日志接入ELK,并配置以下核心告警规则:
- 耗时异常:任务执行时间 > 历史平均值×2,且
record数 > 0; - 空数据告警:
record数 = 0,且非预期空表(如orders表不应为空); - 错误率超标:
errorLimit.percentage> 0.02,且错误记录数 > 100。
告警消息包含:任务名称、耗时、记录数、错误数、日志URL,运维人员点击URL直达Kibana上下文。
我个人在实际操作中的体会是:DataX的价值不在“多快”,而在“多稳”。它像一台老式柴油发电机——启动慢、噪音大,但一旦运转起来,连续工作三个月不用停机。在离线数仓这个讲究“确定性”的战场上,有时候,慢即是快,稳即是赢。
简介:包含两个即用型DataX任务配置文件:import_mysql_to_hdfs.实现MySQL表全量导出为文本格式存入HDFS,export_hive_to_mysql.支持将Hive分区表或外部表数据写回MySQL。已预处理Null值兼容问题——Hive中以\N标识的空值,在导入MySQL时自动转为标准NULL,核心通过”nullFormat”: “\N”参数生效。配置规避了Hive源表为空导致任务失败的常见异常,建议执行前校验源数据存在性。所有配置基于DataX 3.0+版本实测通过,路径需按实际环境调整(如/opt/module/datax/),运行命令统一为python /opt/module/datax/bin/datax.py /opt/module/datax/job/xxx.。支持字段映射、列筛选、并发数设置等基础ETL能力,不依赖HiveServer2,直连HDFS文件系统或MySQL JDBC。配套提供run_etl.py脚本用于快速触发任务,以及requirements.txt说明依赖环境。适用于离线数仓建设中MySQL与Hive/HDFS之间的周期性数据同步、历史数据迁移、ODS层接入等典型场景。

203

被折叠的 条评论
为什么被折叠?



