https://blog.csdn.net/dishugj/article/details/162068579

传统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 + 校验

一句话总结

五个节点,四次形态转换:

  1. DataX 把关系行转成 HBase KeyValue(channel 限速保护源库)
  2. Export/Snapshot 导出(产物是 SequenceFile/HFile,不是 Parquet),需再转成 Parquet
  3. Hive 分层清洗加工,跑在 YARN 上(必须分区 + Parquet + Spark 引擎)
  4. 转 TEXTFILE ------ 全链路最关键的坑,Sqoop 读不了 Parquet 二进制
  5. Sqoop export 解析文本并并发写 Oracle(--batch 必加、-m 别太激进、先写临时表再 MERGE)

每一步都要做行数校验,并用 Oozie 串成定时 DAG(DataX 和 HBase 导出需用 shell action,Hive 和 Sqoop 有原生 action)。

相关推荐
时间售卖铺1 小时前
MySQL 安装部署
运维·数据库·mysql·云原生
泡海椒1 小时前
JQuick-SQL 子查询:WHERE、SELECT、HAVING 与派生表实践
数据库·sql
用户0510122572961 小时前
SQLite 学习博客(一):从零建库,理解 SQLite 的角色与第一张表
数据库·sqlite
福娃筱欢1 小时前
金仓数据库配置SSL双向认证
数据库·网络协议·ssl
captain3761 小时前
MyBatis操作数据库(入门1)
数据库·oracle·mybatis
snpgroupcn1 小时前
SAP 系统退役的归档架构怎么设计:可检索、可理解、可追溯、可证明
java·数据库·架构
清水白石0081 小时前
React Native与Flutter长期维护成本对比
开发语言·数据库
弦儿1 小时前
MySQL 数据库从入门到实践:SQL 四大分类 + DDL/DML/DQL 全梳理(附全套代码)
数据库