Ver código fonte

docs(kb): 锁定 §2.8 HDFS HA 改造为 Path B

2026-04-18 新 CDH 环境三连实测证实:DataX JVM 只认 json 的
`hadoopConfig` 块,`HADOOP_CONF_DIR` env 无效。方案从上一轮
"待定/自检决策树"锁定为必做改造。

- 90 §2.8 简化:去掉 `ha_enabled` 开关(用 `[hadoop_config]`
  节存在性代替)、去掉自检决策树、去掉"运维手工改 IP"误记;
  保留实测证据表 + classpath 原因说明
- 92 阶段 2:条件触发的 4 条改为必做 3 条,打勾已完成的
  `HDFSDataSource` 改造 + `HADOOP_CONF_DIR` 清理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tianyu.chu 2 semanas atrás
pai
commit
93e69bdec1
2 arquivos alterados com 247 adições e 16 exclusões
  1. 215 12
      kb/90-重构路线.md
  2. 32 4
      kb/92-重构进度.md

+ 215 - 12
kb/90-重构路线.md

@@ -42,7 +42,7 @@
 | `RELEASE_USER="alvis"` | `bin/common/init.sh` | 改为 `RELEASE_USER="bigdata"` 并移入 `conf/env.sh` |
 | `RELEASE_ROOT_DIR="/home/alvis/release"` | `init.sh`、`__init__.py` | 改为 `/home/bigdata/release` 并移入 `conf/env.sh` |
 | 项目部署目录 `poyee-data-warehouse/` | `publish.sh` | 新项目发布目录为 `/home/bigdata/release/poyee-data-warehouse/` |
-| `DATAX_WORKERS=(m3 d1 d2 d3 d4)` + 权重 | `init.sh` | 移入 `conf/workers.conf` |
+| `DATAX_WORKERS=(m3 d1 d2 d3 d4)` + `DATAX_WORKERS_WEIGHTS` 权重 map | `init.sh:18-31`(含展开 `DATAX_WORKERS_QUEUE` 的循环) | workers 列表 + 权重 map **整体**移入 `conf/workers.conf`(ini 或 yaml 格式),`init.sh` 仅保留读取 + 展开逻辑 |
 | `HADOOP_CONF_DIR='/etc/hadoop/conf'` | `__init__.py` | 使用系统环境变量 |
 | `LOG_ROOT_DIR="/opt/data/log"` | `init.sh`、`__init__.py` | 移入 `conf/env.sh` |
 | 钉钉 access_token | `dingtalk_notifier.py` | 移入 `conf/alerter.conf`(敏感项) |
@@ -184,6 +184,9 @@ venv/
 *.log
 dw_base.zip
 
+# ---- 开发者本地草稿区(datax-gc-generator 输出的参考模板等) ----
+workspace/
+
 # ---- 敏感配置(运行时自动从 datasource/ 注入或在 conf/ 本地覆盖) ----
 conf/alerter.conf
 ```
@@ -195,7 +198,8 @@ conf/alerter.conf
    - 忽略:`workspace.xml`、`tasks.xml`、`shelf/`、`usage.statistics.xml` 等个人/统计文件
 2. **`.claude/` 也不整体 ignore**:`settings.json`、`commands/`、`agents/` 是团队共享配置;只忽略 `settings.local.json`
 3. **`dw_base.zip`** 是 `spark_sql.py` 运行时生成的 PySpark 打包产物,属于构建产物不入库
-4. **`conf/alerter.conf`** 一开始就放进 `.gitignore`:阶段 2 迁移钉钉/企微 Webhook 时,新建文件前 `.gitignore` 必须先就位
+4. **`workspace/`** 是开发者本地草稿区(`datax-gc-generator` 输出的参考模板、临时 SQL 调试等),**永不入仓**;开发者认可的成品再手动复制到 `jobs/` 或 `manual/` 下提交
+5. **`conf/alerter.conf`** 一开始就放进 `.gitignore`:阶段 2 迁移钉钉/企微 Webhook 时,新建文件前 `.gitignore` 必须先就位
 
 **与仓库改名的联动**:
 
@@ -284,6 +288,198 @@ DW_ENV=dev
 
 **参考**:kb/00-项目架构.md §4.3(DataX 脚本详细使用)、§6.4(多环境机制)、kb/21-命名规范.md §3.9(DataX ini 文件命名)。
 
+### 2.6 DataX 入口脚本收口为两条命令(中优先级)
+
+**现状**:DataX 入口分散,学习成本高:
+
+- `bin/datax-single-job-starter.sh` — 单 ini 执行
+- `bin/datax-multiple-job-starter.sh` — 批量 + 按 start-date/stop-date 范围展开
+- `bin/datax-multiple-hive-job-starter.sh` — Hive 表批量(语义与上者重合)
+- `bin/datax-single-hdfs-job-starter.sh`(如有残留) — 单次 HDFS 导出
+- `bin/datax-job-config-generator.py` — ini → json 翻译器(内部工具)
+- `bin/datax-gc-generator.py` — ini 元生成器(详见 §2.7)
+
+**目标态**:顶层收成两个命令,每个命令内部吃 single / batch 两种输入形态;底层的 json 翻译 / worker 选择 / 日志路径由公共模块承担,调用方不感知。
+
+| 顶层命令 | 语义 | 关键参数 |
+|---|---|---|
+| **`bin/datax-import`**(命名待确认) | 导入到 Hive(目标侧带分区管理) | `-ini <file>` 单 ini · `-inis <dir>` 批量 · `-dt <yyyymmdd>` 指定分区 · `-start-date / -stop-date` 日期范围展开 · `-skip-exist` 默认开,已存在分区跳过 · `-force-overwrite` 强制覆盖 · `-skip-partitions <csv>` 手动跳过特定分区 · `-env <dev\|test\|prod>` |
+| **`bin/datax-export`**(命名待确认) | 从 Hive/HDFS 导出到外部系统(源侧带路径探测) | `-ini / -inis` 同上 · `-src-check`(默认 fail-fast)· `-skip-missing` 源路径缺失时跳过不报错 · `-dt / -start-date / -stop-date` · `-env` |
+
+**实现建议**:
+
+1. 把老脚本 worker 选择、日志路径、json 翻译提到 Python 模块 `dw_base/datax/entry.py`,两个 sh 只做参数解析 + 调用
+2. 分区检查:`datax-import` 在执行前 `SHOW PARTITIONS` 目标表 → 命中则按 `-skip-exist` / `-force-overwrite` 决策;`datax-export` 在执行前 `hdfs dfs -test -e <源路径>` → 不存在按 `-src-check` / `-skip-missing` 决策
+3. `-inis` 的批量展开规则:传目录则递归扫 `.ini` 文件,传文件列表(`jobs.list`)则读文件每行一个 ini
+4. 老脚本 `datax-single-job-starter.sh` / `datax-multiple-*-starter.sh` 在两个新命令稳定后整体删除,保留一期转发封装作为兼容
+
+**第三条命令 `datax-gc-generator`(ini 元生成器)独立保留**:用户已确认。职责是"从 PG 扫 schema 生成 ini 参考模板",和"执行 ini"不是一回事,不收口到上面两条里。详见 §2.7。
+
+### 2.7 `datax-gc-generator` 从零重写(中优先级)
+
+**现状**(凭查证 2026-04-18):`bin/datax-gc-generator.py` 支持 `from ∈ {mysql, hdfs}` × `to ∈ {elasticsearch, hbase, hdfs, kafka, mongo, mysql}`,覆盖面大、代码沉重,且:
+
+- **全项目没有任何其他模块 import 或 shell 调用它**(仅 3 处 `SparkSQL('datax-gc-generator')` 是自己设置 app name)
+- 内部走 `MySQLHandler` + `MySQLDataSource` + `convert_mysql_column_types` + `MySQLReader.generate_hive_ddl()` 一条链,与 §2.1 已登记废弃的"自动 DDL 生成"方向相冲突
+- 新项目新业务只需要 `from=pg to=hdfs` 一条路径,其他组合全是老项目遗产
+
+**定位**:**参考模板生成器**,不是"一键出可用 ini"。产物是开发者人工调整的起点 —— 常见修改包括字段剪裁(只同步用到的列)、WHERE 过滤条件、hivePartitions 配置、大表拆分策略等。开发者 diff 参考模板和自己的需求,改完再把成品 ini 提交到 `jobs/raw/{domain}/`。
+
+**方向**:整个文件废弃 + 从零重写(凭记忆:未完全定稿,待真正开始写代码时再细化)
+
+**重写目标**:
+- 仅支持 `from=pg to=hdfs`
+- 读 PG 表结构(`information_schema.columns` + `pg_catalog.pg_description` 取字段注释)
+- 输出到**开发者本地的 `workspace/{yyyymmdd}/{name}.ini`**(`workspace/` 被 `.gitignore` 排除,**不入仓**),作为参考模板(原始全字段 + 默认分区 + 默认 `where=1=1`);开发者裁剪后再把最终 ini 提交到 `jobs/raw/{domain}/`
+- **不再生成 DDL**:DDL 统一由开发者按 `21-命名规范.md` 手写到 `manual/ddl/{layer}/{domain}/`(CLAUDE.md 单一来源约束,与 §2.1 已登记项一致)
+
+**目录约定**:
+- `workspace/` 在仓库根,**仅存在于开发者本地**,整个目录进 `.gitignore`
+- `workspace/{yyyymmdd}/` 按运行日期分子目录,便于开发者看"我今天生成了哪些候选"
+- 与 `manual/imports/{yyyymmdd}/` 的分工:`manual/imports/` 放一次性**执行**的 SQL / ini(会入仓做审计证据,执行完归档),`workspace/` 放自动化工具**未经人工确认**的中间产物(永不入仓)
+
+**拆除清单**(重写时连带删除):
+- `dw_base/database/mysql_utils.py` 的 `list_tables` / `list_columns` 方法(只服务老 generator)
+- `dw_base/datax/datasources/mysql_data_source.py`
+- `dw_base/datax/plugins/reader/mysql_reader.py` 的 `generate_hive_ddl` / `generate_hive_over_hbase_ddl` 方法
+- `dw_base/datax/datax_utils.py` 的 `convert_mysql_column_types`
+- 所有 mongo / kafka / hbase writer 在 generator 里的分支
+
+**注意**:以上删除范围与 `dw_base/datax/plugins/` 里仍在被真实采集任务调用的 reader/writer 不冲突 —— 真实采集任务只用到 `HDFSReader` / `HDFSWriter` / `MongoWriter`(如果还有 mongo 采集任务)。删之前要用 `grep -r "from dw_base.datax.plugins.reader.mysql_reader"` 再确认一次。
+
+### 2.8 HDFS 数据源 ini 支持 HA nameservice(**Path B 锁定**)
+
+**结论(凭 2026-04-18 新 CDH 环境实测)**:DataX hdfswriter 在新环境下连 HA 集群**必须**在 json 里显式提供 `hadoopConfig` 块,这是 §2.8 改造的动机。
+
+**为什么 `HADOOP_CONF_DIR` 对 DataX 无效(实测三连)**:
+
+| 测试 | json 内容 | `HADOOP_CONF_DIR` | 结果 |
+|---|---|---|---|
+| 1 | `defaultFS` + 完整 `hadoopConfig` | 未设置 | ✅ 成功 |
+| 2 | 只有 `defaultFS` | 未设置 | ❌ UnknownHostException: nameservice1 |
+| 3 | 只有 `defaultFS` | `/etc/hadoop/conf` | ❌ 仍 UnknownHostException |
+
+`hadoop fs -ls` 命令能解析 `nameservice1`,是因为 `hadoop` shell 脚本把 `$HADOOP_CONF_DIR` **加入了 Java classpath**(`hdfs-site.xml` 才能被 `Configuration` 自动加载)。`datax.py` 启动 Java 时**不做这件事**,classpath 里不含 `/etc/hadoop/conf`,所以 DataX 的 Hadoop `Configuration` 对象是"干净的",只能靠 json `hadoopConfig` 块显式注入 HA 参数。
+
+(老项目在上一个公司/服务器只写 `defaultFS` 也能跑通 HA,最可能的原因是运维把 `hdfs-site.xml` 塞进了 DataX 的 classpath 目录 —— 比如 `datax/conf/` / `datax/lib/` / 某 plugin 的 `libs/`。新环境 `/opt/datax` 下没有这类预置文件,不走这条路。)
+
+**老的 env 设置是死代码**:`dw_base/__init__.py:16` 的 `os.environ['HADOOP_CONF_DIR'] = '/etc/hadoop/conf'` 对 DataX JVM 子进程无影响 —— 一是 classpath 不含 conf 目录(见上);二是 DataX 由 `datax-single-job-starter.sh` 通过 `python3 datax.py` 启动,并未 import `dw_base`。这行已在本次 §2.8 一并清理。
+
+---
+
+**正确的 json 形态**:
+
+```json
+"writer": {
+  "name": "hdfswriter",
+  "parameter": {
+    "defaultFS": "hdfs://nameservice1",
+    "hadoopConfig": {
+      "dfs.nameservices": "nameservice1",
+      "dfs.ha.namenodes.nameservice1": "nn1,nn2",
+      "dfs.namenode.rpc-address.nameservice1.nn1": "192.168.33.61:8020",
+      "dfs.namenode.rpc-address.nameservice1.nn2": "192.168.33.62:8020",
+      "dfs.client.failover.proxy.provider.nameservice1": "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider"
+    },
+    ...
+  }
+}
+```
+
+**ini 新格式**:
+
+**非 HA / 单 NN(dev / test 环境)**:
+
+```ini
+[base]
+defaultFS = hdfs://192.168.33.61:8020
+```
+
+**HA / nameservice(prod 环境)**:
+
+```ini
+[base]
+defaultFS = hdfs://nameservice1
+
+[hadoop_config]
+dfs.nameservices = nameservice1
+dfs.ha.namenodes.nameservice1 = nn1,nn2
+dfs.namenode.rpc-address.nameservice1.nn1 = 192.168.33.61:8020
+dfs.namenode.rpc-address.nameservice1.nn2 = 192.168.33.62:8020
+dfs.client.failover.proxy.provider.nameservice1 = org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider
+```
+
+**设计要点**:
+- `[hadoop_config]` 节的 key **原样照搬 `/etc/hadoop/conf/hdfs-site.xml`**,不做缩写 / 翻译 —— 运维直接复制粘贴,出了问题可以字段级对比,零心智成本
+- `[hadoop_config]` 节**可选**:不写即视为非 HA,代码不注入 `hadoopConfig`,生成的 json 只有 `defaultFS`,与单 NN 环境兼容
+- 将来若要支持多 nameservice(federation),`[hadoop_config]` 节天然兼容(多写几组 key 即可),无需再改 ini schema
+
+**代码改造清单**:
+
+| 文件 | 改动 |
+|------|------|
+| `dw_base/datax/datasources/hdfs_data_source.py` | 覆写 `get_datasource_dict()`:父类逻辑外,检测 `[hadoop_config]` 节是否存在;存在则把整节作为 dict 塞进 `ds_dict['hadoopConfig']` |
+| `dw_base/datax/plugins/plugin.py` | **不用改** —— `load_data_source()` 的 `for key, value in ds_dict.items()` 里,value 是 dict 时走 Python 原生赋值,json 序列化自然成立 |
+| `dw_base/datax/plugins/reader/hdfs_reader.py` / `writer/hdfs_writer.py` | **不用改**,`defaultFS + hadoopConfig` 由 `load_data_source()` 自动注入到 `parameter` |
+| `dw_base/__init__.py:16` | **删除** `os.environ['HADOOP_CONF_DIR']` 死代码(实测对 DataX JVM 无影响) |
+| `bin/datax-gc-generator.py` | §2.7 从零重写时一并处理,这里不单独列 |
+| `datasource/hdfs/{env}/*.ini` | 按新 schema 新建(prod 带 `[hadoop_config]`;dev/test 只写 `[base] defaultFS`) |
+
+**回归测试**:
+- prod HA 集群:新 ini 跑一次真实 PG→HDFS 任务,生成 json 含完整 `hadoopConfig`,任务成功(2026-04-18 已用手写 json 预验证)
+- NameNode 主备切换期间跑一次,DataX 自动切到 standby(HA 的原始动机)
+- dev / test 单 NN 集群:不写 `[hadoop_config]`,生成 json 只含 `defaultFS`,任务成功
+
+### 2.9 DataX 速率控制外移到 conf(中优先级)
+
+**现状**:`dw_base/datax/job_config_generator.py:60-67` 硬编码分时调度:
+
+```python
+local_time = int(datetime_utils.formatted_now('%H%M'))
+if 750 < local_time < 1900:
+    speed = self.get_speed(10, byte=10485760, record=40000)          # 白天 10 channel × 10 MB/s
+    core_speed = self.get_core_speed(byte=10485760, record=40000)
+else:
+    speed = self.get_speed()                                          # 夜间 6 channel × 256 MB/s
+    core_speed = self.get_core_speed()
+```
+
+**问题**:
+- 白天 / 夜间的边界(0750、1900)、channel 数、单 channel 速率全写死在代码里,想调一个值要改 py 源码重新发布
+- 不同业务域 / 不同表的速率诉求可能不同,现在一刀切
+- 生产偶发突发(白天要抢刷一次全量)没有临时放大速率的口子
+- 硬编码的动机在代码里一字没留(为什么 0750-1900?避开业务高峰?避开实时链路?后人读代码猜不到)
+
+**目标**:把速率配置抽到 `conf/datax-speed.conf`,`JobConfigGenerator` 运行时读;默认值 = 现硬编码值,保持向后兼容。
+
+**conf 格式**(ini,按时间段分段,自上而下匹配,未命中走 `[default]`):
+
+```ini
+; conf/datax-speed.conf
+; 速率档位定义:从上到下依次匹配,第一条命中的为准;无匹配走 [default]
+
+[daytime]
+hours = 07:50-19:00
+channel = 10
+byte_per_channel = 10485760       ; 10 MB/s/channel,避开业务高峰
+record_per_channel = 40000
+
+[default]
+channel = 6
+byte_per_channel = 268435456      ; 256 MB/s/channel,夜间放满
+record_per_channel = 100000
+```
+
+**代码改造**:
+- 在 `JobConfigGenerator.__init__()` 读 `conf/datax-speed.conf`
+- 用当前时间在各段 `hours` 里找匹配,取 `channel / byte_per_channel / record_per_channel`
+- `assemble()` 里的分支逻辑改为 `speed = self.resolve_speed_profile()` 一行
+- 落 `conf/datax-speed.conf` 时在文件头注释里把**"白天是为了避开业务高峰"**的动机写清楚(消除默认值的来历盲区)
+
+**扩展方向**(P2,先不做):
+- 支持按 ini 名(业务表)在 conf 里覆盖特定任务的速率
+- 支持命令行 `-speed fast` / `-speed slow` 手动切档(突发高峰 / 限流时用)
+
 ## 三、`__init__.py` 瘦身(高优先级)
 
 **现状:** `tendata/__init__.py` 约 120 行,import 即执行以下操作:
@@ -427,16 +623,23 @@ else
 fi
 ```
 
-**问题:**
-- `alvis` 是老环境硬编码,新环境部署用户是 `bigdata`,迁移时必须一起改
-- "按执行者身份决定日志路径"把运行身份与路径策略耦合在一起,代码里到处都要判断当前用户
-- 调度执行(`bigdata`)和个人调试的日志散落到不同目录,排查问题时需要来回切换
-- 本质是把环境差异写进代码,而不是写进配置
-
-**建议:**
-1. 删除 `whoami == RELEASE_USER` 分支逻辑
-2. 日志根路径统一由 `conf/env.sh` 的 `LOG_ROOT_DIR` 决定(默认 `/opt/data/log`),个人调试可在自己的 shell 里 `export LOG_ROOT_DIR=~/data/log` 覆盖
-3. `RELEASE_USER` 若仍需保留(如 publish.sh 发布身份校验),只作为白名单,不参与日志路径决策
+**方向(凭记忆:用户 2026-04-18 确认):分流策略保留,但目的地形态变更**
+
+- release 用户(`bigdata` / `dolphinscheduler`)的生产调度作业:日志落到 `/opt/data/log/{module}/{dt}/{file}.log`
+- 个人调试:落到 `~/log/{module}/{dt}/{file}.log`(不是原来的 `~/data/log`,去掉中间 `data/` 一级)
+
+**为什么保留分流**:个人调试的日志本来就不该和生产日志混在同一系统目录(权限、轮转、审计、磁盘空间都不一样);而统一路径又会引入"调度用户没写权限"类新问题。保留分流是务实选择。
+
+**为什么改目的地形态为 `{module}/{dt}/{file}.log`**:
+- 当前老结构 `/opt/data/log/datax/20260418/xxx.log` 已按 `{module}/{dt}/` 分,但不是所有入口都遵守(spark、ds 等散落在各自子结构下)
+- 新结构强制三级 `{module}/{dt}/{file}.log`,便于按天归档 + 按模块清理
+- `{module}` 取值:`datax` / `spark` / `ds` / `csv` / `export` 等顶层入口名
+
+**代码改动:**
+1. 保留 `whoami == RELEASE_USER` 分支逻辑,但分支里走新模板路径
+2. `LOG_ROOT_DIR` 放到 `conf/env.sh`,两个分支里显式分别赋值为 `/opt/data/log` 和 `${HOME}/log`
+3. 日志文件路径拼接统一走一个工具函数 `log_path(module, dt, file)`(Python 和 Shell 各一份),避免入口脚本各自拼
+4. `RELEASE_USER` 作为单一来源定义在 `conf/env.sh`,与 publish.sh 共用
 
 ### 7.3 部署改进
 

+ 32 - 4
kb/92-重构进度.md

@@ -62,7 +62,7 @@
 - [ ] 新建项目根 `.gitignore`(清单与注意事项见 `90-重构路线.md` §2.4)**— 必须先于 `conf/alerter.conf` 落地,避免敏感文件误入第一次提交**
 - [ ] 建立 `conf/env.sh`(Shell 环境变量)
 - [ ] 建立 `conf/env.py` 或 Python 读 `env.sh` 的桥接
-- [ ] 建立 `conf/workers.conf`(DataX Worker 列表 + 权重)
+- [ ] 建立 `conf/workers.conf`(DataX Worker 列表 + 权重 map,整体迁出 `bin/common/init.sh:18-31`
 - [ ] 建立 `conf/alerter.conf`(钉钉/企微 Webhook,gitignore)
 - [ ] `dw_base/__init__.py` 瘦身(拆分初始化逻辑,见 `90-重构路线.md` §3)**— 必须先做,下面 spark-defaults 依赖瘦身后的 `PROJECT_ROOT_PATH`**
 - [ ] 建立 `conf/spark-defaults.yaml`(Spark 全局默认参数,见 `90-重构路线.md` §2.3)
@@ -71,11 +71,25 @@
 - [ ] 验证:`SET spark.executor.memory=Xg` 不会影响已启动 executor(文档里说清楚这条限制)
 - [ ] `RELEASE_USER="alvis"` → `RELEASE_USER="bigdata"` 并迁入 `conf/env.sh`
 - [ ] `RELEASE_ROOT_DIR="/home/alvis/release"` → `/home/bigdata/release` 并迁入 `conf/env.sh`
-- [ ] `DATAX_WORKERS=(m3 d1 d2 d3 d4)` 迁入 `conf/workers.conf`
-- [ ] `LOG_ROOT_DIR="/opt/data/log"` 迁入 `conf/env.sh`
-- [ ] **删除"按 whoami 分流日志路径"的分支逻辑**(见 `90-重构路线.md` §7.2.1)
+- [ ] `DATAX_WORKERS=(m3 d1 d2 d3 d4)` + 权重 map 迁入 `conf/workers.conf`
+- [ ] `LOG_ROOT_DIR` 放入 `conf/env.sh`(release 分支 `/opt/data/log`、个人分支 `${HOME}/log`,见 `90-重构路线.md` §7.2.1)
+- [ ] **保留 `whoami` 分流**,但目的地改为 `{LOG_ROOT}/{module}/{dt}/{file}.log`(统一 3 层模板)
+- [ ] 实现 `log_path(module, dt, file)` 工具函数(Python / Shell 各一份,单一来源)
 - [ ] 钉钉 access_token 从代码移入 `conf/alerter.conf`
 - [ ] 企微 Webhook Key 从代码移入 `conf/alerter.conf`
+- [ ] **DataX 入口收口为两条命令**(暂称 `datax-import` / `datax-export`,**命名待确认**;见 `90-重构路线.md` §2.6)
+- [ ] 导入命令实现分区管理(`-skip-exist` / `-force-overwrite` / `-skip-partitions`)
+- [ ] 导出命令实现源 HDFS 路径探测(`-src-check` / `-skip-missing`)
+- [ ] `bin/datax-gc-generator.py` 从零重写:定位为**参考模板生成器**(`from=pg to=hdfs`,输出到开发者本地 `workspace/{yyyymmdd}/{name}.ini`,该目录被 `.gitignore` 排除、不入仓;开发者裁剪后把成品提交到 `jobs/raw/{domain}/`),不再生成 DDL(见 `90-重构路线.md` §2.7)
+- [ ] `.gitignore` 增加 `workspace/` 行(见 `90-重构路线.md` §2.4 清单)
+- [ ] 废弃并删除 `mysql_utils`/`MySQLDataSource`/`MySQLReader.generate_hive_ddl*`/`convert_mysql_column_types`(见 §2.7 拆除清单)
+- [x] **HDFS HA 自检(前置决策)**:2026-04-18 新 CDH 环境实测:`HADOOP_CONF_DIR` 未设 + `/opt/datax` 无预置 hdfs-site.xml;手动 `export HADOOP_CONF_DIR=/etc/hadoop/conf` 后跑 DataX 仍 `UnknownHostException: nameservice1`。结论:`HADOOP_CONF_DIR` 对 DataX 无效(`datax.py` 不把 conf 目录入 classpath),**锁定走 Path B**
+- [x] 改造 `HDFSDataSource`:覆写 `get_datasource_dict()` 把 `[hadoop_config]` 整节作为 dict 塞进 `ds_dict['hadoopConfig']`(2026-04-18)
+- [x] 清理 `dw_base/__init__.py:16` 死代码 `os.environ['HADOOP_CONF_DIR']`(2026-04-18,实测对 DataX JVM 无影响)
+- [ ] HDFS 数据源 ini 新建:按新 schema 运维补齐 `datasource/hdfs/{env}/*.ini`(prod 带 `[hadoop_config]`;dev/test 只写 `[base] defaultFS`)
+- [ ] HA 回归测试:真实 HA 集群 + 单 NN 集群 + 主备切换三场景
+- [ ] **DataX 速率配置外移**:`conf/datax-speed.conf` 定义分时速率档;`dw_base/datax/job_config_generator.py:60-67` 硬编码替换为读 conf(见 `90-重构路线.md` §2.9)
+- [ ] 新建 `manual/imports/` + `manual/exports/` 目录(按日期 `{yyyymmdd}/` 组织一次性任务)
 
 ## 阶段 3:业务 SQL 从零开发
 
@@ -123,3 +137,17 @@
 | 2026-04-18 | kb 文档整理:`zhu_tianyu` → `tianyu.chu`(`00-项目架构.md` 2 处负责人注释);`kb/inbox/` 4 份草稿整合完毕 —— `标签服务演进路线.md` 与 `23-标签体系.md §6` 100% 重复直接删除;`dwd明细粒度设计原则.md` 并入 `20-数仓分层与建模.md` §5.5;`hive数据类型映射.md` 并入 `20-数仓分层与建模.md` §8.4(ES→Hive 占位待补);`业务库同步方案.md` 独立成文为 `kb/12-同步方案.md` 并入 README 索引 | — |
 | 2026-04-18 | `02-权限与账号.md §1` 补齐 PySpark 鉴权路线(链路 B):Unix 账号身份 + Ranger UserSync 同时同步 LDAP / Unix group;HS2 doAs 仅链路 A 生效;补漏编号 1 并新增"身份(Who)"条目 | — |
 | 2026-04-18 | 架构框图重排:`01-运行环境.md §1` 大数据平台全景、`20-数仓分层与建模.md §2` 分层 × 维度侧柱、`00-项目架构.md §5` 分层架构图 —— 统一"单层单行 + 右侧 DIM 侧柱"样式,按"中文字符宽 2 / ASCII 宽 1"严格对齐;同步修复三文档数据流 ASCII 尾巴的 orphan `│` 和空行 | — |
+| 2026-04-18 | `02-权限与账号.md §2` Mermaid 时序图补齐链路 B(PySpark → HMS + Ranger Hive Plugin → NameNode + Ranger HDFS Plugin → HDFS,身份=Unix 账号,无 doAs) | — |
+| 2026-04-18 | **反转两条决策**:(a) `manual/ddl/` 目录组织从"扁平存放"→ 按 `{layer}/{domain}/` 分子目录;(b) `jobs/{layer}/{domain}/` 下 SQL 从"一张目标表一个 .sql"→ 简单表仍单文件,**多步表**用 `{表名}/{表名}-{NN}-{描述}.sql` 子目录(`99` 为最终 insert),区分"可复用中间结果应升层"和"单目标加速 tmp 表"两种语义;同步更新 `00-项目架构.md §9.1/§9.2/§9.5` 文件命名速查表 | — |
+| 2026-04-18 | `90-重构路线.md` 新增 §2.6 DataX 入口收口为 `datax-import` + `datax-export`、§2.7 `datax-gc-generator` 从零重写(仅 PG→HDFS,不再生成 DDL)、§2.8 HDFS defaultFS 统一 nameservice;§2.1 workers 行追记"权重 map 一起迁";§7.2.1 反转"删除 whoami 分流"→"保留分流 + 目的地改为 `{module}/{dt}/{file}.log`" | — |
+| 2026-04-18 | `manual/` 新增两个子目录语义:`manual/imports/{yyyymmdd}/`(一次性入仓)+ `manual/exports/{yyyymmdd}/`(一次性出仓),同步更新 `00-项目架构.md §1 目录树` / §9 `manual/` 用途表 / §9.5 文件命名速查 | — |
+| 2026-04-18 | 修正 `90-重构路线.md §2.8` 关于 HDFS nameservice 的表述:代码侧零特殊处理(`dw_base/datax/datasources/hdfs_data_source.py:8,21` 只把 `defaultFS` 当不透明字符串透传),nameservice 解析完全靠 DataX worker 节点的 Hadoop 客户端 + `hdfs-site.xml`;仓库内无 `HADOOP_CONF_DIR` export | — |
+| 2026-04-18 | `90-重构路线.md §2.7` 改写 `datax-gc-generator` 定位为**参考模板生成器**(不是"一键出可用 ini"),产物落 `manual/imports/{yyyymmdd}/` 供开发者按需裁剪字段 / 加 WHERE / 调分区;§2.6 `datax-import` / `datax-export` 名称标注"命名待确认" | — |
+| 2026-04-18 | `30-开发规范.md` 新增 §4.4 Git 提交信息(Conventional Commits):type 白名单(feat/fix/docs/refactor/perf/test/chore/style/build/ci/revert)、scope 约定(`{层}/{域}` 或模块名)、标题 ≤ 50 字符、破坏性变更 `!` + `BREAKING CHANGE:` 正文约定 | — |
+| 2026-04-18 | 新增 `workspace/` 概念:开发者本地草稿区(`datax-gc-generator` 输出的参考模板 / 临时 SQL 调试),被 `.gitignore` 排除、永不入仓。同步更新 `90-重构路线.md §2.4` `.gitignore` 清单增加 `workspace/` 行、§2.7 `datax-gc-generator` 输出目的地从 `manual/imports/` 改为 `workspace/{yyyymmdd}/{name}.ini`;并明确 `workspace/` vs `manual/imports/` 分工 | — |
+| 2026-04-18 | **修正 §2.8**:原"把 `defaultFS` 换 nameservice 就算支持 HA"是错的;DataX HA 必须在 json 里同时提供 `defaultFS` + `hadoopConfig` 块(否则无法把 nameservice 解析为具体 NameNode)。ini schema 升级:新增 `ha_enabled` + `[hadoop_config]` 节,key 照搬 `hdfs-site.xml`;`HDFSDataSource` 需覆写 `get_datasource_dict()`;`reader/writer` 两侧代码不用改(靠 `load_data_source()` 自动注入) | — |
+| 2026-04-18 | **新增 §2.9**:DataX 速率控制从 `job_config_generator.py:60-67` 的分时硬编码(0750-1900 走 10ch×10MB、其它走 6ch×256MB)外移到 `conf/datax-speed.conf`,按时间段分档;默认值保持向后兼容;在 conf 头注释里补"白天避开业务高峰"的动机 | — |
+| 2026-04-18 | **排查"运维写死 nameservice"实现**:全仓 grep `HADOOP_OPTS` / `-Dfs.` / 自定义 `hdfs-site.xml` 写入等全部零命中;现存 `conf/bak/.../hdfs-*.ini` 只有 `defaultFS` 一行。仓库零实现。 | — |
+| 2026-04-18 | 修正早先文档误述:`dw_base/__init__.py:16` 实际上有 `os.environ['HADOOP_CONF_DIR'] = '/etc/hadoop/conf'`(原 2026-04-18 changelog 早条说"仓库内无 HADOOP_CONF_DIR export"不准确) | — |
+| 2026-04-18 | **§2.8 改造降级为"条件触发"**(第三轮修正):用户提供老项目真实生产 json 样例显示只写 `defaultFS`(无 `hadoopConfig`)也能跑 HA —— 说明老 worker 节点 `hdfs-site.xml` 配置完整,`hadoopConfig` 是**可选覆盖**而非 HA 必要条件。前两轮论断("必须加 `hadoopConfig`"、"运维把 xml 写死单 NN")都被推翻。§2.8 加"新环境 HDFS HA 自检清单"(`echo $HADOOP_CONF_DIR` / grep xml HA keys / `hadoop fs -ls hdfs://nameservice1/`),三项全过则整节改造不做;仅任何一项失败才启动 ini schema 升级 + `HDFSDataSource` 改造。92 阶段 2 checklist 相应改为"自检前置 + 条件触发"4 条子项 | — |
+| 2026-04-18 | **§2.8 锁定 Path B(第四轮,实测决定)**:新 CDH 环境三连实测(json 含/不含 `hadoopConfig` × `HADOOP_CONF_DIR` 设/不设),结论:对 DataX JVM,仅 json 的 `hadoopConfig` 块有效,`HADOOP_CONF_DIR` 无效(`datax.py` 不把 conf 目录入 classpath,与 `hadoop` 命令行不同)。老项目能纯 `defaultFS` 跑通最可能是老运维把 `hdfs-site.xml` 塞进了 DataX classpath 目录,新环境 `/opt/datax` 没这类预置文件。改造要点:(a) `HDFSDataSource.get_datasource_dict()` 吃 `[hadoop_config]` 整节注入 `hadoopConfig`;(b) 删除 `dw_base/__init__.py:16` `os.environ['HADOOP_CONF_DIR']` 死代码。简化 §2.8 文本:去掉 `ha_enabled` 开关(用 `[hadoop_config]` 节存在性代替)、去掉自检决策树(已决定)、去掉"运维手工改 IP"误记 | — |