背景
Netflix 有数百个微服务,每个微服务都有自己的数据模型或实体。例如,我们有一个存储电影实体元数据的服务,或者一个存储图像元数据的服务。这些服务在未来的某个时刻希望对它们的对象或实体进行注解。我们的团队(Asset Management Platform)决定创建一个叫作 Marken 的通用服务,让 Netflix 的任何一个微服务都能注解它们的实体。
注解
人们有时候会把注解理解成标签。在 Marken 中,注解是一种元数据,可以被附加到任意领域对象上。我们的客户端应用程序想要生成许多不同种类的注解。下面是一个简单的注解,描述了某一部电影带有暴力内容。
Movie Entity with id 1234 has violence.(ID 为 1234 的 Movie 实体包含了暴力内容)
更有趣的是,用户有时候希望能够保存时间数据或空间数据。图 1 是一个应用程序示例,剪辑师用它来评审他们的工作。他们希望将手套的颜色改为瑞克黑色,这样他们就能够标记这个区域,在本例中使用蓝色圆圈圈起来,并保存注解。这是评审应用程序的一个典型用例。
同时保存时间和空间数据的另一个例子是机器学习算法,它们可以识别帧中的字符,并保存如下内容:
在特定的帧(时间);
在图像的某个区域(空间);
字符名(注解数据)。
Marken 的目标
我们希望创建一个注解服务,它将实现以下这些目标。
可以对任意实体进行注解。用户应该能够定义注解数据模型。
注解可以进行版本控制。
这个服务应该能够为实时(UI)应用程序提供服务,因此 CRUD 和搜索操作应该是低延迟的。
所有数据都可以在 Hive/Iceberg 中进行离线分析。
Schema
因为 Netflix 的所有团队都可以使用注解服务,所以我们需要支持不同的数据模型。Marken 中的数据模型可以使用 Schema 来描述——就像我们为数据库表创建 Schema 一样。
我们的团队有另外一个服务,它使用基于 JSON 的 DSL 来描述媒体资产 Schema。我们对这个服务进行了扩展,也可以用它来描述注解对象的 Schema。
在上面的例子中,应用程序想要在视频中表示一个跨越了一段时间的矩形区域。
Schema 的名称叫 BOUNDING_BOX。
Schema 有版本化,用户可以在数据模型中添加或删除属性。我们不允许出现不兼容的变更,例如,用户不能修改属性的数据类型。
被保存的数据在“属性”部分,在本例中有两个属性。
boundingBox 属性的类型为“bounding_box”,表示一个矩形区域。
boxTimeRange 属性的类型为“time_range”,我们可以为这个注解指定开始和结束时间。
几何对象
为了在注解中表示空间数据,我们使用了Well Known Text(WKT)格式。我们支持以下这些对象:
Point
Line
MultiLine
BoundingBox
LinearRing
我们的模型是可扩展的,可以根据需要添加更多的几何对象。
时间对象
有几个应用程序需要保存包含时间的视频的注解。应用程序可以将时间保存为帧数或纳秒。
要以帧为单位保存数据,客户端还需要保存每秒的帧数。我们把它叫作 SampleData,包含了以下组件:
sampleNumber,即帧号;
sampleNumerator;
sampleDenominator。
注解对象
与 Schema 一样,注解对象也用 JSON 表示。下面是 BOUNDING_BOX 的注解示例。
第一个组件是这个注解的唯一性 ID。注解是不可变对象,因此注解的标识总会包含版本信息。每当有人更新这个注解时,我们会自动增加它的版本号。
注解必须与某个属于某个微服务的实体相关联。在本例中,这个注解是为 ID 为“1234”的电影创建的。
然后我们指定了注解的 Schema 类型,在本例中是 BOUNDING_BOX。
实际的数据保存在 JSON 的 metadata 部分。就像我们上面讨论的,包含了一个边界框和以纳秒为单位的时间段。
基础 Schema
与面向对象编程一样,我们的服务也支持 Schema 继承,这样客户端就可以在 Schema 之间创建“is-a-type-of”的关系。与 Java 不同的是我们还支持多重继承。
我们有几个机器学习算法,它们扫描 Netflix 的媒体资产(图像和视频),并创建非常有趣的数据,例如识别帧中的字符或识别匹配剪辑。然后,这些数据作为注解保存在我们的服务中。
作为一个平台服务,我们创建了一些基础 Schema,方便为不同的机器学习算法创建新的 Schema。一个基础模式(TEMPORAL_SPATIAL_BASE)具有以下这些可选的属性。这个基础 Schema 可以派生出任意的 Schema,不限于机器学习算法。
Temporal(与时间相关的数据);
Spatial(几何数据)。
另一个 BASE_ALGORITHM_ANNOTATION 包含了以下可选的属性,机器学习算法通常会使用它。
label(字符串类型);
confidenceScore(双精度类型)——表示算法生成数据的置信度;
algorithmVersion(字符串类型)——机器学习算法的版本。
一个典型的机器学习算法通过使用多重继承可以继承 TEMPORAL_SPATIAL_BASE 和 BASE_ALGORITHM_ANNOTATION 两种 Schema。
架构
基于我们的服务目标,在进行架构设计时必须牢记以下这几点。
我们的服务将被许多内部 UI 应用程序使用,因此 CRUD 和搜索操作的延迟必须很低。
除了应用程序,我们还需要保存机器学习算法数据。其中一些数据属于视频帧级别,因此存储的数据量可能很大。我们选择的数据库应该是水平可伸缩的。
我们预计服务的 RPS 会很高。
其他一些目标来自搜索需求。
具备搜索时间和空间数据的能力。
能够使用不同的关联 ID 和附加的关联 ID 进行搜索。
全文搜索注解对象中的不同字段。
支持词干搜索。
随着时间的推移,搜索的需求只会增加,我们将在后续的小节中详细讨论这些需求。
考虑到我们团队的需求和专业知识,我们决定用 Cassandra 来保存注解。为了支持不同的搜索需求,我们选择了 ElasticSearch。除了支持各种功能外,我们还有一些内部辅助服务,如 ZooKeeper 服务、国际化服务等。
Marken 的架构
上图是 Marken 服务的架构图。左边是我们的几个客户端团队创建的数据管道,它们自动将新数据输入到我们的服务中。其中最重要的数据管道是由机器学习团队创建的。
Netflix 的媒体搜索平台现在使用 Marken 来存储注解,并执行稍后将介绍的各种搜索操作。我们的架构使得从媒体算法中加载和摄取数据变得十分容易。这些数据被不同的团队使用,例如宣传媒体团队用它们来改善他们的工作流程。
搜索
注解服务(数据标签)的成功主要依赖了在不了解大量输入算法细节的情况下对标签进行的高效搜索。如上所述,被索引到服务中的每一个新注解类型(取决于算法)使用了基础 Schema。这有助于客户端搜索不同的注解类型。客户端可以通过简单的数据标签或添加更多的过滤器(如电影 ID)进行搜索。
我们定义了一种自定义查询 DSL,支持对注解结果进行搜索、排序和分组。我们使用 Elasticsearch 作为后端搜索引擎支持不同类型的搜索查询。
全文搜索——客户端可能不知道机器学习算法创建了哪些标签。例如,标签可以是“shower curtain(浴帘)”,全文搜索可以让客户端通过使用标签“curtain”搜索找到注解。我们还支持标签的模糊搜索。例如,如果客户端想要搜索“curtain”,但他们错误地输入了“curtian”,服务将返回带有“curtain”标签的注解。
词干搜索——由于 Netflix 支持全球不同语言的内容,我们的客户端需要支持不同语言的词干搜索。Marken 服务包含了 Netflix 所有电影的字幕,包括多种不同的语言。举个词干搜索的例子——“clothing”和“clothes”的词根都为“ cloth”。我们使用 ElasticSearch 支持 34 种不同语言的词干搜索。
时间注解搜索——如果视频注解包含了时间信息(开始和结束时间范围),那么它的相关性就更强了。视频的时间范围映射到了帧号。我们支持对提供的时间范围/帧数内的时间注解进行标签搜索。
空间注解搜索——视频或图像的注解也可以包含空间信息。例如,定义标注对象在注解中的位置的边界框。
时间和空间搜索——视频的注解可以同时包含时间范围和空间坐标。因此,我们支持搜索指定的时间范围和空间坐标范围内的注解。
语义搜索——在理解用户查询的意图后搜索注解。这种类型的搜索会在概念上匹配相似的查询关键字,这与传统的基于标记的搜索不同,传统的基于标记的搜索会精确地匹配关键字。机器学习算法还摄取带有向量的注解(非实际标签)来支持这种类型的搜索。我们使用相同的机器学习模型将用户提供的文本转换为向量,然后使用转换后的“文本到向量”执行搜索,找到与搜索向量最接近的向量。根据客户端的反馈,这样的搜索提供了更为相关的结果,并且在没有找到与用户提供的查询标签完全匹配的注解的情况下不会返回空结果。我们使用 Open Distro for ElasticSearch 来支持语义搜索。我们将在以后的文章中详细介绍语义搜索。
语义搜索
范围交集——我们最近开始支持针对特定标题的多个注解类型交集进行实时查询。客户端可以在视频特定时间范围或整个视频内搜索多个数据标签(由不同的算法产生,因此它们是不同的注解类型)。一个常见例子是找到“James in the indoor shot drinking wine”。对于这样的查询,查询处理器先找到数据标签(James、Indoor shot)和向量搜索(drinking wine)的结果,然后在内存中找到结果的交集。
搜索延迟
我们的客户端应用程序是 UI 应用程序,它们期望低延迟的搜索。如之前所述,我们通过使用 Elasticsearch 支持这样的查询。为了保持低延迟,我们必须确保所有的注解索引是均衡的,并且不会因为回填旧电影数据摄入算法而出现热点。我们采用了滚动索引策略,避免集群中出现这类热点,这些热点会导致 CPU 利用率峰值并降低查询的响应速度。普通搜索的延迟达到了毫秒级。语义搜索的延迟比普通搜索高一些。下图显示了普通搜索和语义搜索(包括KNN和ANN搜索)的平均搜索延迟。
平均搜索延迟
语义搜索延迟
伸缩性
设计注解服务的一个关键挑战是处理 Netflix 不断增长的电影目录和机器学习算法需求。视频内容分析在跨应用程序内容利用中起着至关重要的作用。我们预计机器学习算法类型将在未来几年得到广泛发展。随着注解数量的增长及其在应用程序中的使用,解决可伸缩性问题就变得至关重要。
机器学习数据管道的数据摄取通常是批量进行的,特别是在设计新算法并为整体目录生成注解时。我们已经建立了一个不同的技术栈(实例集群)来控制数据摄取流,为客户端提供一致的搜索延迟体验。在这个技术中,我们使用 Java 线程池控制对后端数据库的写吞吐量。
Cassandra 和 Elasticsearch 后端数据库为数据和查询不断增长的服务提供水平伸缩能力。我们从 12 个节点的 Cassandra 集群开始,扩展到现在的 24 个节点。今年基本上已经为 Netflix 的整体目录添加了注解。一些标题有超过 3M 的注解(其中大部分与字幕有关)。目前,这个服务大约有 19 亿个注解,数据大小为 2.6TB。
分析
我们可以跨多个类型批量搜索注解,为一个标题或多个标题构建数据事实。对于这样的用例,我们将所有注解数据持久化在Iceberg表中,这样就可以使用不同的维度批量查询注解,而不会影响实时应用程序 CRUD 操作的响应速度。
一个常见的用例是媒体算法团队会批量读取不同语言的字幕数据来改进他们创建的机器学习模型。
未来的工作
在这个领域还有很多有趣的工作要做。
我们的数据量随着时间的推移不断增加。我们修改过几次算法,与新版本相关的注解变得更加准确和实用。因此我们需要在不影响服务的情况下清理大量的数据。
我们需要在低延迟交集查询大规模数据并返回结果方面投入更多的时间。
原文链接:
https://netflixtechblog.com/scalable-annotation-service-marken-f5ba9266d428
相关阅读:
应用实践 | Apache Doris 整合 Iceberg + Flink CDC 构建实时湖仓一体的联邦查询分析架构
评论