DDB 后端系统性优化:bug 修复、并发安全、健壮性与查询性能(面向社区版 2核/8GB) - #3
Conversation
- 删除 inst_processors 分支内提前 return pd.DataFrame() 的历史 bug(处理结果被整体丢弃且跳过归一化) - 并行处理结果 concat 后 sort_index 保证确定性,落入统一的 cache_to_origin_data 路径 - 新增 tests/test_ddb_dataset_processor.py 离线回归(注入假 ExpressionD 与顺序 ParallelExt,覆盖分块与组合路径) Co-Authored-By: Hugo <shen.lan123@gmail.com>
- 连接池由类变量改为实例属性:类变量会导致连接不同服务器的客户端共享同一个池 - close_pool 曾引用不存在的 cls._pool_lock(AttributeError 被裸 except 吞掉)且永远读到 None,实际从未关闭过池;改为实例方法 + 实例锁的双重检查 - 删除死方法 tableAppender/tableUpsert(调用不存在的 self.get_session(),全仓零调用者;正确实现在 DDBTableOperator) - 删除 __main__ 块中的硬编码服务器凭据 - 新增显式 close();日志统一走 get_module_logger - 新增 tests/test_ddb_client.py 离线单元测试(fake dolphindb SDK) Co-Authored-By: Hugo <shen.lan123@gmail.com>
init 时急切执行 connector.pool 会向服务器开 4 条无人使用的连接, 在社区版(2 核/8GB)上白占服务器内存。改为惰性 @Property, 名称与类型保持兼容(DBClient.pool 首次访问时才建池)。 Co-Authored-By: Hugo <shen.lan123@gmail.com>
插件名笔误导致 MySQL 桥接的 mysql::connect 必然失败。 新增 tests/test_ddb_mysql_bridge.py 离线回归。 Co-Authored-By: Hugo <shen.lan123@gmail.com>
__exit__/__del__ 曾无条件 close 进程全局的 DBClient.session—— 一个 with 块或一次 GC 就会把共享会话关掉,破坏 storage 与 feature 路径的所有其他使用方。引入 _owns_session 所有权标志, 仅独占会话时才允许关闭;__del__ 对构造中途失败也安全。 Co-Authored-By: Hugo <shen.lan123@gmail.com>
ddb.Session 非线程安全,且 feature 查询是「run(上传日期)→upload(变量)→ run(主查询)」的多步会话对话,并发交错会互相覆盖服务器端变量(跨线程 数据污染根因)。在 DolphinDBClientProvider 上新增 session_lock(RLock, storage 读取可嵌套于 feature 查询持锁期间),并约定所有 DBClient.session 触点必须持锁: - dolphindb_storage: exists/_read_calendar/_read_instrument/DBFeatureStorage.__getitem__ - loader.DolphinDBDataLoader: _validate_table_exists/_load_via_dolphindb/_load_from_dolphindb/get_table_info 新增 tests/test_ddb_concurrency.py:多线程交错检测 + RLock 可重入 + 接口契约。 Co-Authored-By: Hugo <shen.lan123@gmail.com>
跨线程数据污染仅在 DDB 后端出现(单一共享 session + 全局 H 缓存); 文件后端本就线程安全,全局锁对其是纯粹的吞吐回归。收窄后文件路径 恢复无锁并行;DDB 路径在会话级 session_lock 之外保留 load 级锁, 作为全局 H MemCache 写模式的双保险。 Co-Authored-By: Hugo <shen.lan123@gmail.com>
- 12 处 print 改为 get_module_logger(bridge 同步进度/警告、operator 建表提示、 ddb_features 因子计算失败的排查上下文——instruments 只记数量与头部样本) - 2 处裸 except: 收敛为 except Exception 并记录 warning(bridge 关闭路径) - 3 处异常重包装补 from e 保留原始异常链(ddb_features 因子计算失败、 loader 表校验/加载失败),异常类型不变 Co-Authored-By: Hugo <shen.lan123@gmail.com>
QlibDDBMySQLInitializer 的同步方法把用户可控参数直接 f-string 拼入 SQL
(唯一真实注入面)。新增 utils.validate_date_str(^\\d{8}$)与
validate_sql_identifier(^[A-Za-z0-9._-]+$,覆盖合法 Wind 代码)并应用于
sync_calendar/sync_feature_daily/sync_index_daily。合法输入行为不变,
病态输入在构造 SQL 前抛 ValueError。
Co-Authored-By: Hugo <shen.lan123@gmail.com>
- 删除注释掉的大块死代码 ddb_compute_features/get_query_date_range - 删除零调用者函数 extract_fields_from_expressions/is_pure_fields_expressions (逻辑早已内联进 normalize_fields_to_ddb,未在 __init__ 导出) - OPERATOR_MAPPING 删除被静默覆盖的前置重复键 Slope:mslr/Resi:mmse (ops.dos 自定义版本才是生效行为,语义不变) - 删除从不加载的 ddb_scripts/featureEngineeringStreaming_bak(16KB 备份文件) - 新增 tests/test_operator_mapping.py:锁定生效映射 + ast 检查禁止重复键 Co-Authored-By: Hugo <shen.lan123@gmail.com>
- tests/ddb_mocks.py:可复用的记录型假会话(run/upload/runFile/existsTable/ loadTable 链式查询 + RPC 计数),供后续所有性能测试共用 - tests/test_fetch_features.py:固化四大分支行为(纯字段×list/dict、 计算×list/dict)、空输入早退、spans 掩码、缺失字段兜底,以及当前 RPC 往返数基线(日历每次全量下载、计算分支 2 次 run 等) - scripts/benchmark_ddb_backend.py:DDB_BENCH_URI 开启的 live 基准 (计时 + RPC 计数),离线 CI 安全跳过 Co-Authored-By: Hugo <shen.lan123@gmail.com>
- TradeDateUtils 增加模块级日历缓存:fetch_features_from_ddb 每批字段 (30 个/批)都会构造本类,此前每次都全量下载交易日历——Alpha158 一次 D.features ≈ 6 次下载,现在预热后为 0 - DBCalendarStorage.index()/__getitem__ 改走 H["c"] 缓存(此前绕过缓存, 数据集对齐期间反复全量下载),新增 _cached_calendar() 与 data 共用缓存键 - 新增 ddb_qlib.invalidate_ddb_caches():清空日历缓存与 H["c"], write_df_to_ddb / clean_qlib_db(含 CSV 导入路径)写入后自动调用 - 新增 tests/test_ddb_calendar_cache.py;更新 RPC 基线测试为缓存后目标值 Co-Authored-By: Hugo <shen.lan123@gmail.com>
每批字段的 RPC 往返从 3-4 次降到 2 次(预热后): - 日期改为字面量内联进查询脚本,消除独立的 start_time/end_time 变量上传往返(复杂值 instruments/expressions 仍走变量上传, 遵循 dolphindb_skill 的变量上传建议) - 纯字段分支改为单条 SQL 脚本:spans dict 的 createDateStockMapping 与 conditionalFilter 主查询合并为一次 run,同时消除 loadTable 句柄往返 - 上传字典按分支裁剪:纯字段分支只传 instruments 结构化拆分(行为不变,测试为证):200 行函数拆为编排器 + 4 个 helper (_resolve_query_window/_fetch_pure_fields/_compute_expressions/ _format_result_panel),后续优化各自独立演进。 Co-Authored-By: Hugo <shen.lan123@gmail.com>
- existsTable 仅正向缓存(DBStorageMixin._exists_cache):每个存储访问器 的 check() 此前都多付一次 RPC;表不存在仍持续报错(负结果不缓存) - DBInstrumentStorage 股票池经 H["i"] 缓存(此前每次访问全量下载 + 行循环重建 dict),返回浅拷贝防调用方污染缓存 - 新增 utils.get_table_columns 表列名进程内缓存,应用于 build_field_expr 与 DDBTableOperator.table_appender/table_upsert(表结构仅随 DDL 变更) - adapt_qlib_expr_syntax_for_ddb 在默认 OPERATOR_MAPPING 下经 lru_cache 记忆化(identity 判断覆盖全部真实调用方,自定义映射直接解析) - invalidate_ddb_caches 统一失效上述缓存;新增 tests/test_ddb_storage_caches.py Co-Authored-By: Hugo <shen.lan123@gmail.com>
旧路径 concat→unstack→stack→swaplevel→sort_index 约 4 次全景拷贝 (5000 股×多年×30 列时数百 MB 客户端内存 churn),是客户端 CPU 主要 开销。_computed_dict_to_panel 按 (instrument, datetime) 预排序两轴后 一次分配直构面板;所有 alias 共享同一 (dates, codes) 轴(服务器端同一 panel() 产出),形状不一致(DDB 端异常占位返回)时运行时回退 _legacy_reshape 保持旧语义。pandas 版本分支随之移出热路径。 新增 tests/test_ddb_reshape_equivalence.py:乱序轴/NaN/单 alias/单日期/ 单代码等形态下直构与 legacy 逐值等价 + 回退路径验证。 Co-Authored-By: Hugo <shen.lan123@gmail.com>
三个 alpha 库脚本(gtja191 56KB / wq101 40KB / qlib158 23KB,合计约 119KB)此前每次 qlib.init 都全量 runFile,让 2 核/8GB 社区版服务器 解析大量通常用不到的脚本。改为: - init 仅加载核心三件套(ops/featureEngineering/prepareInstruments, ops.dos 优先序保持不变) - ensure_alpha_libs_loaded 按字段前缀(gtjaAlpha/qlib158Alpha/WQAlpha, 大小写不敏感)在 fetch 时按需加载,每会话每库仅一次 - 兜底:主查询报 "Cannot recognize the token" 时全量加载后重试一次 (覆盖绕过字段扫描的调用路径),其他错误不重试 - 逃生开关:preload_alpha_libs=True 或配置 C["ddb_preload_alpha_libs"] 恢复历史全量加载行为 Co-Authored-By: Hugo <shen.lan123@gmail.com>
create_table/clean_qlib_db/write_df_to_ddb(含三个 CSV 导入器路径) 此前每次调用都新建 DDBClient——新 TCP 会话 + 登录,批量导入 N 个 DataFrame 就开 N 条会话,白占社区版(2 核/8GB)服务器内存。 - 新增模块级 get_shared_client(uri):按 URI 复用进程内客户端 - 三个入口函数增加 keyword-only 参数 client=None(签名向后兼容), 显式传入优先,缺省走共享客户端 - 共享客户端仅用于现状单线程写入流程(ddb.Session 非线程安全,已注释) Co-Authored-By: Hugo <shen.lan123@gmail.com>
不猜新常数,把两个批次参数暴露为配置、由 live 基准按实际服务器校准: - C["ddb_field_chunk_size"]=30:ddb_dataset_processor 每批字段数 - C["ddb_days_step"]=252:FeatureEngineeringByDate 的 mr 日期分片, .dos 侧为带默认值的尾参(旧调用方行为不变) - C["ddb_preload_alpha_libs"]=False:D5 惰性加载的逃生开关正式注册 配置注释写明 2 核/8GB 的内存模型:单任务 ≈ days_step × 股票数 × 基础字段数 × 8B,8GB 建议 days_step ≤ 504。 Co-Authored-By: Hugo <shen.lan123@gmail.com>
- CHANGELOG.md 记录本轮 17 个提交的完整变更(bug/并发/质量/性能) - ddb_qlib/README.md 补充进程内缓存失效指南(invalidate_ddb_caches) 与社区版 2 核/8GB 的批次参数调优说明 Co-Authored-By: Hugo <shen.lan123@gmail.com>
Code reviewFound 1 issue:
qlib-ddb/qlib/data/dataset/loader.py Lines 216 to 228 in 3a9cac0 相关:原始修复引入的回归测试 https://github.com/hugo2046/qlib-ddb/blob/3a9cac099f06414ac2691b3ab566c76ab02621c6/tests/data_mid_layer_tests/test_dataloader_concurrency.py 与类注释 qlib-ddb/qlib/data/dataset/loader.py Lines 161 to 170 in 3a9cac0 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
- 取前序期:Python 侧复用 qlib 算子树 get_extended_window_size 解析 每批表达式的(向前, 向后)外扩交易日数(嵌套递归、双臂取 max、 Ref($close,-2) 标签向后外扩),经 FeatureEngineeringByDate 尾参 lookbackDays/rightDays 下发;DDB 端外扩查询窗口计算后把结果矩阵 截断回请求区间,消除 Mean($close,20) 等在区间头部的窗口不足 NaN。 qlib 解析不了的表达式退回正则扫窗口 + C["ddb_lookback_default"] (默认 252)兜底。 - 分段计算:移除从未生效的 repartitionDS(RANGE,[start,end])+mr (2 个边界只产生 1 个数据源的"虚假分区")。改为内存估算放得下 整段单次计算;放不下按 ddb_days_step 切段顺序循环,每段带 lookback 重叠(halo)、段内截断,统一列标签对齐后 concatMatrix 纵向合并,分段边界处滚动结果与整段一致。 - 兼容:FeatureEngineeringByDate 新尾参默认 0,旧调用行为不变; D.features() 签名不变。离线单测 +12,DDB 端行为待 live 验证。 Co-Authored-By: Hugo <shen.lan123@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code review针对新增提交
qlib-ddb/qlib/data/backend/ddb_qlib/ddb_scripts/featureEngineering.dos Lines 752 to 761 in ab525e9
qlib-ddb/qlib/data/backend/ddb_qlib/ddb_features.py Lines 238 to 242 in ab525e9 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
代码审查(PR #3)发现的两个问题: - mergeSegmentResults 在某分段计算失败/空时遇占位 TABLE 即 break, 丢弃该表达式其余分段已算出的正确矩阵。改为失败/空分段统一由 buildPlaceholderMatrix 产出 NULL 占位矩阵(行-该段交易日,列- presentCodes),与正常结果同为带标签矩阵;合并时按行拼接、不再 丢弃其它分段,顺带消除空 baseData 分段的日期空洞与 Python 侧 TABLE/MATRIX 类型不一致隐患。移除因此零调用的 createEmptyTable。 - get_expression_extended_window 正则兜底取最大整数为窗口,会把 数值常量(如 $volume/1000000)误判为百万日回看,触发超量查询。 新增 _MAX_FALLBACK_WINDOW=2000 上界剔除常量,无候选则退回 ddb_lookback_default。 离线套件 122 项通过(新增 3 项兜底单测);DDB 端占位矩阵/拼接行为 待 live 验证。 Co-Authored-By: Hugo <shen.lan123@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
审查问题修复(6ee10965)两个问题均已修复: 1. qlib-ddb/qlib/data/backend/ddb_qlib/ddb_scripts/featureEngineering.dos Lines 740 to 780 in 6ee1096 2. 正则兜底误判数值常量 — 新增 qlib-ddb/qlib/data/backend/ddb_qlib/ddb_features.py Lines 204 to 210 in 6ee1096 离线套件 122 项通过(新增 3 项兜底单测)。DDB 端占位矩阵/拼接行为已列入 live 验证清单(含「单分段失败不丢弃其它分段」用例)。 🤖 Generated with Claude Code |
概述
在
optimize/ddb-backend分支上对 DolphinDB 后端做一轮系统性优化,共 19 个原子提交。硬前提:目标服务器为 DolphinDB 社区版(2 核 / 8GB)——优化方向是省往返、省传输、省服务器内存、控会话数,明确不堆并行。D.features()/D.calendar()/D.instruments()等公共接口的签名与返回结果完全兼容。变更内容
Bug 修复(5 项,全部带回归测试)
ddb_dataset_processor传入inst_processors时提前return pd.DataFrame(),并行处理结果被整体丢弃(data.py:782)DDBClient连接池生命周期双重损坏:_pool_instance类变量致多客户端共享同一池;close_pool引用不存在的_pool_lock且永远读到 None(从未真正关闭过池);删除调用不存在get_session()的死方法与__main__硬编码凭据DolphinDBClientProviderinit 急切创建 4 连接池(无消费方)→ 懒创建load_mysql_plugin误装lgbm插件(应为mysql)DolphinDBDataLoader.__exit__/__del__无条件关闭进程级共享 session →_owns_session所有权标志并发/线程安全
DBClient.session_lock(RLock):feature 查询是「run→upload→run」多步会话对话,并发交错会互相覆盖服务器端变量(跨线程数据污染根因);所有 session 触点统一持锁QlibDataLoader全局_load_lock收窄至 DDB 路径,文件后端恢复无锁并行健壮性/质量
print→ loguru;3 处裸except:收敛;异常重包装补from eOPERATOR_MAPPING重复键(生效映射不变,ast 测试锁定)性能(计算分支每批 RPC 3-4 次 → 预热后 2 次)
D.features从 ~6 次全量日历下载 → 0;DBCalendarStorage.index()/__getitem__改走H["c"]fetch_features_from_ddb拆为编排器 + 4 个 helperH["i"]、表 schema 进程内缓存、表达式翻译 lru_cache;统一由invalidate_ddb_caches()失效(写路径自动调用)(instrument, datetime)MultiIndex 面板,替代 concat→unstack→stack→swaplevel 的 ~4 次全景拷贝(保留 legacy 兜底 + 逐值等价测试)ddb_preload_alpha_libs逃生开关DDBClient(批量导入 N 会话 → 1)ddb_field_chunk_size=30、ddb_days_step=252(默认值不变,附 8GB 内存模型说明与 live 基准脚本)明确不做(2 核/8GB 约束)
读路径不引入
DBConnectionPool并发、不调高 mr 并行度、不做服务器端全景 pivot、不引入 module/functionView 持久化。追加(2026-07-22,ab525e91):滚动算子自动取前序期 + 真实分段计算
start_time原样下发,Mean($close,20)在区间头部前 19 个交易日必然 NaN。现在 Python 侧复用 qlib 算子树get_extended_window_size解析每批表达式的(向前, 向后)外扩交易日数(嵌套递归、双臂取 max、Ref($close,-2)标签向后外扩),经FeatureEngineeringByDate尾参下发;DDB 端外扩查询窗口计算后截断回请求区间。qlib 解析不了的表达式退回正则扫窗口 +ddb_lookback_default(默认 252)兜底repartitionDS(RANGE,[start,end])+mr(2 个边界只产生 1 个数据源的"虚假分区")。改为内存估算放得下整段单次计算;放不下按ddb_days_step切段顺序循环,每段带 lookback 重叠(halo)、段内截断、统一列标签对齐后concatMatrix纵向合并——分段边界处滚动结果与整段一致FeatureEngineeringByDate新尾参默认 0,旧调用不变;D.features()签名不变。离线套件 +4 项至 119 项全部通过(新增tests/test_ddb_lookback.py);DDB 端脚本行为列入 live 验证清单(见docs/DDB取前序期与分段计算_20260722.md)测试
tests/ddb_mocks.py可复用记录型假会话与 RPC 计数基线fetch_features_from_ddb四大分支行为,再做重构;直构重塑与 legacy 路径逐值等价测试(乱序轴/NaN/单 alias 等边界)DDB_BENCH_URI=... python scripts/benchmark_ddb_backend.py前后对比 + spans 成分股回测样例比对🤖 Generated with Claude Code