特征生产契约:数据源接入规范、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逻辑契约 输入输出定义 转换规则 异常处理 窗口计算约定 窗口类型 时间语义 触发策略 回溯机制 回溯范围 回溯模式 数据一致性 四个环节必须同时定义,缺一不可。任何一个环节的契约缺失,都会导致特征生产的不确定性。 建议将契约文档纳入代码仓库,走 CR 流程,每次修改都要更新版本号。

嗯,这一章的内容就到这儿。四个契约环环相扣,少一个都不行。下次团队讨论特征生产时,你可以把这四个问题拍在桌上:数据源怎么接?ETL 怎么洗?窗口怎么算?回溯怎么补?答案就在契约里。