Procházet zdrojové kódy

feat(conf): Spark 参数两文件外配 + spark_sql.py 三级覆盖

tianyu.chu před 2 týdny
rodič
revize
4eccb63140

+ 26 - 0
conf/spark-defaults.conf

@@ -0,0 +1,26 @@
+# Spark 底层运行时默认参数(原生格式:key value,# 注释,空白分隔)
+# 本文件收录行为/开关/调试类参数,初始化后应极少改动。业务调优参数见 conf/spark-tuning.conf。
+# 加载入口:dw_base/spark/spark_sql.py 构造 SparkSession 前加载
+# 覆盖规则:L1 本文件 + spark-tuning.conf < L2 SQL 内 SET(仅 spark.sql.*)< L3 构造函数显式传参 / extra_spark_config
+
+# Hive ORC
+hive.exec.orc.default.block.size                134217728
+
+# 调试
+spark.debug.maxToStringFields                   5000
+
+# 动态分配与端口
+spark.dynamicAllocation.enabled                 false
+spark.port.maxRetries                           999
+
+# 容错:读取时跳过损坏文件
+spark.files.ignoreCorruptFiles                  true
+spark.sql.files.ignoreCorruptFiles              true
+
+# SQL 优化器 / 执行行为
+spark.sql.adaptive.enabled                      true
+spark.sql.broadcastTimeout                      -1
+spark.sql.codegen.wholeStage                    false
+spark.sql.execution.arrow.enabled               true
+spark.sql.execution.arrow.fallback.enabled     true
+spark.sql.statistics.fallBackToHdfs             true

+ 25 - 0
conf/spark-tuning.conf

@@ -0,0 +1,25 @@
+# Spark 业务调优参数(原生格式:key value,# 注释,空白分隔)
+# 本文件收录资源/并行度/队列类参数,新业务落地早期会频繁调整以找到合适默认值。
+# 底层行为/开关类参数见 conf/spark-defaults.conf。
+# 加载入口:dw_base/spark/spark_sql.py 构造 SparkSession 前加载(在 spark-defaults.conf 之后,相同 key 覆盖 defaults)
+# 覆盖规则:L1 spark-defaults.conf + 本文件 < L2 SQL 内 SET(仅 spark.sql.*)< L3 构造函数显式传参 / extra_spark_config
+# 注意:spark.driver.* / spark.executor.* / spark.executor.memoryOverhead 属于资源类参数,session 启动后不可变,
+#       SQL 内 SET 不生效;需临时改资源走 L3(命令行 -sc 或调用方 SparkSQL(...) 显式传参)。
+
+# Driver
+spark.driver.cores                              2
+spark.driver.memory                             2g
+spark.driver.maxResultSize                      4g
+
+# Executor
+spark.executor.cores                            2
+spark.executor.instances                        15
+spark.executor.memory                           6g
+spark.executor.memoryOverhead                   512
+
+# 并行度 / Shuffle
+spark.default.parallelism                       200
+spark.sql.shuffle.partitions                    200
+
+# YARN 队列
+spark.yarn.queue                                spark

+ 60 - 46
dw_base/spark/spark_sql.py

@@ -3,7 +3,7 @@
 import inspect
 import re
 from importlib import import_module
-from typing import List, Union, Dict, Any, Tuple
+from typing import List, Optional, Union, Dict, Any, Tuple
 
 from pyspark.sql import Row, SparkSession, DataFrame
 
@@ -17,6 +17,23 @@ from dw_base.utils.sql_utils import get_sql_list_from_file, check_parameter_subs
 HDFS_EXPORT_DATA_PATH = '/hdfs-mnt/export-data'
 
 
+def _load_spark_conf_file(path: str) -> Dict[str, str]:
+    """读 Spark 原生 conf(每行 `key value`,# 注释,空白分隔)。文件缺失或行非法时返回空 dict,不抛错。"""
+    if not os.path.isfile(path):
+        return {}
+    config = {}
+    with open(path, 'r') as f:
+        for line in f:
+            line = line.strip()
+            if not line or line.startswith('#'):
+                continue
+            parts = line.split(None, 1)
+            if len(parts) != 2:
+                continue
+            config[parts[0]] = parts[1]
+    return config
+
+
 class SparkSQL(object):
     """
     封装执行 Spark 相关操作的类, 相关参数说明:
@@ -49,16 +66,16 @@ class SparkSQL(object):
     def __init__(self,
                  session_name: str = 'spark',
                  master: str = 'yarn',
-                 spark_yarn_queue: str = 'spark',
-                 spark_driver_memory: str = '2g',
-                 spark_executor_memory: str = '6g',
-                 spark_executor_memory_overhead: str = '512',
-                 spark_driver_cores: int = 2,
-                 spark_executor_cores: int = 2,
-                 spark_executor_instances: int = 15,
-                 spark_driver_max_result_size='4g',
-                 spark_shuffle_partitions=200,
-                 spark_default_parallelism=200,
+                 spark_yarn_queue: Optional[str] = None,
+                 spark_driver_memory: Optional[str] = None,
+                 spark_executor_memory: Optional[str] = None,
+                 spark_executor_memory_overhead: Optional[str] = None,
+                 spark_driver_cores: Optional[int] = None,
+                 spark_executor_cores: Optional[int] = None,
+                 spark_executor_instances: Optional[int] = None,
+                 spark_driver_max_result_size: Optional[str] = None,
+                 spark_shuffle_partitions: Optional[int] = None,
+                 spark_default_parallelism: Optional[int] = None,
                  extra_spark_config: Dict[str, Any] = None,
                  udf_files: List[str] = None,
                  resource_files: List[str] = None):
@@ -125,44 +142,41 @@ class SparkSQL(object):
         if self._spark_session:
             return
         pretty_print(f'{NORM_MGT}基于用户 {NORM_GRN}{USER}{NORM_MGT} 创建 SparkSession')
-        # for element in os.environ:
-        #     pretty_print(f'{NORM_MGT}Environment {NORM_GRN}{element} => {os.environ[element]}')
         builder = SparkSession.builder \
             .appName(self._session_name) \
-            .master(self._master) \
-            .config('hive.exec.orc.default.block.size', 134217728) \
-            .config('spark.debug.maxToStringFields', 5000) \
-            .config('spark.default.parallelism', self._spark_default_parallelism) \
-            .config('spark.driver.cores', self._spark_driver_cores) \
-            .config('spark.driver.maxResultSize', self._spark_driver_max_result_size) \
-            .config('spark.driver.memory', self._spark_driver_memory) \
-            .config('spark.dynamicAllocation.enabled', False) \
-            .config('spark.files.ignoreCorruptFiles', True) \
-            .config('spark.executor.cores', self._spark_executor_cores) \
-            .config('spark.executor.instances', self._spark_executor_instances) \
-            .config('spark.executor.memory', self._spark_executor_memory) \
-            .config('spark.executor.memoryOverhead', self._spark_executor_memory_overhead) \
-            .config('spark.sql.adaptive.enabled', 'true') \
-            .config('spark.sql.broadcastTimeout', -1) \
-            .config('spark.sql.codegen.wholeStage', 'false') \
-            .config('spark.sql.execution.arrow.enabled', True) \
-            .config('spark.sql.execution.arrow.fallback.enabled', True) \
-            .config('spark.sql.files.ignoreCorruptFiles', True) \
-            .config('spark.sql.shuffle.partitions', self._spark_shuffle_partitions) \
-            .config('spark.sql.statistics.fallBackToHdfs', True) \
-            .config('spark.yarn.queue', self._spark_yarn_queue) \
-            .config('spark.port.maxRetries', 999)
+            .master(self._master)
+        # L1:conf/spark-defaults.conf(底层)+ conf/spark-tuning.conf(调优,相同 key 覆盖 defaults)
+        l1_defaults = {}
+        l1_defaults.update(_load_spark_conf_file(f'{PROJECT_ROOT_PATH}/conf/spark-defaults.conf'))
+        l1_defaults.update(_load_spark_conf_file(f'{PROJECT_ROOT_PATH}/conf/spark-tuning.conf'))
+        pretty_print(f'{NORM_MGT}L1 加载 {NORM_GRN}{len(l1_defaults)}{NORM_MGT} 条 conf 默认')
+        for key, value in l1_defaults.items():
+            builder.config(key, value)
+        # L2:SQL 内 SET(仅 spark.sql.* 生效,资源类参数 session 启动后不可变)
+        for key, value in self._final_spark_config.items():
+            pretty_print(f'{NORM_MGT}L2 应用 SQL SET {NORM_GRN}{key} => {str(value)}')
+            builder.config(key, value)
+        # L3:构造函数显式传参 + extra_spark_config
+        l3_overrides: Dict[str, Any] = {}
+        for conf_key, attr_val in (
+                ('spark.yarn.queue', self._spark_yarn_queue),
+                ('spark.driver.memory', self._spark_driver_memory),
+                ('spark.executor.memory', self._spark_executor_memory),
+                ('spark.executor.memoryOverhead', self._spark_executor_memory_overhead),
+                ('spark.driver.cores', self._spark_driver_cores),
+                ('spark.executor.cores', self._spark_executor_cores),
+                ('spark.executor.instances', self._spark_executor_instances),
+                ('spark.driver.maxResultSize', self._spark_driver_max_result_size),
+                ('spark.sql.shuffle.partitions', self._spark_shuffle_partitions),
+                ('spark.default.parallelism', self._spark_default_parallelism),
+        ):
+            if attr_val is not None:
+                l3_overrides[conf_key] = attr_val
         if self._extra_spark_config:
-            for spark_config, config_value in self._extra_spark_config.items():
-                if self._final_spark_config.__contains__(spark_config):
-                    pretty_print(f'{NORM_YEL}构造函数传入的 Spark 配置 {NORM_GRN}{spark_config} => {config_value} '
-                                 f'{NORM_YEL}覆盖了在 SQL 文件中定义的配置 '
-                                 f'{NORM_GRN}{spark_config} => {self._final_spark_config[spark_config]}')
-                self._final_spark_config[spark_config] = config_value
-        if self._final_spark_config:
-            for key, value in self._final_spark_config.items():
-                pretty_print(f'{NORM_MGT}添加自定义 Spark 配置 {NORM_GRN}{key} => {str(value)}')
-                builder.config(key, value)
+            l3_overrides.update(self._extra_spark_config)
+        for key, value in l3_overrides.items():
+            pretty_print(f'{NORM_MGT}L3 应用构造参数/extra {NORM_GRN}{key} => {str(value)}')
+            builder.config(key, value)
         pretty_print(f'{NORM_MGT}创建 SparkSession')
         self._spark_session = builder.enableHiveSupport().getOrCreate()
         self._spark_session.sparkContext._jsc.hadoopConfiguration().set('mapred.max.split.size', '33554432')

+ 37 - 98
kb/90-重构路线.md

@@ -22,26 +22,21 @@
 ### 依赖 DAG
 
 ```
-          ┌───────────────┐
-          │ B1 __init__   │
-          │    瘦身       │
-          └───────┬───────┘
-                  ↓
-     ┌────────────┴────────────┐
-     ↓                         ↓
-┌──────────┐            ┌──────────────┐
-│ A2 spark │            │ B2 四模块    │
-│ defaults │            │    边界定稿  │
-└──────────┘            └──────┬───────┘
-                               ↓
-                     ┌─────────┴──────────┐
-                     ↓                    ↓
-              ┌──────────┐         ┌──────────┐
-              │ B4 新占位 │         │  C bin   │
-              │  (骨架)   │         │  收口    │
-              └──────────┘         └──────────┘
-
-A 其余项 ───────┐
+┌──────────────┐
+│ B2 四模块    │
+│    边界定稿  │
+└──────┬───────┘
+       ↓
+ ┌─────┴──────┐
+ ↓            ↓
+┌──────────┐ ┌──────────┐
+│ B4 新占位 │ │  C bin   │
+│  (骨架)   │ │  收口    │
+└──────────┘ └──────────┘
+
+A 全部子项 ─────┐
+B1 __init__ 瘦身┤  ✅ 2026-04-21
+A2 spark conf ─┤  ✅ 2026-04-21
 B3 风格修正 ────┤ 可独立推进,与上图并行
 D 基础设施 ─────┘
 
@@ -56,7 +51,6 @@ D 基础设施 ─────┘
 
 **关键依赖边**(强依赖,不能翻转):
 
-- **B1 → A2**:`spark-defaults.conf` 路径解析依赖瘦身后的 `PROJECT_ROOT_PATH`,反过来做会踩返工
 - **B2 → B4**:四模块(`common/utils/io/ops`)边界不定,新占位模块放哪里都是赌博
 - **B2 → C**:`bin/` 两命令的底层实现要放 `dw_base/datax/entry.py`,属于四模块定稿后的延伸
 - **新业务 SQL 生产稳定 → F**:`launch-pad/` 删除前置条件是新 `jobs/` 在生产跑稳一个完整周期(新业务 SQL 开发本身不纳入重构 scope)
@@ -136,85 +130,32 @@ conf/
 ├── env.sh                    # Shell + Python 环境变量单源(Python 侧由 dw_base/utils/env_loader.py 通过 bash 子进程解析注入 os.environ)
 ├── workers.ini               # DataX Worker 列表与权重
 ├── alerter.ini               # 告警 Webhook 配置(入库;见 §2.1)
-└── spark-defaults.conf       # Spark 默认参数(Spark 原生格式)
+├── spark-defaults.conf       # Spark 底层行为/开关类(12 条,初始化后少改;Spark 原生格式)
+└── spark-tuning.conf         # Spark 资源/并行度/队列类(10 条,业务早期常改;同 tuning 相同 key 覆盖 defaults)
 ```
 
-### 2.3 Spark 配置三级覆盖策略
+### 2.3 Spark 配置三级覆盖策略(已完成)
 
-**现状**:`dw_base/spark/spark_sql.py` 构造函数里硬编码了约 15 个 `.config(...)` 调用(executor/driver/memory/parallelism/shuffle/adaptive/arrow/codegen 等),默认值写死在构造参数里,覆盖只能通过 SparkSQL 构造函数传参或 SQL 文件内 `SET`
+**2026-04-21 落地形态**:按业务调整频率拆两文件入库 + `spark_sql.py` 三级覆盖
 
-**问题**:
-- 想批量调整 shuffle partitions 的默认值,就得改代码 + 发版
-- 不同类型的作业(dwd 大宽表 / ads 小聚合)需要不同默认,现状只能每张表的 SQL 开头都重复写一遍 `SET`
-- 默认参数和业务代码耦合,不便于运维按集群负载动态调整
-
-**目标态:三级覆盖**
-
-```
-conf/spark-defaults.conf         (L1) 全局默认,运维可改,发版同步到集群
-        ↓ 被覆盖
-SQL 文件内 SET spark.xxx=yyy     (L2) 单作业级别的覆盖,业务开发写
-        ↓ 被覆盖
-命令行 -sc key=value / Python 构造函数传参  (L3) 临时/调试 override
-```
+**两文件拆分**:
 
-**`conf/spark-defaults.conf` 草案**:
-
-沿用 Spark 官方 `$SPARK_HOME/conf/spark-defaults.conf` 的格式:**flat `spark.x.y  value`**,空白分隔,`#` 注释,无 section。好处:运维熟悉、与 `spark-submit --properties-file` 原生兼容、代码侧零映射转换。
-
-```conf
-# 全局 Spark 默认参数,dw_base/spark/spark_sql.py 启动时加载
-# 单作业需要覆盖时,在对应 jobs/*.sql 文件开头写 SET;不要改本文件
-
-# Executor
-spark.executor.instances                    4
-spark.executor.cores                        4
-spark.executor.memory                       8g
-spark.executor.memoryOverhead               2g
-
-# Driver
-spark.driver.cores                          2
-spark.driver.memory                         4g
-spark.driver.maxResultSize                  2g
-
-# SQL
-spark.sql.shuffle.partitions                200
-spark.sql.adaptive.enabled                  true
-spark.sql.broadcastTimeout                  -1
-spark.sql.codegen.wholeStage                false
-spark.sql.execution.arrow.enabled           true
-spark.sql.execution.arrow.fallback.enabled  true
-spark.sql.files.ignoreCorruptFiles          true
-spark.sql.statistics.fallBackToHdfs         true
-
-# Default parallelism
-spark.default.parallelism                   400
-```
+- `conf/spark-defaults.conf`(12 条)—— 底层行为/开关类,初始化后少改(`spark.sql.adaptive/broadcastTimeout/codegen/arrow*/files/statistics.*` + `spark.dynamicAllocation.enabled` + `spark.files.ignoreCorruptFiles` + `spark.debug.maxToStringFields` + `spark.port.maxRetries` + `hive.exec.orc.default.block.size`)
+- `conf/spark-tuning.conf`(10 条)—— 资源/并行度/队列类,业务早期常改(`spark.{driver,executor}.{memory,cores}` + `spark.executor.instances` + `spark.executor.memoryOverhead` + `spark.driver.maxResultSize` + `spark.default.parallelism` + `spark.sql.shuffle.partitions` + `spark.yarn.queue`)
 
-**代码改动要点**:
+两文件都用 Spark 原生 `key value` 格式(空白分隔、`#` 注释、无 section),与 `spark-submit --properties-file` 同语法。
 
-1. `dw_base/spark/spark_sql.py`
-   - 新增 `_load_default_config() -> dict`:逐行读 `conf/spark-defaults.conf`,跳过空行与 `#` 注释,首段空白切成 `(key, value)`;key 已是全限定 `spark.x.y`,直接作为 dict key 返回,不做任何变换
-   - 构造函数接收的显式参数(`spark_executor_cores` 等)改为 `None` 默认,若未传则 fall back 到 conf
-   - `SparkSession.builder` 的 `.config(...)` 链改成 `for k, v in resolved_config.items(): builder.config(k, v)`
-2. SQL 文件内的 `SET spark.xxx=yyy` 本来就由 `spark.sql(...)` 原生支持,无需改动
-3. 命令行 `-sc` 参数保持现有语义,覆盖 L1
-4. **Python 单测要能跑**:conf 读取要容错(测试环境下找不到 conf 文件时回退到一套最小内置默认,不阻塞 `tests/unit/`)
+**三级覆盖**:
 
-**兼容性**:老代码里已在写 `SparkSQL(spark_executor_cores=8, ...)` 的调用站点不破坏,因为显式传参仍是最高级(L3)。
-
-**落地时的两个坑**:
-
-1. **L2 覆盖只对 `spark.sql.*` 系参数生效**。Spark 的参数分两类:
-   - `spark.sql.*`、`spark.default.parallelism` 等运行时参数 —— `spark.conf.set(...)` 或 SQL 内 `SET` 可动态改写
-   - `spark.executor.*`、`spark.driver.*`、`spark.executor.memoryOverhead` 等资源类参数 —— **session 启动时锁定**,SQL 里写 `SET spark.executor.memory=16g` 不会真的扩容已启动的 executor
-
-   因此开发写 SQL 内 `SET` 时只能调 `spark.sql.*` 和并行度;需要改资源的场景只能走 L3(命令行 `-sc` 或调用方在构造 `SparkSQL(...)` 时显式传参)。文档里和 `spark-defaults.conf` 注释里都要讲清楚这条,避免开发以为 `SET spark.executor.memory` 有效。
+```
+L1   conf/spark-defaults.conf  +  conf/spark-tuning.conf     (相同 key tuning 覆盖 defaults)
+     ↓
+L2   SQL 文件内 SET spark.xxx=yyy     (仅 spark.sql.* 生效,资源类参数 session 启动后不可变)
+     ↓
+L3   SparkSQL(...) 显式传参  +  extra_spark_config  +  命令行 -sc
+```
 
-2. **`conf/spark-defaults.conf` 的路径解析依赖 `PROJECT_ROOT_PATH`**,这和 §三 `__init__.py` 瘦身存在先后依赖:
-   - 现状 `PROJECT_ROOT_PATH` 在 `dw_base/__init__.py` 顶部定义,`import dw_base` 就会拿到
-   - 瘦身后 `__init__.py` 只保留最基本路径定义,`PROJECT_ROOT_PATH` 仍可用,但拆分过程中要保证 `spark_sql.py` 加载 conf 的那行代码拿到的根路径与瘦身前一致
-   - **执行顺序建议**:先做 §三 `__init__.py` 瘦身,把 `PROJECT_ROOT_PATH` 的定义稳定下来;再做 §2.3 的 `spark-defaults.conf` 接入。反过来做会踩到"瘦身后路径变了"的返工
+**落地坑**:`SET spark.executor.memory=16g` 等资源类参数 session 启动后锁定,SQL 内 SET 不生效(Spark 行为,非本项目限制)。改资源只能走 L3。代码实现见 `dw_base/spark/spark_sql.py:_load_spark_conf_file` 与 `__init_spark_session`。
 
 **与仓库改名的联动**:
 
@@ -720,7 +661,7 @@ else:
 | `conf/env.sh`(LOG_ROOT_DIR / RELEASE_USER / RELEASE_ROOT_DIR / PYTHON3_PATH / DATAX_HOME) | 待启动 | — | §2.1 / §7.2.1 |
 | `conf/workers.ini`(DataX Workers + 权重 map 外移) | 待启动 | — | §2.1 |
 | `conf/alerter.ini`(告警 Webhook,入库) | 待启动 | 旧告警代码删除(已 2026-04-20 完成) | §2.1 |
-| `conf/spark-defaults.conf`(Spark 全局默认参数,Spark 原生 flat 格式) | 待启动 | **B1 `__init__.py` 瘦身** | §2.3 |
+| `conf/spark-defaults.conf`(底层 12 条)+ `conf/spark-tuning.conf`(调优 10 条)+ `spark_sql.py` 三级覆盖 | ✅ 2026-04-21 | — | §2.3 |
 | `conf/datax-speed.ini`(DataX 分时速率) | 待启动 | — | §2.9 |
 | `datasource/{db_type}/{env}/{instance}.ini` 多环境分层 | 待启动 | — | §2.5 |
 | DataX 脚本去前缀剥离 + 加 `-env` 参数 | 待启动 | datasource 多环境 | §2.5 |
@@ -730,7 +671,7 @@ else:
 
 | 子项 | 状态 | 依赖 | 参见 |
 |------|------|------|------|
-| **B1** `__init__.py` 瘦身 + `PROJECT_ROOT_PATH` 稳定下来 | 待启动 | — | §三 |
+| **B1** `__init__.py` 瘦身(修剪式) | ✅ 2026-04-21 | — | §三 |
 | **B2** `common/utils/io/ops` 四模块边界定稿 | 待启动 | — | §2.10 |
 | **B3** `__contains__` → `in` 全局替换 | 待启动 | — | §4.1 |
 | **B3** Shell/Python 环境检测去重(`bin/common/init.sh` ↔ `dw_base/__init__.py`) | 待启动 | B1 | §4.2 |
@@ -772,14 +713,12 @@ else:
 **本阶段可并行开工**(无前置阻塞):
 
 1. A 大部分子项(env.sh / workers.ini / alerter.ini / datax-speed.ini)
-2. B1 `__init__.py` 瘦身(解锁 A2 spark-defaults.conf)
-3. B2 四模块边界定稿(只需写决策,不改代码)
-4. B3 代码风格修正(`__contains__` 全替换)
-5. D 首批 UDF 单测(tests 骨架已建)
+2. B2 四模块边界定稿(只需写决策,不改代码)
+3. B3 代码风格修正(`__contains__` 全替换)
+4. D 首批 UDF 单测(tests 骨架已建)
 
 **等待前置**:
 
-- A2 spark-defaults.conf ← B1
 - C `bin/` 两命令 / `datax-gc-generator` 重写 ← B2 + A datax
 - D `ops/` 下两个工具 ← B2
 - F `launch-pad/` 整删 ← 新业务 SQL 生产稳定一个完整周期(新业务开发本身不属于重构 scope)

+ 3 - 2
kb/92-重构进度.md

@@ -68,8 +68,8 @@
 - [ ] 建立 `conf/workers.ini`(DataX Worker 列表 + 权重 map,整体迁出 `bin/common/init.sh:18-31`)
 - [ ] 建立 `conf/alerter.ini`(企微 Webhook,**入库**;格式见 `90-重构路线.md` §2.1)
 - [x] `dw_base/__init__.py` 瘦身(2026-04-21,修剪式,不拆 `core/`;见 `90-重构路线.md` §三 已完成态)
-- [ ] 建立 `conf/spark-defaults.conf`(Spark 全局默认参数,Spark 原生格式,见 `90-重构路线.md` §2.3)
-- [ ] 改造 `dw_base/spark/spark_sql.py`:构造函数 fall back 到 conf;实现 L1(conf) < L2(SQL 内 SET,仅 `spark.sql.*` 系生效) < L3(命令行 -sc / 构造函数传参) 三级覆盖
+- [x] 建立 `conf/spark-defaults.conf`(底层行为/开关类 12 条,少改)+ `conf/spark-tuning.conf`(资源/并行度 10 条,业务常改)(2026-04-21,Spark 原生格式;两文件拆分,见 `90-重构路线.md` §2.3)
+- [x] 改造 `dw_base/spark/spark_sql.py`:构造函数 10 个 tuning 默认值 → `None` sentinel;新增 `_load_spark_conf_file()`;`__init_spark_session` 按 L1(两 conf 叠加) < L2(SQL SET) < L3(构造参数非 None + `extra_spark_config`) 三级覆盖(2026-04-21)
 - [ ] 验证:同一条 SQL 在无 SET、有 SET、命令行 -sc 三种场景下 `spark.conf.get(...)` 返回值符合优先级预期
 - [ ] 验证:`SET spark.executor.memory=Xg` 不会影响已启动 executor(文档里说清楚这条限制)
 - [x] `RELEASE_USER="alvis"` → `RELEASE_USER="bigdata"` 并迁入 `conf/env.sh`
@@ -179,3 +179,4 @@
 | 2026-04-21 | **SQL 风格基线尝试后撤回**:本轮前半段把 `sql_style.xml` 从 `conf/` 挪到项目根,并在 kb/30 §3.2.1 / §3.2.2 立了强基线(关键字/类型 UPPER、SELECT/FROM/ORDER/GROUP 一项一行、JOIN/ON 缩进、CASE/CTE/UNION/OVER/INSERT OVERWRITE/分号九条换行与缩进样例)。实测 IDEA formatter 支持面不足(KEYWORD_CASE 仅作用于新输入不改存量、SELECT 前置逗号长期不支持、ORDER/GROUP 一项一行的 option 名 JetBrains 非公开、UNION 前后空行 formatter 不管、CASE THEN/END 独立行无选项控制),强约束无法靠 formatter 落地。本轮后半段全部回退:删 `sql_style.xml`、kb/30 §3.2.1 / §3.2.2 整节删除、原 §3.2.3 不对齐 AS 重编号为 §3.2.1。团队 SQL 格式化改由各自 IDEA 默认 + 项目 SQL 方言统一设为 Spark 承担,冲突走 review | — |
 | 2026-04-21 | **下线"阶段 3:业务 SQL 从零开发" + 取消 UDF 注释补齐 + kb/31 首批登记 13 个通用 UDF**:(a) 业务 SQL 从零开发属于新开发、不属于重构 scope:kb/92 总览表删阶段 3 行、阶段 3 整节删除、阶段 5 前置条件从"阶段 3 稳定"改为"新业务 SQL 稳定";kb/90 §〇 聚簇表删 E 行、DAG 图删 E 节点、关键依赖边 "A+B+C→E" 与 "E→F" 合并为 "新业务 SQL 生产稳定→F"、§八 聚簇 E 整节删除、当前推进建议"等待前置"里 E 行改为 F 行。(b) 通用 UDF 注释已由开发者手动补完(`spark_common_udf.py` 13 个 `@udf` 函数均带 `UDF-XX` 顺序编号 + 分节注释),kb/90 §2.12 删"5 段模板 + 5 批 commit"规划、"40 函数"更正为"13 个注册 UDF"、标题从"注释完整化 + 自查表"改为"自查表"。(c) `kb/31-UDF手册.md` §1 通用 UDF 表从空壳填入 13 行(UDF-01/02/21/22/23/31/32/33/41/42/51/52/53),分类按代码中分节注释(JSON / ARRAY / STRING / NUMERIC-DATE-HASH / CROSS-TYPE),函数编号按代码中 `UDF-XX` 注释;§2 业务 UDF 保持占位;非 `@udf` 普通 `def`(18 个辅助 / 工具函数)不登记 | — |
 | 2026-04-21 | **聚簇 B.1 `__init__.py` 瘦身(修剪式,不拆 `core/`)**:`dw_base/__init__.py` 从 127 行 → 83 行。三处删除:(a) `import findspark` + `findspark.init()` —— 查证 3 条事实后安全删:findspark 全仓仅此 2 处引用;入口全走 `python3 xxx.py`(非 `spark-submit`),`SPARK_HOME` 从未被代码注入,findspark 在 CDH 节点上 `which spark-submit → readlink -f` 反推出 parcel `$SPARK_HOME` 把 `$SPARK_HOME/python` 前插进 sys.path,但 pip pyspark 2.4.0 和 parcel pyspark 2.4.0 同版本,业务表现零差异(见里程碑 `datax+spark-smoke-2026-04-20` 冒烟链路,HMS 真正入口是 `SPARK_CONF_DIR=/etc/spark/conf/hive-site.xml`,与 findspark 无关);(b) 删 21 个外部零引用的颜色常量 —— `CHG_BOLD` / `NORM_BLU` / `NORM_WHT` / 7×`BOLD_*` / 7×`BGRD_*`(if/else 两分支同步删),保留实际被引用的 6 个(`DO_RESET` / `NORM_RED` / `NORM_GRN` / `NORM_YEL` / `NORM_MGT` / `NORM_CYN`);(c) 删 `IS_RUN_BY_NORMAL_USER` 状态变量(两处赋值外部无引用,仅内部 `elif` 分支走到时为 `True`,无消费者)。**不拆 `core/*` 的理由**:findspark 去掉后"懒加载"诉求大半消失,拆分需改 11 处调用点 import,ROI 低;py/sh 颜色双份是运行时分家的必然(跨 runtime 单源化要加 subprocess 解析,得不偿失),真冗余只是 py 侧定义超过实际被用的部分。联动:`requirements.txt:3` 删 `findspark==2.0.1`;`tests/README.md:26` findspark 段改写为 HMS 入口说明;`kb/00 §1` `__init__.py` 行注释去 findspark;`kb/90 §三` 改写为"已完成 · 修剪式"并附未拆 `core/` 的理由;`kb/90 §7.1` KEEP 行去 findspark + 末尾 TODO 行改写为"已删除" | — |
+| 2026-04-21 | **聚簇 A.4 Spark 参数外配 + `spark_sql.py` 三级覆盖**:按业务调整频率拆两文件入库 —— `conf/spark-defaults.conf`(12 条底层行为/开关类,初始化后少改:`spark.sql.adaptive/broadcastTimeout/codegen/arrow*/files/statistics.*` + `spark.dynamicAllocation.enabled` + `spark.files.ignoreCorruptFiles` + `spark.debug.maxToStringFields` + `spark.port.maxRetries` + `hive.exec.orc.default.block.size`)+ `conf/spark-tuning.conf`(10 条资源/并行度/队列,业务早期常改:`spark.{driver,executor}.{memory,cores}` + `spark.executor.instances` + `spark.executor.memoryOverhead` + `spark.driver.maxResultSize` + `spark.default.parallelism` + `spark.sql.shuffle.partitions` + `spark.yarn.queue`)。`dw_base/spark/spark_sql.py` 改造:(a) 模块级新增 `_load_spark_conf_file(path)`,读 Spark 原生 `key value` 格式,支持 `#` 注释与空行,文件缺失返回 `{}` 容错单测;(b) `__init__` 10 个 tuning 相关构造参数默认值 `'2g' / 200 / ...` → `Optional[...] = None` sentinel,不破坏既有调用点显式传参;(c) `__init_spark_session` 原 22 条硬编码 `.config(...)` 链替换为三段:L1 先 `spark-defaults.conf` 后 `spark-tuning.conf`(相同 key tuning 覆盖 defaults)→ L2 `self._final_spark_config`(SQL 内 SET)→ L3 构造参数非 None 项 + `extra_spark_config`(L3 内 extra 覆盖 named),保持原"extra > SQL SET > named" 的向后兼容;日志分层打 `L1/L2/L3` 前缀便于排查。联动:`kb/90 §2.2` conf 结构加 `spark-tuning.conf` + `§2.3` 改写为两文件模型(去单文件草案)+ 删"坑 2"(B1 → A2 依赖边)+ 聚簇 L59 依赖边删 | — |