1. 在线系统数据从哪里来
在前几篇文章中,我们介绍了在线系统的各种数据。在第 10 篇在线系统总览中,我们看到有 Feature Server、召回源、Prediction Server 模型。后续的召回篇、精排篇、重排篇,我们在用这些数据,从用 User Profile 做召回,用 User、Item 等 Feature 做精排,到用运营数据做重排等。但前面我们没介绍过,这些在线系统用到的数据从哪里来的、怎么来的呢?
之前的离线部分(第 4 篇到第 9 篇),我们也介绍了很多种类的数据生产过程,包括内容特征、User Profile、Feature Pipeline 计算的统计类特征、训练好的模型等等。这些正是在线系统使用的数据的源头。但离线数据怎么写入到在线?
本篇就介绍中间的这个环节,离线数据怎么写入到在线系统。在这里我们把这个环节称作 Feeder,是连接离线和在线数据的 Pipeline,是离线和在线数据的桥梁。
需要注意的是,Feeder 处理的数据有很多类型,因此它不是一个单一 Pipeline,而是一组。倒排索引、向量索引、热门召回源、内容和用户特征、运营数据等等,它们的数据量、更新频率都不一样,不能一股脑塞在一个 Pipeline 中运行。本篇文章就介绍,这些数据怎么分成多个 Pipeline,每个 Pipeline 传输的数据来源、目标和传输方式。前面介绍过模型数据,它是 Prediction Server 专有的数据,由 Prediction Server 直接从 Model Registry 中拉取,不需要经过 Feeder 中转。
2. Feeder 负责的数据分类
我们先把由 Feeder 负责从离线搬运到在线的数据分类列举出来。每组数据的更新节奏和原子切换方式,决定了对应 Pipeline 的运行方式。
用户画像、用户特征:写入 Feature Server,天级全量更新,同时每 5 分钟同步 NRT 特征。Batch 采用版本切换;NRT 按版本或去重规则更新。
内容特征、其他特征:同样写入 Feature Server,采用天级全量和 5 分钟 NRT 更新,生效方式与用户特征相同。
内容 Tag:包括分类、关键词、主题等,写入 ES 倒排索引,用于 Tag 召回。每天增量扫描,逐 Document 更新。
内容 Embedding:写入 Milvus 向量索引,用于向量召回。每天全量重建,新 Collection 就绪后切换 alias。
热门数据:写入 Redis,用于全局热门、分类热门召回。每 5 分钟或 1 小时更新一次,逐 Key 替换,容忍短暂的新旧混合态。
运营数据:写入 Redis,用于过滤、重排中的黑名单与运营规则。通过 Kafka 接收实时变更,启动时和每天拉取全量,逐条更新并通过全量对账纠偏。
下面章节将展开说这些数据的 Feeder Pipeline。在展开之前,先说一下本文的取舍和写作边界:
- 下一节内容以实际做法和演进经历为主,后面会补充一些这些做法成立的条件以及可采用的完善方案。
- 本文描述的实时特征是以我们当时系统所使用的 5 分钟 NRT 特征,没有使用真正的流式特征。真流式特征可以通过 Kafka 发布,由 Feeder 消费来达到实时更新目的。
- 用户画像从独立服务并入 Feature Server。我们最开始用户画像是单独存储,有专门的服务管理,也是由 Feeder 负责更新长期画像和实时画像。后面有了 Feature Server,画像并入其中作为 User Feature 存在,原独立的画像服务逐渐退役。对于 Feeder 来说,原 User Profile Feeder Pipeline 并入了 Feature Feeder Pipeline。这也印证了本篇的一个观点:Feeder Pipeline 不是一直不变,会随着架构的变化拆分或合并。
3. 各类数据 Feeder Pipeline
3.1 Feature Feeder Pipeline
特征的数据来源是离线 Feature Pipeline 计算出来的长期特征(Batch)和实时特征(NRT),详细介绍可以参考第 7 篇。这里已经包含第 2 节提到的、作为 User Feature 并入 Feature Server 的用户画像。
更新方式:每天凌晨更新全量 Batch,替换前一天的版本;同时,每 5 分钟把 NRT Feature 写入到在线 Feature Server。
更新策略:
- Batch Features:双版本、原子切换。Feature Server 内部维持两个版本,一个版本提供服务、另外一个版本接收更新,全量更新并校验通过后,由服务层切换读取版本。Redis 可以用双集群,也可以选用 v1/v2 key 前缀。为避免一次读取混用新旧数据,请求需要固定读取版本,缓存也按版本隔离;跨实例发布不代表所有实例在同一瞬间完成切换。更新时间窗口一般选择凌晨流量低峰期。
- Realtime Features:采用 Redis 的原子写入接口(SET、HSET、INCR 等),控制写入的频率以免影响线上服务。如果 Feature 是完整窗口结果,要防止旧版本覆盖新版本;如果是累加增量,要按事件或者批次标识去重。特别需要注意,INCR 操作是原子的,但它不是幂等,如果重放会重复累加,因此累加写入必须配套去重;如果上游能提供带版本的完整结果,也可以采用覆盖写入。
Feeder 视角的调用方式:我们之前系统在早期时,Feeder 直接操作裸 Redis,Batch 全量更新完之后,通知 Feature Server 进行版本切换;Realtime 直接写 Redis。但这样的设计导致 Feature Feeder Pipeline 和 Feature Server 耦合严重。后续的迭代中,Feature Server 封装 Feature 的更新接口,Feeder 只调用接口而不直接操作 Redis。Feature 批量更新接口采用: BeginBatchUpdate / BatchUpdate / CommitBatchUpdate / RollbackBatchUpdate 的组合(注意:此处这几个接口名字是示意)。实时更新时直接调用 RealtimeUpdate 接口。
全量和 NRT 的衔接:同一个统计特征采用“批量基线 + 实时增量”的模式,需要注意协调覆盖范围。不能简单的直接切换基线,也不能按自然日清空增量。详细的讨论参考后面 5.3 节。
3.2 倒排索引 Tag Feeder Pipeline
内容具有 Tag 类属性,例如分类、关键词、实体、主题等等。这些属性在召回源中属于 Tag 召回,根据 Tag 的倒排索引来查找符合用户兴趣的召回候选。本文以 ES 为例,说明倒排索引 Feeder Pipeline。
更新方式:每天跑一次,按记录的更新时间游标来拉取需要写入的记录,按 item/document 级别调用 ES 的 document update 接口。每天的 Tag Feeder Pipeline 成功运行完成之后,记录该批次扫描开始时间;下一天从 Content Pool 中扫描 lastUpdateTime > 上次扫描时间 的记录集合继续。
演进过程:我们早期的 Content Pool 每天会 Dump 一份当天有更新的文档到 HDFS,每天一个目录。各机房的 Tag Feeder Pipeline 扫描这个集中的 HDFS 目录,更新本机房的倒排索引。后来演进为 Content Pool 提供接口(内部扫描 HBase),传入 lastUpdateTime 和分页等参数来拉取增量。无论哪种方式,上次扫描时间 这个游标都保存在本机房的 Redis 中。
原子性边界:Tag Feeder Pipeline 保证单个 Document 的原子性,不能保证全部索引的原子性。我们这里采用逐文档增量更新,业务允许新内容逐个生效,因此不要求整库切换。
游标异常时的恢复:Content Pool 的接口中,不传 lastUpdateTime 或者传入一个很久之前的时间戳,相当于全量拉取。所以当游标丢失时,可以退化成全量更新,用额外的扫描和写入成本恢复同步。单次扫描失败时不更新游标,下次仍从上一次游标开始,重新处理尚未确认完成的范围。不过,这种恢复方式还依赖正确的扫描边界、分页和幂等写入,不能仅凭游标回退就保证不漏更新,具体见 5.2 节。失败时需要记录异常日志并告警,必要时人工介入调查原因。
3.3 向量索引 Vector Feeder Pipeline
向量召回源保存的是内容的向量(例如 Embedding 向量),本文以 Milvus 为例说明。
更新方式:每天全量重建一次向量索引,不做增量。重建索引时创建新的 Collection,写入向量、完成索引构建,加载确认没问题之后,把服务侧的 alias 指向新 Collection。此时旧的 Collection 保留一段时间,供回滚使用。
我们当时选择全量重建的原因是因为重建的成本可控,完整的索引更方便用于校验、切换和回滚。Milvus 本身支持 insert/upsert 操作,但这种增量的更新需要配套的逻辑来保证边界条件。在这多种原因综合之下,我们选择了全量索引重建,但并不是说增量索引是不可行的。
需要注意的是,上面介绍的流程,是在向量模型没有变化,只每天更新内容向量索引时的逻辑。如果更换向量生成模型,在业务侧,需要保留两个版本的向量。此时需要保证查询侧的用户向量、内容向量索引、预处理逻辑和距离度量属于同一套兼容的发布组合。用户塔和内容塔不一定采用相同版本号,关键是查询向量与内容向量处于兼容空间;alias 切换本身不会协调这些依赖。每一版本内部的向量更新采用上面介绍的流程进行。
对比前面的 Tag Feeder Pipeline,Tag 倒排是增量生效,而向量采用全量重建发布。这种数据形态、更新频率、重建成本和发布要求共同确定了 Pipeline 的运行方式。
3.4 热门索引 Popularity Feeder Pipeline
热门召回的数据由一个离线的 Popularity Pipeline 定期(例如 5min、1h)计算好,保存在离线的存储中。然后由各机房的 Popularity Feeder Pipeline 定期从离线存储,同步到本机房的 Redis 中。更新时,逐个 Key 更新到 Redis 存储中。
热门(包含分类热门等)一共只有几十个 Key,每个 Key 保存一个 item 列表(id + 分数)。我们当时没有做整体的原子性切换,直接逐 Key 替换。这种设计下,在线服务可能会读取到部分新、部分旧的混合形态。但因为只有几十个 Key,当时全部更新一遍的时间很短,业务可以容忍这个窗口内的不一致。具体窗口仍取决于数据量、网络和失败重试,不能仅凭 Key 数量推断。这里逐 Key 替换指单个榜单完整生效;若采用 LIST/ZSET 多步重建,可先构建临时 Key 再替换,避免在线读到半成品。
如果需要保证整体的原子性,可以采用类似 Feature Server 批量更新那种双版本切换的方式。工程实现会稍微复杂,但代价也还可以接受。具体的设计选型,可以和业务商量之后再确定。
3.5 运营数据 CMS Feeder Pipeline
运营的数据,包括有黑名单、运营手工调整(置顶、强插)的内容等等。这些数据源是来自于 CMS 系统,维护在其内部存储中。
CMS Feeder Pipeline 更新这一类的数据时,采用的是“广播 + 全量”的组合:
Feeder 启动、或每天凌晨,调用 CMS 的全量数据接口,拉取全量数据。启动时的拉取是为了建立基线;每天的全量同步是为了对账,负责纠偏。
CMS Feeder Pipeline 运行期间,接收 CMS 的 Kafka 广播。如果 CMS 有变更,更新的数据会实时同步过来。
CMS 的数据都会带修改的时间戳,Kafka 广播中也带更新时间戳,CMS Feeder Pipeline 写数据时根据时间戳来判断,防止旧数据覆盖已同步的新数据。这套判断需要同时用于全量和广播两条路径:既防止旧广播覆盖新值,也防止新广播先到、随后被全量拉取的旧值覆盖。
如果两条路径并发写入,版本比较和写入需要在目标端原子执行,不能先读取时间戳,再无条件覆盖。时间戳还应具有明确的精度和排序规则;若同一实体的不同变更可能拥有相同时间戳,需要增加单调版本号等判序依据。这些是使用版本判断时需要补齐的条件。
还有一个容易遗漏的问题是删除:撤销置顶、移出名单、删除运营项,都需要传播到在线。仅覆盖全量接口返回的现存条目,不能清理 Redis 中的残留。可采用带版本的删除事件或删除标记,或基于完整快照做集合对账;对账时同样要保护快照之后的新变更,不能无条件删除。
为什么此 Feeder Pipeline 会采用“广播 + 全量”的方式?这是因为 CMS 中的一些操作,例如下线某些内容、或者封禁某个作者,这样的操作希望尽快生效,因此通过 Kafka 广播缩短传播延迟,并根据业务要求约定生效时限。每天的全量拉取负责对账纠偏,不能替代广播消费失败后的重试和告警。
4. Feeder 的组织运行方式
前面介绍了多组 Feeder Pipeline 的运行逻辑,本节讨论一下这么多组 Pipeline,怎么实际跑起来的?核心的几个设计理念包括:按执行模型分类、把周期交给调度平台、用配置来抽象共性。
4.1 最开始的方式
我们在初期,把 Pipeline 分成了两类:定期执行的 Pipeline 和持续运行的 Pipeline。Feature、Tag、Vector、Popularity 这几组 Feeder Pipeline 都属于定期执行的,而只有 CMS Feeder Pipeline 需要持续运行(接收 Kafka 广播)。
对于定期执行 Pipeline,利用系统的 crontab 来执行。部署好程序之后,在 crontab 中配置定期执行的入口,然后等系统来定期调起执行。对于 CMS Feeder Pipeline,部署完成之后,直接启动,后台一直运行。持续运行的 CMS Feeder Pipeline 监听 Kafka 广播,然后每天凌晨调用一次 CMS 全量数据接口。
这种方式简单可用,在初期是一个可行的方案。但随着规模的增大,每个 Pipeline 内部的逻辑在变化;以及配套的监控、告警等辅助程序需求也升级了。之前的组织方式带来一些问题:状态和运行进度无法观测、治理(监控和告警等)不能统一处理等。所以后续的迭代引出来调度平台的需求。
4.2 调度平台
创建一个 Feeder Pipeline 调度平台,把所有 Pipeline 的启动交给这个调度平台。定期执行的 Pipeline 由平台统一调度执行,监控每次启动状态和结束状态。持续运行的 Pipeline 也由平台调起,在后台持续执行。
所有的 Feeder Pipeline 都把运行状态节点暴露给平台,由平台统一提供观测接口。例如,全量 Pipeline 把一次任务拆解成“拉取数据 -> 加载候选版本 -> 校验/检查 -> 发布”,增量任务可以把状态设置为“拉取数据 -> 校验/检查 -> 写入数据 -> 确认写入结果 -> 推进游标”。CMS Feeder Pipeline 把一些关键事件,例如:收到 Kafka 广播、拉取全量数据等暴露给平台。
不过需要注意的是,每个 Pipeline 内部的业务逻辑,例如 Tag Feeder Pipeline 的游标,还是需要内部维护,只是把对应的一些关键节点暴露给平台供统一管理。
在这种设计下,定时、重试、错峰、告警、历史、状态监测等等都可以由平台统一管理。一些典型的问题,例如定时任务的重叠问题(上一批次还没结束下一批次就要开始)可以被检测出来;某个 Pipeline 长时间被卡住也能被检测出来。
4.3 Pipeline 配置
从数据传输这一层看,每个 Feeder Pipeline 都有 source 和 target。基于这两个端点,每个 Pipeline 都可以在调度平台中用声明式配置来定义,例如:
pipeline: tag-inverted
type: batch
source: content-pool-api # 或 kafka topic / hive 表 / cms-api
source_tag_version: v1.1 # 源数据 tag 版本,此处可放其他 source 配置
mode: incremental # incremental / full / snapshot
cursor: redis:local:tag_last_scan # 游标位置。也可以设置其他业务配置项
target: es-doc-update
other_params: xxx # 其他业务配置项目,例如 ES field set 等
schedule: 0 2 * * * # 周期由调度平台执行,不在代码里
validate: [row_count, null_rate]
如果把 source、target 等标准化,甚至可以只通过配置的增加来新增一组 Pipeline,从而一定程度上“用一组配置代替一个程序”。
4.4 分层实现和可选工具
我们当时是自己开发了一个调度平台来组织各类 Feeder Pipeline。整个平台按功能分成四层:
配置层:承载 4.3 节那种声明式 YAML,一个 Pipeline 的 source、target、周期、游标、校验规则都在这一层定义。已有适配器和发布流程覆盖的 Pipeline,主要通过增加配置接入。
调度编排层:把配置解析成可执行任务,并统一管理任务的定时、重试、错峰、告警和状态上报。具体执行层再分成两类:批量任务(Batch,例如 Tag Feeder Pipeline,跑完即结束)和常驻任务(Resident,例如 CMS Feeder Pipeline,启动后一直运行并监听 Kafka 广播)。
同步框架:与具体 source/target 无关的搬运执行体,负责拉取、分批、写入目标、失败重试、游标推进这类通用动作,不内置具体业务字段,通过适配器和发布钩子承接差异。
source/target 适配器:每个适配器封装一种具体数据源或目标的操作接口,例如 Content Pool API、ES document update、Redis、Milvus Collection、CMS 全量接口和 Kafka 消费。同步框架面向适配器编程;新增数据源时实现对应适配器。如果涉及新的发布语义,例如 Collection 构建、加载与切换,还需扩展相应的发布流程。
其中同步框架和适配器是 Feeder 平台开发工作量最大的两层,也是 Feeder 的核心所在,值得我们投入更多精力的两部分。
结合现在开源技术再回头看当时的 Feeder 平台,有一些工具可能能适用。例如 Apache DolphinScheduler 或 Apache Airflow 可以用在批量任务的编排上,它们提供定时调度、DAG、重试、告警、历史和状态观测等。但要注意这两者接入方式的不一样,一个偏向低代码、一个用 Python 定义工作流。Apache SeaTunnel 是一个分布式数据集成平台,它提供多种连接器(Connector)可以读写多种数据源,可以提供批量和实时流数据同步功能。不过我们做推荐系统时候,这些同步和编排框架大多还没有开源或尚不成熟,所以我们选择了自研。
Feeder 的核心不在于"搬运"这个动作本身,而在于发布语义。例如天级全量怎么双版本原子切换、NRT 增量怎么和 Batch 基线衔接、游标怎么推进和恢复等等。这些发布语义在采用开源工具时仍需要按目标服务的能力实现或集成。
5. 核心工程问题
5.1 从传输完成到在线可用:一条版本发布链路
前面介绍了数据是怎么从离线传输到在线,但数据写入到在线系统并不代表直接可用。一条数据从离线就绪到在线服务可用,是一条完整的版本发布链路。以向量索引为例,它的全流程是:上游内容就绪 -> 拉取与转换 -> 写入新 Collection -> 完成索引构建 -> 数据校验 -> 加载与预热 -> 就绪探测 -> alias 切换 -> 在线查询验证。
在最开始时,需要注意上游内容就绪状态,Feeder 不能把半成品的内容读取出来写入到在线系统。这个就绪状态一般由数据源头(例如 Content Pool)维护,保证 Feeder 能扫描到数据都是完整的。
数据写入到在线系统时,需要注意几个状态:写入成功、服务加载完成、在线请求可见。这几个状态是不一样的,例如 ES 的 document update 在默认不等待 refresh 的情况下返回成功后,更新还需要经过 refresh 才能被搜索查询读到。加载和搜索可见性由目标服务实现;Feeder 按发布契约等待或检查相应状态,必要时由独立探测程序确认最终生效。
5.2 增量游标与断点续传
Tag Feeder Pipeline 使用游标来记录上次扫描的位置,每个机房保存其独立的游标状态。这样万一更新失败,可以通过上一次游标继续断点续传。
对于游标的更新,需要明确其边界。我们当时以 Pipeline 启动扫描的时间点作为游标点。例如某个任务在 10/20 02:00 开始扫描,内容 A 在 02:03 修改、02:05 被扫描并写入,任务最终在 02:08 结束。任务成功后保存 10/20 02:00 作为游标,下一天(10/21)仍从这个时间点继续扫描。因此,A 在 02:03 的变更虽然已经写入,下一天还会被重复处理。
这种重叠扫描可以覆盖扫描期间的新变更,但不能仅凭扫描开始时间保证绝不遗漏。还需要处理同时间戳边界、分页和延迟可见的数据:例如 lastUpdateTime > 游标 会排除恰好等于游标的记录,可采用包含边界的重扫并配合幂等写入;分页需要稳定的排序和续页规则,扫描期间记录变化造成的遗漏还需源端快照或变更记录等机制解决。对于时间戳早于游标、却在扫描后才可见的记录,则需要上游可见水位或有明确迟到上限的回扫方案。
还需要注意的是,只有本次任务成功之后,再把游标写入 Redis 保存。如果任务出错,后面有些内容可能没有成功被扫描到,不能用新游标覆盖昨天(10/19)的旧游标。这个原则同样适用于 NRT 小批次更新和 CMS Feeder 的 Kafka 消费,都是要确认执行成功之后,再更新 offset。此处每一条内容的成功是指调用在线服务(ES)的 update 接口或写 Redis 操作成功。这不要求每条写入都等待搜索可见:写入成功但尚未 refresh 时,ES 搜索可能暂时看不到更新,不能直接视为 refresh 失败。目标写入确认后可以推进同步游标,最终搜索可见性则由发布检查或独立探测确认。
在这种增量更新 Pipeline 中,因为可能会有数据重叠,所以要保证每条内容的更新都是幂等。并发重试时,还需要注意防止旧内容覆盖新内容。
5.3 Feature 全量与 NRT:覆盖边界一起切换
长期兴趣和短期兴趣分开存储,可以分别更新。但同一个特征的批量基线和增量,需要一套覆盖范围和合并的规则。NRT 的完整窗口结果不能直接当成累加增量。
举个例子,假设某特征的批量基线包含截至 00:00 的事件,02:00 才加载完。这个加载的期间 NRT 在继续处理 00:00 - 02:00 之间的事件。发布新基线的时候,不能把这段时间(00:00 - 02:00)的增量清掉,也不能把基线已经包含的事件再加一次。
所以,此时如果要正确处理这个逻辑,发布的版本至少要包含三种描述:基线覆盖范围、增量范围、迟到补偿规则。候选基线和匹配的增量都准备好之后,作为一个逻辑上的版本一起切换。迟到的事件是不是已经进入基线,需要按生产契约来判断。
回滚时也同样要恢复旧基线和它匹配的增量范围、特征配置和模型契约。具体的边界和补偿可以参考第 7 篇的 3.4,本文主要讲 Feeder 传哪些元数据,什么时候可以提交。
5.4 原子性与数据校验:按业务约束设计
前面介绍了几类 Feeder Pipeline,此处总结一下各 Pipeline 的更新方式和一致性边界:
倒排(ES):单 Document 更新是原子的,整体允许渐进生效;搜索可见性单独确认。
向量(Milvus):构建完整版本,通过 alias 切换查询目标;查询向量与目标索引须保持兼容。
Popularity(Redis):逐 Key 完整替换,容忍短暂的跨 Key 混合态。
Batch Feature:通过服务层切换逻辑版本,一次读取固定版本,缓存按版本隔离。
Realtime Feature:按实体或窗口更新,防止旧写覆盖和重放重复;有关联字段时协调更新。
运营数据:单条异步变更加全量对账,满足约定的传播延迟;关联规则按需成组发布。
从上面可以看出,有一些数据的原子性并不是要求所有数据在同一时间瞬时刷新,而是根据数据的兼容性、业务容忍度来定义。
在双版本切换的数据集上,发布前一般需要做校验。可能的校验包括:条数、关键字段空值率、schema/向量维度、版本兼容、特征分布变化和抽样比对等等。如果校验失败则不切换,输出错误日志告警、人工介入排查。对于增量类数据和实时数据,可以逐条判断有效性;热门榜单即使逐 Key 发布,也可以先校验整批结果。无效记录不能只输出日志后静默跳过,应进入可追踪、可重放的失败队列。是否阻断游标推进,由数据重要性和恢复策略决定;对于下架等时效敏感的变更,需要及时告警并持续追踪处理结果。
5.5 延迟可观测:成功不等于新鲜
这里先区分几个时间口径:
- 同步耗时:任务开始到目标写入完成,反映搬运本身的耗时。
- Feeder 发布延迟:上游产物就绪到在线确认可用,包含等待调度、同步、校验、加载和切换等时间。持续消费的变更,以源端变更就绪时间作为起点。
- 端到端生效延迟:源事件发生或源数据修改,到在线确认可用,还包含上游生产过程。
- 数据新鲜度:当前时间距离在线数据覆盖时间的差值。任务刚刚成功,也可能发布的是一份很旧的数据。
在各 Feeder Pipeline 中,应记录上游就绪、任务开始、目标写入完成、加载就绪和在线确认生效等时间点。总延迟直接用对应起止时间之差计算;各阶段耗时用于定位瓶颈,不能简单相加,因为阶段可能并行,也可能包含未计入的等待时间。对于发布延迟、数据新鲜度以及长时间无进展,都需要设置符合数据更新周期的监控和告警。
最终生效情况还需要从在线读取路径验证。例如,倒排索引可以创建一个特殊的 document,定期修改其内容,再从 ES 搜索侧检测新值何时可见。这个探针能说明它经过的链路是否在工作,不能证明所有文档都同步完成,还需要结合扫描水位、失败记录和抽样对账。
对于向量索引,可以通过在线查询确认实际服务的 Collection 版本,并关联该版本的数据覆盖时间;Feature Server 则返回实际读取的 Batch 版本和 NRT 已生效窗口或覆盖水位。单看向量创建时间或特征写入时间不够:旧数据重新写入一次,时间戳会变新,数据本身却没有变新。有时效要求的数据,应把这些生效检查接入监控和告警。
分享一个我们之前真实的案例:某一天 Tag 召回源表现异常,当天的新变更没有应用到线上。然后排查问题,发现倒排索引 Feeder Pipeline 进程卡住了,在后台运行但数据却没有前进。查看日志发现,不知道是因为网络原因还是程序自身 Bug,在查询 Content Pool 获取具体内容更新时,超时返回。每条内容超时、重试,然后后续内容又超时继续重试,导致进程卡了好几个小时还没执行成功。那个时候我们没有对 Feeder Pipeline 的执行时间进行监控告警,导致没有及时发现。
这个案例的教训是,重试也需要超时预算、退避和进度监控。持续重试不代表任务在前进;依赖持续异常时,应在耗尽预算后明确失败、保留恢复位置并告警,而不是让整条管道长时间停在重试循环里。
6. 总结
Feeder 是离线数据工厂(第 4 - 9 篇)和在线推荐引擎(第 10 - 15 篇)之间的桥梁,它不仅仅做数据的搬运,而是把离线数据生产完成推进到在线服务可用。
本文介绍了不同数据不同的发布方式:倒排逐文档更新,向量索引按完整版本发布,热门逐 Key 替换,Feature 协调批量基线与 NRT 增量,运营数据结合实时广播和全量对账。这些数据传输 Pipeline 设计选择都是由其数据特点和业务特性共同决定的。
到本篇为止,推荐系统的离线生产、在线服务,以及连接两者的数据通道就串起来了。下一篇第 18 篇将为架构系列收官之作。下一篇我们一起回看这些模块如何随着业务规模、时效要求和团队协作的变化逐步演进。架构系列结束之后,我会在工程实践系列继续写几篇,目前计划有:Reader,介绍如何看懂和排查一次推荐请求;配置后台,介绍一次配置或运营修改如何可靠地在线生效。
如果大家还对其他话题感兴趣,可以评论区留言或者私信告诉我。对于一些共性话题,后面可以继续安排介绍。