特征生产契约:数据源接入规范、ETL逻辑契约、窗口计算约定、回溯机制
好,咱们进入特征生产契约的核心环节。说白了,这一章就是要把「数据怎么进来、怎么洗、怎么算、怎么补」这四个环节的规矩定死。
我在多个实时项目里踩过的坑,十有八九都出在这四个环节的边界模糊上。数据源说断就断、ETL逻辑偷偷改、窗口算出来的结果对不上、回溯时把线上数据搞乱……嗯,今天咱们就把这些问题的解药给出来。
数据源接入规范:第一道防线
数据源接入,是特征生产的第一道关卡。这里出问题,后面全是白费功夫。
我习惯把数据源接入契约拆成三个层次:连接契约、格式契约、保活契约。
连接契约
连接契约解决的是「怎么连」的问题。Kafka、Pulsar、MySQL CDC、HTTP 轮询……每种数据源都有自己的一套连接参数。
| 数据源类型 | 连接参数 | 必填/选填 |
|---|---|---|
| Kafka | bootstrap.servers, group.id, auto.offset.reset | 必填 |
| MySQL CDC | host, port, database, table, server-id | 必填 |
| HTTP 轮询 | url, interval, retry, timeout | 必填 |
| Redis Stream | host, port, consumer-group, block-timeout | 必填 |
这里有个坑:连接参数必须写死版本。我曾经遇到过一个项目,Kafka 客户端版本从 2.4 升到 2.8,结果 group.id 的语义变了,消费位移全乱套。所以,连接契约里要明确标注「客户端版本号」。
格式契约
格式契约解决的是「数据长什么样」的问题。我推荐用 Avro 或 Protobuf,带 Schema Registry。为什么?
- 字段名、类型、默认值,全在 Schema 里写死
- 生产者改了 Schema,消费者能自动感知
- 兼容性检查(向后、向前、全量)由 Registry 保证
如果你非要用 JSON,那也得把 Schema 文件放到 Git 里,走 CR 流程。别问我为什么强调这个——我见过有人偷偷在 JSON 里加了个字段,下游解析直接崩了,查了三天才找到原因。
格式契约模板(Avro Schema 示例)
{
"type": "record",
"name": "UserClickEvent",
"namespace": "com.example.realtime.feature",
"fields": [
{"name": "user_id", "type": "string"},
{"name": "item_id", "type": "string"},
{"name": "click_time", "type": "long", "logicalType": "timestamp-millis"},
{"name": "page_type", "type": ["null", "string"], "default": null}
]
}
保活契约
保活契约解决的是「断了怎么办」的问题。数据源不可能永远在线,你得有预案。
- 重试策略:指数退避 + 最大重试次数(比如 5 次)
- 熔断机制:连续失败超过阈值,切到备用数据源或降级
- 健康检查:每 30 秒发一次心跳,超时告警
- 数据补偿:断连期间的数据,通过离线批处理补回来
注意:保活契约里一定要定义「数据源不可用时的默认值」。比如用户画像特征,如果画像服务挂了,是返回 null 还是返回一个兜底值?这个决策会影响下游模型的推理结果。
ETL逻辑契约:洗数据的规矩
ETL 逻辑契约,说白了就是「数据怎么洗」的规矩。我见过太多团队,ETL 逻辑写在代码里,改的时候直接 push,连个评审都没有。结果就是:同一个特征,不同时间算出来的值不一样。
ETL 逻辑契约要包含三个要素:输入输出定义、转换规则、异常处理。
输入输出定义
每个 ETL 步骤,必须明确写出:
- 输入字段列表(字段名、类型、来源)
- 输出字段列表(字段名、类型、含义)
- 字段映射关系(哪个输入字段对应哪个输出字段)
举个例子:
ETL 步骤:raw_click → cleaned_click
输入字段:
- raw_timestamp: long (原始时间戳)
- raw_page_url: string (原始页面URL)
- raw_user_agent: string (用户代理)
输出字段:
- event_time: timestamp (清洗后的时间,已转UTC)
- page_path: string (URL路径部分,去掉了查询参数)
- device_type: string (从User-Agent解析出的设备类型)
转换规则:
1. raw_timestamp → event_time: 统一转为UTC毫秒时间戳
2. raw_page_url → page_path: 提取URL路径,去掉query string
3. raw_user_agent → device_type: 使用UA解析库,映射为mobile/desktop/tablet
转换规则
转换规则必须用 声明式 的方式写,而不是过程式。为什么?声明式规则容易做版本对比、自动化测试、甚至可视化。
我推荐用 SQL 或 规则引擎(比如 Drools、EasyRules)来表达转换逻辑。SQL 的好处是:
- 可读性强,业务人员也能看懂
- 可测试,输入输出一目了然
- 可版本化,放到 Git 里 diff 很方便
我的习惯:每个 ETL 步骤都配一个单元测试。输入一条原始数据,输出一条清洗后的数据,断言字段值对不对。这个测试跑在 CI 里,任何修改都要过测试才能合并。
异常处理
ETL 过程中一定会遇到脏数据。契约里要定义清楚:
- 丢弃:哪些异常数据直接扔掉(比如字段类型不匹配)
- 修复:哪些异常数据可以自动修复(比如时间戳格式不对,尝试多种解析)
- 告警:异常率达到多少阈值时触发告警(比如 1% 的数据解析失败)
- 死信队列:无法处理的数据发到死信队列,人工排查
窗口计算约定:时间维度的契约
窗口计算是实时特征的核心。窗口算不对,特征就是错的。窗口计算约定要解决三个问题:窗口类型、时间语义、触发策略。
窗口类型
常见的窗口类型有四种:
| 窗口类型 | 适用场景 | 注意事项 |
|---|---|---|
| 滚动窗口 | 固定时间段的统计(如每分钟PV) | 窗口边界对齐,无重叠 |
| 滑动窗口 | 实时趋势计算(如最近5分钟滑动平均) | 窗口大小和滑动步长要匹配 |
| 会话窗口 | 用户行为会话(如一次购物流程) | 会话超时时间要合理 |
| 累计窗口 | 全量累计统计(如用户累计消费金额) | 注意状态膨胀问题 |
我个人习惯在契约里写明:窗口类型 + 窗口大小 + 滑动步长(如果是滑动窗口)。比如:
特征名称:user_click_count_5min_slide_1min
窗口类型:滑动窗口
窗口大小:5分钟
滑动步长:1分钟
时间字段:event_time
延迟容忍:30秒
时间语义
时间语义是窗口计算里最容易出问题的地方。我见过一个案例:上游数据延迟了 2 分钟,窗口按处理时间算,结果特征值全错了。
契约里要明确:
- 事件时间:数据产生的时间,由业务系统打戳
- 处理时间:数据到达计算引擎的时间
- 摄入时间:数据进入消息队列的时间
实时特征场景,强烈建议使用事件时间。处理时间只用于监控和告警。
触发策略
窗口什么时候输出结果?Flink 提供了多种触发策略:
- 水位线触发:水位线到达窗口结束时间时触发
- 提前触发:每隔一段时间输出一次中间结果
- 延迟触发:允许迟到数据,延迟一段时间再触发
契约里要写明:水位线延迟时间、是否允许提前触发、迟到数据如何处理。
窗口计算契约示例
特征:user_purchase_amount_1h
窗口类型:滚动窗口,大小1小时
时间语义:事件时间
水位线:允许30秒乱序
触发策略:水位线到达窗口结束时间时触发
迟到数据:窗口关闭后10分钟内到达的数据,触发一次更新
状态清理:窗口关闭后1小时清理状态
回溯机制:补数据的艺术
回溯,是实时特征生产里最容易被忽视、也最容易出问题的环节。为什么?因为回溯要「重算历史数据」,而实时系统通常没有保存历史数据的习惯。
回溯机制契约要包含:回溯范围、回溯模式、数据一致性。
回溯范围
回溯范围定义「要重算哪些数据」:
- 全量回溯:从业务开始到现在,全部重算
- 增量回溯:从某个时间点开始,重算之后的数据
- 定点回溯:只重算某个时间窗口内的数据
我建议在契约里定义好:回溯的起始时间、回溯的结束时间、回溯的粒度(按天、按小时、按分钟)。
回溯模式
回溯模式解决「怎么重算」的问题。有两种主流模式:
| 模式 | 原理 | 适用场景 |
|---|---|---|
| 离线回溯 | 用 Spark/Flink 批处理模式重算历史数据 | 数据量大、对实时性要求不高 |
| 实时回溯 | 重放消息队列中的历史消息,走实时计算链路 | 数据量小、需要验证实时链路 |
我个人更推荐离线回溯。原因很简单:实时回溯会污染线上数据,而且速度慢。离线回溯可以并行计算,效率高得多。
数据一致性
回溯最怕的是:回溯出来的结果和线上实时算出来的结果对不上。所以契约里要定义一致性检查:
- 抽样对比:随机抽取 1000 条数据,对比回溯结果和实时结果
- 总量对比:对比回溯结果的总量和实时结果的总量
- 分布对比:对比回溯结果和实时结果的数据分布(均值、方差、分位数)
我曾经踩过的坑:有一次做全量回溯,没注意回溯期间线上还在写入新数据。结果回溯结果和线上结果差了 10 万条。后来加了「回溯期间禁止写入」的锁,才解决问题。所以,回溯契约里一定要写明「回溯期间的数据隔离策略」。
知识体系总览
下面这张图,把特征生产契约的四个核心环节串起来了。你可以把它当作团队讨论时的「挂图」。
嗯,这一章的内容就到这儿。四个契约环环相扣,少一个都不行。下次团队讨论特征生产时,你可以把这四个问题拍在桌上:数据源怎么接?ETL 怎么洗?窗口怎么算?回溯怎么补?答案就在契约里。