传统CDH集群+Oracle rac 数据仓库实现架构解析
周边系统 → DataX → HBase → HDFS → Hive → Sqoop → Oracle RAC 全链路详解
整体流程图
周边业务系统(MySQL / 接口 / 文件)
↓ ① DataX(channel 限速,保护源库)
↓
HBase(落地明细,支持 upsert / 点查)
↓ ② 导出(HBase Export / Snapshot / Hive 映射)
↓
HDFS(分析副本,Parquet)
↓ ③ Hive SQL 清洗加工(跑在 YARN,Spark/MR 执行)
↓
Hive 结果表(Parquet)
↓ ④ ★ 转成 TEXTFILE(关键!Sqoop 读不了 Parquet)
↓
HDFS 文本文件 /tmp/export/result
↓ ⑤ Sqoop export -m 8
↓
Oracle RAC 数据仓库
全局数据形态变化
关系型记录(MySQL 行 / 文件行)
↓ ① DataX
KeyValue(HBase:RowKey + 列族:列 → 值)
↓ ② 导出 + 转换
Parquet 列式文件(HDFS)
↓ ③ Hive SQL 清洗加工
Parquet 结果表(清洗聚合后)
↓ ④ 转文本 ★ 形态必须变,Sqoop 不认二进制
行式文本文件(HDFS)
↓ ⑤ Sqoop export
Oracle 关系表
节点 ①:周边业务系统 → DataX → HBase
目的
把 MySQL / 接口 / 文件等异构数据抽出来落到 HBase,同时限速保护源库。
DataX 作业模型
Job(一个作业)
↓ 按 channel 数切分
Task × N(每个 Task 一个独立线程)
单条链路:
Reader ──> Channel(缓冲/限流) ──> Writer
mysqlreader 内存队列 hbasewriter
配置文件 periphery_to_hbase.json
json
{
"job": {
"setting": {
"speed": {
"channel": 4,
"byte": 10485760,
"record": 10000
}
},
"content": [{
"reader": {
"name": "mysqlreader",
"parameter": {
"username": "xxx",
"password": "yyy",
"column": ["id", "name", "amt", "update_time"],
"splitPk": "id",
"connection": [{
"table": ["orders"],
"jdbcUrl": ["jdbc:mysql://host:3306/db"]
}]
}
},
"writer": {
"name": "hbase11xwriter",
"parameter": {
"hbaseConfig": {"hbase.zookeeper.quorum": "zk1,zk2,zk3"},
"table": "ods:periphery",
"mode": "normal",
"rowkeyColumn": [
{"index": 0, "type": "string"}
],
"column": [
{"index": 1, "name": "cf:name", "type": "string"},
{"index": 2, "name": "cf:amt", "type": "double"},
{"index": 3, "name": "cf:upd_t", "type": "string"}
],
"versionColumn": {"index": -1, "value": ""}
}
}
}]
}
}
执行
bash
python /opt/datax/bin/datax.py /opt/datax/job/periphery_to_hbase.json
数据形态变化
MySQL 一行:
id=1001, name=张三, amt=99.5, update_time=2026-10-22
↓
HBase 一个 RowKey:
RowKey = "1001"
cf:name = 张三
cf:amt = 99.5
cf:upd_t = 2026-10-22
RowKey 设计(成败关键)
错误做法:
RowKey = 自增ID → 单调递增 → 永远写最后一个 Region = 热点
RowKey = 时间戳 → 同上
正确做法:
方案A(分散): RowKey = md5(id).substring(0,4) + "_" + id
方案B(分散+时间):RowKey = 反转时间戳 + "_" + id
建表时预分区,避免自动分裂抖动
bash
hbase shell
create 'ods:periphery', 'cf', {SPLITS => ['1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']}
验证
bash
hbase shell
count 'ods:periphery'
scan 'ods:periphery', {LIMIT => 5}
注意事项
| 项 | 说明 |
|---|---|
| RowKey 设计 | 必须分散,否则写热点 |
| DataX 是单机多线程 | 受单机 CPU/内存/网卡限制,超大表交给 Sqoop |
| 必须用 splitPk | 不指定则只能单 channel 跑,失去并发 |
节点 ②:HBase → 导出 → HDFS
目的
把 HBase 明细导出成 HDFS 上的列式文件,供 Hive 高效分析。
关键认知:导出产物不是 Parquet
| 方式 | 产物格式 | 说明 |
|---|---|---|
| hbase mapreduce.Export | SequenceFile | 二进制,Hive 需建表映射 |
| Snapshot + ExportSnapshot | HFile | 物理文件,最轻量,推荐 |
| Hive 映射表 INSERT OVERWRITE | Parquet | 简单但慢(全 Region 扫) |
方式 A:Export(离线常用)
bash
hbase org.apache.hadoop.hbase.mapreduce.Export \
ods:periphery \
/warehouse/hbase_export/periphery \
1 \
0 \
1761321600000
参数依次:表名、输出路径、版本数、起始时间戳、结束时间戳。
方式 B:Snapshot(推荐,对线上影响最小)
bash
# 1. 打快照(秒级)
hbase shell
snapshot 'ods:periphery', 'snap_periphery_20261022'
# 2. 导出快照到 HDFS
hbase org.apache.hadoop.hbase.snapshot.ExportSnapshot \
-snapshot snap_periphery_20261022 \
-copy-to /warehouse/hbase_export/periphery
# 3. 清理快照
hbase shell
delete_snapshot 'snap_periphery_20261022'
转换成 Parquet(必须的一步)
sql
-- ① 建外部表指向导出目录
CREATE EXTERNAL TABLE ods_periphery_raw (
rowkey STRING,
name STRING,
amt DOUBLE,
upd_t STRING
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.hbase.HBaseSerDe'
STORED AS SEQUENCEFILE
LOCATION '/warehouse/hbase_export/periphery';
-- ② 转成 Parquet 分区表
INSERT OVERWRITE TABLE ods_periphery PARTITION (dt='2026-10-22')
SELECT rowkey, name, amt, upd_t FROM ods_periphery_raw;
数据形态变化
HBase HFile(列式 KeyValue,带版本)
→ SequenceFile / HFile(二进制)
→ Hive 外部表读取
→ Parquet 分区表(列式压缩)
注意事项
| 项 | 说明 |
|---|---|
| 别用 Hive 直接全表扫 HBase | 会全 Region 扫描,极慢 |
| 导出要带时间范围 | 增量导出,别每次全量 |
| Parquet 转换必须做 | 分区裁剪、列裁剪、压缩全靠它 |
节点 ③:Hive SQL 清洗加工(YARN + Spark/MR)
Hive 编译执行流程
① Parser(语法解析)
SELECT ... FROM ... WHERE ...
↓ AST 抽象语法树
② Semantic Analyzer(语义分析)
查 Metastore:表是否存在、字段是否正确、类型是否匹配
↓ QB(Query Block)
③ Logical Plan(逻辑计划)
TableScan → Filter → Join → GroupBy → Select
④ Optimizer(优化器)
· 分区裁剪:WHERE dt='2026-10-22' → 只读该分区目录
· 列裁剪:只 SELECT 用到的列
· 谓词下推:WHERE 条件下推到 Scan 阶段
· MapJoin:小表广播到内存,避免 shuffle
· 倾斜处理:倾斜 key 单独处理
↓
⑤ Physical Plan(物理计划)
MR DAG / Tez DAG / Spark DAG
⑥ 提交 YARN
AM 申请 Container → NM 启动 → 读 HDFS → 计算 → shuffle → 写结果
分层加工 SQL
sql
-- 设置执行引擎
SET hive.execution.engine=spark;
SET spark.executor.memory=4g;
SET hive.auto.convert.join=true;
-- 【ODS → DWD】清洗:去重、脱敏、过滤脏数据
INSERT OVERWRITE TABLE dwd_orders PARTITION (dt='2026-10-22')
SELECT
rowkey,
TRIM(name) AS name,
CASE WHEN amt < 0 THEN 0 ELSE amt END AS amt,
upd_t
FROM ods_periphery
WHERE dt='2026-10-22'
AND rowkey IS NOT NULL;
-- 【DWD → DWS】聚合:按主题汇总
INSERT OVERWRITE TABLE dws_orders_daily PARTITION (dt='2026-10-22')
SELECT
name,
COUNT(*) AS order_cnt,
SUM(amt) AS total_amt
FROM dwd_orders
WHERE dt='2026-10-22'
GROUP BY name;
-- 【DWS → ADS】应用指标
INSERT OVERWRITE TABLE ads_result PARTITION (dt='2026-10-22')
SELECT name, total_amt
FROM dws_orders_daily
WHERE dt='2026-10-22' AND total_amt > 10000;
数据形态变化
Parquet ODS(明细,可能脏)
→ Parquet DWD(清洗后明细)
→ Parquet DWS(聚合汇总)
→ Parquet ADS(应用结果,最终要导出的表)
注意事项
| 项 | 说明 |
|---|---|
| 必须分区 | 按 dt 分区,否则每次全表扫描 |
| 用 Parquet/ORC | 列式存储,压缩比高,支持列裁剪 |
| 执行引擎选 Spark/Tez | MR 太慢 |
| 小表用 MapJoin | SET hive.auto.convert.join=true |
节点 ④:Parquet → TEXTFILE(全链路最关键的坑)
为什么必须做
Parquet 是【二进制列式】文件
↓
Sqoop export 只能解析【行式文本】文件(按分隔符切字段)
↓
直接指向 Parquet 目录 → 乱码 / 报错 / 数据错乱 / 静默错行
做法 A:INSERT OVERWRITE DIRECTORY(推荐)
sql
INSERT OVERWRITE DIRECTORY '/tmp/export/result'
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\001'
LINES TERMINATED BY '\n'
SELECT name, total_amt
FROM ads_result
WHERE dt='2026-10-22';
做法 B:建 TEXTFILE 表(可复用)
sql
CREATE TABLE IF NOT EXISTS ads_result_text (
name STRING,
total_amt DOUBLE
)
PARTITIONED BY (dt STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\001'
STORED AS TEXTFILE;
INSERT OVERWRITE TABLE ads_result_text PARTITION (dt='2026-10-22')
SELECT name, total_amt FROM ads_result WHERE dt='2026-10-22';
对应 HDFS 路径:
/user/hive/warehouse/ads_result_text/dt=2026-10-22/000000_0
分隔符的三个坑
| 坑 | 后果 | 解法 |
|---|---|---|
| 字段值含分隔符 | 切错列,数据错乱 | 用 \001,或提前清洗替换 |
| 字段值含换行符 | 行数错乱 | REGEXP_REPLACE(col,'\n','') |
| NULL 值 | 变成字符串 "NULL" | Sqoop 加 --input-null-string '\N' |
数据形态变化
Parquet(二进制列式,压缩)
→ 行式文本:张三\0019999.5\n李四\0018888.0\n
代价提示:文本比 Parquet 大 3~8 倍,但导出到 Oracle 只能这样。
节点 ⑤:Sqoop export → Oracle RAC
Sqoop export 内部原理
sqoop export --export-dir /tmp/export/result --table TARGET -m 8
↓
① 连 Oracle,读目标表元数据(列数、类型、主键)
② 生成 ORM 类 + 只有 Map 的 MapReduce(无 Reduce)
③ 按 -m 8 把输入文件切成 8 个 split
④ 8 个 map 并行,每个 map 一个 JDBC 连接
⑤ 每个 map 逐行解析文本 → 拼 INSERT → 提交
map 1 map 2 ... map 8
| | |
└───────┼─────────────┘
↓
Oracle RAC(通过 SCAN IP 分发到各实例)
完整命令
bash
sqoop export \
--connect "jdbc:oracle:thin:@//scan-ip:1521/dw_service" \
--username mes \
--password xxx \
--table TARGET_RESULT \
--export-dir /tmp/export/result \
--fields-terminated-by '\001' \
--lines-terminated-by '\n' \
--input-null-string '\\N' \
--input-null-non-string '\\N' \
--columns "name,total_amt" \
--batch \
-m 8
五个必须注意
| 项 | 说明 |
|---|---|
| Oracle 不支持 --direct | 只能走 JDBC |
| -m 8 = 8 个并发 JDBC 连接 | 会打 Oracle:锁争用、undo/redo、索引维护 |
| 必须加 --batch | 默认逐行提交,--batch 批量提交提速数倍 |
| 大批量先禁用索引 | 导完再重建 |
| 先写临时表再 MERGE | 保证一致性,失败不留半截数据 |
Oracle 侧配合操作
sql
-- ① 导出前禁用索引
ALTER INDEX idx_target_name UNUSABLE;
-- ② Sqoop 写入临时表 TARGET_RESULT_TMP
-- ③ 校验行数
SELECT COUNT(*) FROM TARGET_RESULT_TMP;
-- ④ MERGE 入正式表
MERGE INTO TARGET_RESULT t
USING TARGET_RESULT_TMP s
ON (t.name = s.name)
WHEN MATCHED THEN UPDATE SET t.total_amt = s.total_amt
WHEN NOT MATCHED THEN INSERT (name, total_amt) VALUES (s.name, s.total_amt);
-- ⑤ 重建索引
ALTER INDEX idx_target_name REBUILD;
数据形态变化
HDFS 文本:张三\0019999.5\n
→ Sqoop 解析成字段
→ INSERT INTO TARGET_RESULT(name,total_amt) VALUES('张三',9999.5)
→ Oracle 关系表的一行
横向支撑一:Oozie 工作流
xml
<workflow-app name="periphery_etl" xmlns="uri:oozie:workflow:0.5">
<start to="datax_extract"/>
<!-- ① DataX:无原生 action,用 shell -->
<action name="datax_extract">
<shell xmlns="uri:oozie:shell-action:0.3">
<exec>python</exec>
<argument>/opt/datax/bin/datax.py</argument>
<argument>periphery_to_hbase.json</argument>
</shell>
<ok to="hbase_export"/>
<error to="fail"/>
</action>
<!-- ② HBase 导出:shell -->
<action name="hbase_export">
<shell xmlns="uri:oozie:shell-action:0.3">
<exec>hbase_export.sh</exec>
</shell>
<ok to="hive_etl"/>
<error to="fail"/>
</action>
<!-- ③④ Hive:原生 action -->
<action name="hive_etl">
<hive xmlns="uri:oozie:hive-action:0.5">
<script>etl.sql</script>
</hive>
<ok to="sqoop_export"/>
<error to="fail"/>
</action>
<!-- ⑤ Sqoop export:原生 action -->
<action name="sqoop_export">
<sqoop xmlns="uri:oozie:sqoop-action:0.3">
<command>export --connect jdbc:oracle:thin:@//scan-ip:1521/dw_service --username mes --password xxx --table TARGET_RESULT --export-dir /tmp/export/result --fields-terminated-by '\001' --batch -m 8</command>
</sqoop>
<ok to="end"/>
<error to="fail"/>
</action>
<kill name="fail">
<message>ETL failed</message>
</kill>
<end name="end"/>
</workflow-app>
Coordinator 定时:
xml
<coordinator-app name="periphery_etl_coord"
frequency="${coord:days(1)}"
start="2026-10-22T02:00Z"
end="2030-01-01T00:00Z"
timezone="Asia/Shanghai"
xmlns="uri:oozie:coordinator:0.4">
<action>
<workflow>
<app-path>${nameNode}/user/oozie/workflows/periphery_etl</app-path>
</workflow>
</action>
</coordinator-app>
横向支撑二:每步数据校验
| 环节 | 校验方法 |
|---|---|
| ① DataX 后 | 日志里"读出总数 vs 写入总数"一致 |
| ② 导出后 | hbase count vs HDFS 文件行数 |
| ③ Hive 后 | 各层行数对比,抽样查异常值 |
| ④ 转文本后 | hdfs dfs -cat 人工看几行 |
| ⑤ Sqoop 后 | Oracle COUNT(*) vs HDFS wc -l |
bash
# 最实用的一条:比对两边行数
hdfs dfs -cat /tmp/export/result/* | wc -l
sqlplus mes/xxx@dw
SELECT COUNT(*) FROM TARGET_RESULT_TMP;
全链路坑位汇总
| # | 环节 | 坑 | 解法 |
|---|---|---|---|
| 1 | ① | RowKey 热点 | hash 前缀 / 反转时间戳 + 预分区 |
| 2 | ① | DataX 单机瓶颈 | 大表交 Sqoop,DataX 只跑中小量与异构源 |
| 3 | ② | Export 产物不是 Parquet | 需二次转换 |
| 4 | ② | Hive 直接扫 HBase 慢 | 用 Export / Snapshot |
| 5 | ③ | 无分区导致全表扫 | 必须按 dt 分区 |
| 6 | ④ | Sqoop 读不了 Parquet | 必须转 TEXTFILE |
| 7 | ④ | 分隔符与数据冲突 | 用 \001,提前清洗 |
| 8 | ⑤ | -m 8 打爆 Oracle | --batch + 禁用索引 + 先临时表 |
| 9 | ⑤ | Oracle 无 --direct | 只能 JDBC,超大批量考虑 SQL*Loader |
| 10 | 全 | 中途失败留半截数据 | 临时表 + MERGE + 校验 |
一句话总结
五个节点,四次形态转换:
- DataX 把关系行转成 HBase KeyValue(channel 限速保护源库)
- Export/Snapshot 导出(产物是 SequenceFile/HFile,不是 Parquet),需再转成 Parquet
- Hive 分层清洗加工,跑在 YARN 上(必须分区 + Parquet + Spark 引擎)
- 转 TEXTFILE ------ 全链路最关键的坑,Sqoop 读不了 Parquet 二进制
- Sqoop export 解析文本并并发写 Oracle(--batch 必加、-m 别太激进、先写临时表再 MERGE)
每一步都要做行数校验,并用 Oozie 串成定时 DAG(DataX 和 HBase 导出需用 shell action,Hive 和 Sqoop 有原生 action)。