4、实时特征系统架构:典型实时特征计算流程、Flink/Kafka Streams特征管道、在线特征存储(Redis/HBase)
实时特征系统是离线与实时一致性校验的核心执行环境。与离线批处理不同,实时特征系统需要处理无界数据流,在毫秒到秒级延迟内完成特征计算、状态维护与结果输出。本章深入剖析实时特征系统的三大支柱:典型计算流程、流处理管道(Flink / Kafka Streams)以及在线特征存储(Redis / HBase)。
4.1 典型实时特征计算流程
一个通用的实时特征计算流程通常包含以下五个阶段,每个阶段都直接影响特征的一致性与时效性。
| 阶段 | 核心动作 | 一致性关注点 |
|---|---|---|
| 1. 数据接入 | 从 Kafka / Pulsar 等消息队列消费原始事件 | 确保 Exactly-Once 语义,避免重复或丢失事件 |
| 2. 事件解析与清洗 | 反序列化(Avro / Protobuf)、过滤无效字段、时间戳标准化 | 时间戳对齐(Event Time vs Processing Time) |
| 3. 窗口与状态计算 | 滑动窗口聚合、Session 窗口、计数/均值/比率等统计量 | 窗口边界与离线对齐;状态后端选择(RocksDB / Heap) |
| 4. 特征加工与转换 | 特征交叉、分箱、归一化、Lookup 外部维度表 | 特征函数版本与离线一致;维表变更感知 |
| 5. 特征输出与存储 | 写入在线特征存储(Redis / HBase),或直接推送到下游推理服务 | 写入原子性;TTL 管理;特征版本标记 |
4.2 Flink / Kafka Streams 特征管道
Flink 和 Kafka Streams 是当前实时特征管道的主流选择。两者在架构哲学、状态管理和时间语义上存在显著差异,直接影响特征一致性的实现难度。
4.2.1 Flink 特征管道
Flink 提供真正的有状态流处理,支持精确一次语义(Exactly-Once Semantics)和事件时间(Event Time)处理,非常适合需要复杂窗口和状态一致性的特征计算场景。
- 状态后端: 支持 MemoryStateBackend、FsStateBackend、RocksDBStateBackend。生产环境推荐 RocksDB,支持大状态且容错。
- 时间语义: 原生支持 Event Time,通过 Watermark 机制处理乱序事件,与离线批处理的时间窗口对齐更自然。
- 维表 Join: 支持 Async I/O 和 Lookup Join,可实时关联 HBase / Redis 中的维度特征。
// Flink SQL 示例:实时滑动窗口特征计算
CREATE TABLE click_events (
user_id BIGINT,
item_id BIGINT,
event_time TIMESTAMP(3),
WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'click_topic',
'properties.bootstrap.servers' = 'localhost:9092',
'format' = 'avro'
);
CREATE TABLE user_features (
user_id BIGINT,
click_count_1h BIGINT,
window_start TIMESTAMP(3),
window_end TIMESTAMP(3)
) WITH (
'connector' = 'upsert-kafka',
'topic' = 'feature_output',
'key.format' = 'json',
'value.format' = 'json'
);
INSERT INTO user_features
SELECT
user_id,
COUNT(*) AS click_count_1h,
TUMBLE_START(event_time, INTERVAL '1' HOUR) AS window_start,
TUMBLE_END(event_time, INTERVAL '1' HOUR) AS window_end
FROM click_events
GROUP BY
user_id,
TUMBLE(event_time, INTERVAL '1' HOUR);
4.2.2 Kafka Streams 特征管道
Kafka Streams 作为轻量级流处理库,直接嵌入应用内部,无需独立集群。其状态存储基于 RocksDB 或内存,通过 Kafka 主题的变更日志(Changelog)实现容错。
- 状态存储: 使用
KeyValueStore或WindowStore,状态通过 Kafka 的changelog主题备份。 - 时间语义: 主要依赖 Record 中的时间戳(可自定义),对 Event Time 的支持不如 Flink 原生,需要手动处理乱序。
- 部署形态: 作为 Java 应用的一部分运行,适合微服务架构中的轻量特征计算。
// Kafka Streams 示例:滑动窗口计数特征
KStream<String, ClickEvent> clicks = builder.stream("click_topic",
Consumed.with(Serdes.String(), clickEventSerde));
KGroupedStream<Long, ClickEvent> groupedByUser = clicks
.map((key, value) -> KeyValue.pair(value.getUserId(), value))
.groupByKey(Grouped.with(Serdes.Long(), clickEventSerde));
TimeWindowedKStream<Long, ClickEvent> windowed = groupedByUser
.windowedBy(TimeWindows.of(Duration.ofHours(1)).advanceBy(Duration.ofMinutes(5)));
KTable<Windowed<Long>, Long> clickCounts = windowed.count();
clickCounts.toStream().map((windowedKey, count) -> {
Feature feature = new Feature(
windowedKey.key(),
"click_count_1h",
count,
windowedKey.window().start(),
windowedKey.window().end()
);
return KeyValue.pair(feature.getUserId().toString(), feature);
}).to("feature_output", Produced.with(Serdes.String(), featureSerde));
4.3 在线特征存储(Redis / HBase)
在线特征存储是实时特征系统的最后一环,负责以低延迟(通常 < 10ms)为在线推理服务提供特征数据。Redis 和 HBase 是两种最主流的方案,它们在数据模型、一致性保证和扩展性上各有侧重。
4.3.1 Redis 特征存储
Redis 基于内存,提供极低的读写延迟(微秒级),适合存储维度特征、实时聚合结果和短期特征快照。
| 特性 | 说明 | 一致性影响 |
|---|---|---|
| 数据结构 | String、Hash、Sorted Set、Stream | Hash 适合存储多字段特征;Sorted Set 可用于时间窗口排序 |
| 持久化 | RDB 快照 + AOF 日志 | AOF 可配置 everysec,但极端情况下可能丢失 1 秒数据 |
| TTL 管理 | 支持 Key 级别过期 | 必须为特征设置合理的 TTL,避免过期特征影响一致性校验 |
| 原子操作 | INCR、HSETNX、LUA 脚本 | 可用于实现特征更新的原子性,防止并发写覆盖 |
// Redis 特征写入示例(使用 Hash 结构)
// 特征 Key: feature:user:{userId}:click_count_1h
// 特征 Field: window_start, window_end, count, version
HSET feature:user:12345:click_count_1h \
window_start "2025-04-01T10:00:00Z" \
window_end "2025-04-01T11:00:00Z" \
count "42" \
version "v2.1"
EXPIRE feature:user:12345:click_count_1h 7200
4.3.2 HBase 特征存储
HBase 基于列族存储,支持海量数据(PB 级)和强一致性(CP 系统),适合存储历史特征序列、长周期聚合特征以及需要范围扫描的场景。
| 特性 | 说明 | 一致性影响 |
|---|---|---|
| 数据模型 | RowKey + 列族 + 列限定符 + 时间戳 | RowKey 设计直接影响特征查询效率;时间戳可用于特征版本管理 |
| 一致性 | 强一致性(CP),写入后立即可见 | 适合需要严格一致性的特征校验场景 |
| TTL 管理 | 列族级别 TTL,自动清理过期数据 | 需与离线特征表的保留周期对齐 |
| 批量读取 | 支持 Scan 操作,可批量获取多个用户特征 | 可用于离线-实时批量比对任务 |
// HBase 特征表设计示例
// 表名: user_features
// 列族: cf (包含 click_count_1h, purchase_amount_1d, ...)
// RowKey 设计: userId_reverse + featureType + windowEndTimestamp
// 例如: 54321_click_count_1h_1711929600
// 写入特征
Put put = new Put(Bytes.toBytes("54321_click_count_1h_1711929600"));
put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("value"), Bytes.toBytes("42"));
put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("version"), Bytes.toBytes("v2.1"));
table.put(put);
// 批量扫描(用于一致性校验)
Scan scan = new Scan();
scan.setStartRow(Bytes.toBytes("54321_click_count_1h_1711929600"));
scan.setStopRow(Bytes.toBytes("54321_click_count_1h_1711933200"));
ResultScanner scanner = table.getScanner(scan);
4.4 实时特征系统架构总览
下图展示了从数据源到在线特征存储的完整实时特征管道架构,以及各组件在一致性校验中的角色。
4.5 实时特征管道与存储的协同策略
为了实现高效的离线-实时一致性校验,特征管道与在线存储需要协同设计以下策略:
- 特征版本标记: 在写入 Redis / HBase 时,同时写入特征计算所使用的算法版本号(如
feature_version: v2.1),便于离线回溯时匹配正确的计算逻辑。 - 时间戳对齐: 实时特征的时间窗口边界必须与离线特征表的窗口边界严格一致。建议使用 UTC 时间,并统一窗口起始时间的对齐方式(如整点对齐)。
- 双写校验: 在特征管道中增加一个“影子输出”通道,将特征同时写入一个独立的校验存储(如 Kafka 的校验主题),供离线比对任务消费。
- TTL 与生命周期管理: 在线特征的 TTL 应覆盖离线校验的最大延迟窗口。例如,如果离线任务可能延迟 2 小时,则在线特征的 TTL 应至少设置为 3 小时。