本博客深入探讨了构建视频编码服务 (VES) 的细节,并分享了我们的经验。
宇宙是 Netflix 的下一代媒体计算平台。Cosmos 将微服务架构与异步工作流和无服务器功能相结合,旨在通过提高灵活性、效率和开发人员生产力来实现 Netflix 媒体处理管道的现代化。在过去的几年里,Encoding Technologies(ET)的视频团队一直致力于重建 Cosmos 上的整个视频流程。
这个新管道由多个微服务组成,每个微服务专用于一个功能。视频编码服务 (VES) 就是这样一个微服务。编码是视频管道的重要组成部分。从高层次上讲,它获取摄取的夹层并将其编码为适合 Netflix 流媒体或服务于某些工作室/制作用例的视频流。对于 Netflix 来说,这项服务有许多要求:
鉴于设备种类繁多,从手机到浏览器到智能电视,需要支持多种编解码器格式、分辨率和质量级别。分块编码对于满足我们业务需求的延迟要求是必须的,并且需要适应具有不同级别延迟敏感度的用例。持续发布的能力对于实现流媒体和工作室领域的快速产品创新至关重要。每天有大量的编码工作。服务需要具有成本效益并充分利用可用资源。在这篇技术博客中,我们将介绍如何构建 VES 以实现上述目标,并分享我们从构建微服务中学到的一些经验教训。请注意,为简单起见,我们选择省略某些与本博文主要信息无关的 Netflix 特定细节。
在 Cosmos 上构建视频编码服务Cosmos 微服务由三层组成:接收请求的 API 层 (Optimus)、协调媒体处理流程的工作流层 (Plato) 和处理媒体的无服务器计算层 (Stratum)。这三层通过名为Timestone的自主开发的基于优先级的消息传递系统进行异步通信。我们选择 Protobuf 作为有效负载格式,因为它效率高且具有成熟的跨平台支持。
为了帮助服务开发人员抢占先机,Cosmos 平台提供了强大的服务生成器。该生成器具有直观的用户界面。只需单击几下,它就可以创建一个基本但完整的 Cosmos 服务:创建所有 3 层的代码存储库;启用所有平台功能,包括发现、日志记录、跟踪等;设置发布管道并随时可以访问仪表板。我们可以立即开始添加视频编码逻辑并将服务部署到云中进行实验。
擎天柱作为 API 层,Optimus 充当 VES 的网关,这意味着服务用户只能通过 Optimus 与 VES 交互。定义的 API 接口是 VES 与外部世界之间的强大契约。只要 API 稳定,用户就不会受到 VES 内部变化的影响。这种解耦有助于加快 VES 内部的迭代速度。
作为单一用途服务,VES 的 API 非常简洁。我们定义了一个端点encodeVideo,它接受EncodeRequest并返回EncodeResponse(通过 Timestone 消息以异步方式)。EncodeRequest对象包含有关源视频以及编码配方的信息。编码视频的所有要求(编解码器、分辨率等)以及延迟控制(分块指令)都通过编码配方的数据模型公开。
//protobuf definition message EncodeRequest { VideoSource video_source = 1;//source to be encoded Recipe recipe = 2; //including encoding format, resolution, etc.}message EncodeResponse { OutputVideo output_video = 1; //encoded video Error error = 2; //error message (optional)}message Recipe { Codec codec = 1; //including codec format, profile, level, etc. Resolution resolution = 2; ChunkingDirectives chunking_directives = 3; ...}与任何其他 Cosmos 服务一样,该平台会根据 VES API 数据模型自动生成 RPC 客户端,用户可以使用该客户端构建请求并调用 VES。收到传入请求后,Optimus 会执行验证,并(在适用的情况下)将传入数据转换为内部数据模型,然后再将其传递到下一层 Plato。
柏拉图工作流层 Plato 控制媒体处理步骤。Cosmos 平台支持两种 Plato 编程范式:前向链式规则引擎和有向无环图 (DAG)。VES 具有线性工作流,因此我们选择 DAG 是因为它很简单。
在 DAG 中,工作流由节点和边表示。节点表示工作流中的阶段,而边表示依赖关系 — 只有在所有依赖关系都完成后,阶段才可以执行。VES 需要并行编码视频块以满足其延迟和弹性目标。DAG 通过 MapReduce 模式实现了这种工作流级别的并行性。可以注释节点以指示这种关系,并且只有在所有关联的 Map 节点都准备就绪时才会触发 Reduce 节点。
对于 VES 工作流,我们定义了五个节点及其关联的边,如下图所示:
分割器节点:此节点根据配方中的分块指令将视频分成几块。编码器节点:此节点对视频块进行编码。它是一个 Map 节点。组装节点:此节点将编码的块拼接在一起。它是一个 Reduce 节点。验证器节点:此节点执行编码视频的验证。通知节点:整个工作流程完成后,此节点会通知 API 层。在这个工作流中,诸如 Notifier 之类的节点执行非常轻量的操作,可以直接在 Plato 运行时中执行。但是,资源密集型操作需要委托给计算层 (Stratum) 或其他服务。Plato 调用 Stratum 函数执行编码和组装等任务,其中节点 (Encoder 和 Assembler) 将消息发布到相应的消息队列。Validator 节点调用另一个 Cosmos 服务,即视频验证服务,来验证组装的编码视频。
地层计算层 Stratum 是可以访问媒体样本的地方。Cosmos 服务的开发人员创建 Stratum 函数来处理媒体。他们可以带来自己的媒体处理工具,这些工具被打包成函数的 Docker 镜像。然后,这些 Docker 镜像被发布到我们的内部 Docker 注册表(Titus的一部分)。在生产中,Titus 会根据作业队列的深度自动扩展实例。
VES 需要支持将源视频编码成各种编解码器格式,包括 AVC、AV1 和 VP9 等等。我们对不同的编解码器格式使用不同的编码器二进制文件(简称为“编码器”)。对于 AVC 这种已有 20 年历史的格式,编码器非常稳定。另一方面,Netflix 流媒体的最新成员AV1 正在不断进行积极的改进和试验,因此需要更频繁地升级编码器。为了有效地管理这种多变性,我们决定创建多个 Stratum Functions,每个函数专用于一种特定的编解码器格式,并且可以独立发布。这种方法可确保升级一个编码器不会影响其他编解码器格式的 VES 服务,从而全面保持稳定性和性能。
在 Stratum Function 中,Cosmos 平台为常见的媒体访问模式提供了抽象。无论文件格式如何,源都统一呈现为本地安装的帧。同样,对于需要在云中持久保存的输出,平台将该过程呈现为写入本地文件。所有细节,例如字节流和错误重试,都被抽象出来。由于平台处理了基础设施的复杂性,因此 Stratum Function 中视频编码的基本代码可以简单如下。
ffmpeg -i input/source%08d.j2k -vf ... -c:v libx264 ... output/encoding.264编码是一个资源密集型过程,所需资源与编解码器格式和编码配方密切相关。我们进行了基准测试,以了解不同编码配方的资源使用模式,尤其是 CPU 和 RAM。根据结果,我们利用了 Cosmos 平台的“容器整形”功能。
我们定义了许多不同的“容器形状”,指定了 CPU 和 RAM 等资源的分配。
# an example definition of container shapegroup: containerShapeExample1resources: numCpus: 2 memoryInMB: 4000 networkInMbp: 750 diskSizeInMB: 12000创建路由规则,根据编解码器格式和编码分辨率的组合将编码作业分配到不同的形状。这有助于平台进行“装箱”,从而最大限度地提高资源利用率。
“装箱” 的示例。圆圈表示 CPU 核心,区域表示 RAM。此 16 核 EC2 实例装有 5 个编码容器(矩形),每个容器有 3 种不同的形状(用不同的颜色表示)。
持续发布在我们完成所有三层的开发和测试后,VES 投入生产。然而,这并不意味着我们工作的结束。恰恰相反,我们相信并且仍然相信,服务价值的很大一部分是通过迭代来实现的:支持新的业务需求、提高性能和提高弹性。我们愿景的一个重要部分是让 Cosmos 服务能够以安全的方式持续将代码更改发布到生产中。
由于专注于单一功能,与 VES 中单一功能添加相关的代码更改通常很小且具有凝聚力,因此易于审查。由于调用者只能通过其 API 与 VES 交互,因此内部代码是真正的“实现细节”,可以安全更改。显式 API 合同限制了 VES 的测试面。此外,Cosmos 平台提供了一个基于金字塔的测试框架,以指导开发人员创建不同级别的测试。
经过测试和代码审查后,更改将合并并准备发布。发布管道是完全自动化的:合并后,管道将签出代码、编译、构建、按规定运行单元/集成/端到端测试,如果没有遇到问题,则继续进行全面部署。通常,从代码合并到功能落地大约需要 30 分钟(这个过程在我们上一代平台上需要 2-4 周!)。较短的发布周期为开发人员提供了更快的反馈,并帮助他们在上下文仍然新鲜时进行必要的更新。
我们生产环境中运行的发布管道的屏幕截图
在生产环境中运行时,服务会不断发出指标和日志。平台会收集这些指标和日志,以可视化仪表板并驱动监控/警报系统。指标偏离基线过多将触发警报,并可能导致服务自动回滚(启用“金丝雀”功能时)。
经验教训:VES 是我们团队构建的第一个微服务。我们从微服务的基础知识开始,一路上学到了很多东西。这些学习加深了我们对微服务的理解,并帮助我们改进了设计选择和决策。
定义适当的服务范围微服务架构的一个原则是,服务应该为单一功能而构建。这听起来很简单,但究竟什么才是“单一功能”?“视频编码”听起来不错,但“将视频编码为 AVC 格式”难道不是更具体的单一功能吗?
当我们开始构建 VES 时,我们采取了为每种编解码器格式创建单独编码服务的方法。虽然这种方法具有诸如工作流分离等优势,但很快我们就被开发开销压垮了。想象一下,如果用户要求我们在编码中添加水印功能。我们需要对多个微服务进行更改。更糟糕的是,所有这些服务中的更改都非常相似,本质上我们一次又一次地添加相同的代码(和测试)。这种重复性工作很容易让开发人员精疲力竭。
本博客中介绍的服务是我们对 VES 的第二次迭代(是的,我们已经进行了一次迭代)。在此版本中,我们将不同编解码器格式的编码整合到单个服务中。它们共享相同的 API 和工作流程,而每种编解码器格式都有自己的 Stratum Functions。到目前为止,这似乎取得了良好的平衡:通用 API 和工作流程减少了代码重复,而单独的 Stratum Functions 保证了每种编解码器格式的独立发展。
我们所做的更改并非不可逆转。如果将来某一天,某种特定编解码器格式的编码演变为完全不同的工作流程,我们可以选择将其拆分为自己的微服务。
务实的数据建模一开始,我们对数据模型分离非常严格——我们坚信共享等同于耦合,而耦合可能会导致未来的潜在灾难。为了避免这种情况,对于每个服务以及服务内的三个层,我们定义了自己的数据模型,并构建了转换器来在不同数据模型之间进行转换。
我们最终在整个系统中为位深度和分辨率等方面创建了多个数据模型。公平地说,这确实有一些优点。例如,我们的编码管道支持 AVC 编码(8 位)和 AV1 编码(10 位)的不同位深度。通过定义AVC.BitDepth和AV1.BitDepth,可以将位深度的约束内置到数据模型中。然而,这种差异化能力的好处是否大于缺点(即多个数据模型转换),这一点值得商榷。
最终,我们创建了一个库来托管视频领域常见概念的数据模型。这些概念的示例包括帧速率、扫描类型、颜色空间等。如您所见,它们非常常见且稳定。这个“通用”数据模型库在视频团队拥有的所有服务之间共享,避免了不必要的重复和数据转换。在每个服务中,都会为特定于服务的对象定义额外的数据模型。
拥抱服务 API 的变化这听起来可能有些矛盾。我们一直在说,API 是服务与用户之间的牢固契约,保持 API 稳定可以保护用户免受内部变化的影响。这绝对是真的。然而,当我们设计服务 API 的第一个版本时,我们都没有水晶球。不可避免的是,在某个时候,这个 API 变得不够用。如果我们过于坚信“API 不能改变”,开发人员将被迫寻找解决方法,而这些解决方法几乎肯定不是最优的。
关于如何优雅地发展 API,有很多很棒的技术文章。我们相信我们还有一个独特的优势:VES 是 Netflix Encoding Technologies (ET) 内部的一项服务。我们的两个用户,Streaming Workflow Orchestrator 和 Studio Workflow Orchestrator,由 ET 内部的工作流团队拥有。我们的团队拥有相同的背景,并朝着共同的目标努力。如果我们认为更新 API 符合 Netflix 的最佳利益,我们会与他们会面以寻求一致意见。一旦达成更新 API 的共识,团队就会合作以确保平稳过渡。
作者:Liwei Guo, Vinicius Carvalho, Anush Moorthy, Aditya Mavlankar, Lishan Zhu
出处:https://netflixtechblog.com/the-making-of-ves-the-cosmos-microservice-for-netflix-video-encoding-946b9b3cd300