01问题:一份口径,三处实现
同一件事在链路里会被问很多遍:这个字符串里的容量是多少 GB?内存容量的字段名叫什么?这个价格是有效价还是脏值?
采集侧要解析、Spark 侧要抽取、后台要展示换算。如果每处各写一份规则,那么它们会在某个时刻开始互相矛盾——而且矛盾发生在结果层,你只会看到某个指标不对劲,很难反推是规则分叉了。
02解法:一个能被两侧同时 import 的纯 Python 模块
spark/stages/spec_ontology.py 里只放:
- 键别名优先级元组(内存容量、存储容量、线程数在不同源里的不同字段名);
- 正则常量(容量抽取规则、DDR 代数、主频、套装、中文数字);
- 单位折算表与折算函数;
- 取值的
pick(),以及注册成 UDF 的折算函数。
约定是硬的:只放纯 Python,不许 import pyspark。
这条约定才是关键。因为 Spark 侧要用同一份规则去构造 regexp_extract 的模式串,Django 侧要直接用 re 跑同样的模式。只有当规则是“字符串和常量”而不是“DataFrame 表达式”时,两侧才可能真的共用。**口径的单一来源必须是一个可被 import 的模块,而不是一份文档。**写进文档的口径不会有约束力。
品牌词表是例外:它体量大、需要独立更新,所以放在 JSON 里由专门模块加载(brand_taxonomy.json + 品牌推断)。但它是“数据”不是“规则”——词表变化不改变口径的定义方式。
03一次血案:把单位写成可选
容量规则最初写成“数字 + 单位”,为了兼容某些不带单位的写法,我把两段单位都设成可选。
结果是正则能匹配空串,于是在位置 0 就命中,抽出来的单位恒为空。单位为空意味着折算永不发生,1TB 被当成 1GB 落库。
整条链路没有任何报错:值是个正常的数字,数量级看起来也“还行”,只有跨源对比或价格分布图会显得奇怪。
现在的规则是三段式回退,每一段都必须明确单位:
严格:数字 + 单位(单位必填,末尾用负向断言排除 3200MHz 这类频率串)
宽松:出现重复单位的脏写法(源站的 8GBGB 这类)也能解析
兜底:纯数字结尾时按该字段的默认单位处理,并打上降级标记教训:**正则里“可选”是危险词。**让一个可能匹配空串的模式参与身份或数值抽取,等于允许系统静默造出错值。
04when 链把 executor 撑爆
口径规则在 Spark 里最初的写法是很自然的表达式链:
df.withColumn("cap_gb",
F.when(F.col("k") == "内存总容量", F.col("v"))
.when(F.col("k") == "内存容量", ...)
... # 十几层
)问题不在语义,在序列化。十几层 F.when 让 task binary 膨胀到 MB 级,executor 直接 OOM 被杀。中途试过 F.create_map,更糟——它在每个调用点内联展开,体积更大。
换成 UDF 之后,序列化过去的只是模块路径和函数名,表达式规模与映射表大小无关。代价是每行一次 Python 调用,在这个数据量级完全可以接受。
这条经验现在写进规范里:映射表类逻辑一律 UDF,不允许 when 链堆叠。
05两处口径仍然分叉(还没收口)
诚实记录一下当前没做完的部分:
- 价格下限:采集侧按类目表用一套默认值,计算侧另有一套地板价常量。两边不一致时,一个值在采集侧被判为可疑、在计算侧却被保留。
- 取价优先级:一个模块里有一张优先级表,另一个 stage 里仍写死了判断顺序。只改一处,最终价的归属就和文档不符。
这两处的正确归宿都是 spec_ontology,但重构会同时改变两侧行为,需要一次全链路回归,所以挂着。已知问题公开写出来,比“看起来已经统一”更有价值——论文里也是这么写的。
06顺带一条:DDL 是期望态,线上是实际态
建表脚本用 CREATE TABLE IF NOT EXISTS,对已存在的表不生效。所以修改 DDL 文件并不会改变线上表结构,而文件看起来是对的。
后果是字段宽度不够时线上直接拒收新值,整批失败。治理办法:
- 改表结构必须同时改 DDL 并执行
ALTER; - 在
doris/ddl/migrate/下留按编号可重放的迁移脚本; - 对外(包括论文)描述表结构,一律以线上
SHOW CREATE TABLE为准。
“文件里写了”和“线上生效了”是两件事,中间需要一个显式的、可重放的动作。
07相关文章
- 键的演进:跨源实体对齐:宁可少合并,不可假合并
- 装载的坑:Doris Stream Load 的假成功