Netflix 实用 API 设计 (下)

简介: Netflix 实用 API 设计 (下)

gRPC 如今被很多公司应用在大规模生产环境中,很多时候我们并不需要通过 RPC 请求所有数据,而只关心响应数据中的部分字段,Protobuf FieldMask 就可以帮助我们实现这一目的。本文介绍了 Netflix 基于 FieldMask 设计更高效健壮的 API 的实践,全文分两个部分,这是第二部分。原文:Practical API Design at Netflix, Part 2: Protobuf FieldMask for Mutation Operations[1]


背景


上一篇文章中,我们讨论了在设计 API 时如何利用 FieldMask[2]作为解决方案,以便消费者可以只请求他们需要的数据。在这篇文章中,我们将继续介绍 Netflix Studio Engineering 如何基于 FieldMask 进行更新和删除等变更操作。


示例:Netflix 工作室内容制作


image.png

《纸钞屋》(La casa de papel) / Netflix


上一篇文章我们概述了什么是 Production,以及 Production 服务如何对其他微服务(如 Schedule 服务和 Script 服务)进行 gRPC 调用,以检索特定产品(如《纸钞屋》)的日程和脚本(即剧本)。我们将继续利用这个示例并展示如何在生产中改变特定字段。


改变制作细节


假设我们为剧集添加了一些动画元素,因此想将format字段从LIVE_ACTION更新为HYBRID。解决这个问题的简单方法是添加一个 updateProductionFormatRequest 方法以及对应的 gRPC endpoint 来更新 productionFormat:


message UpdateProductionFormatRequest {
  string id = 1;
  ProductionFormat format = 2;
}
service ProductionService {
  rpc UpdateProductionFormat (UpdateProductionFormatRequest) 
      returns (UpdateProductionFormatResponse);
}


这允许我们更新特定产品的生产格式,但如果我们想要更新其他字段,如title,甚至多个字段,如productionFormat, schedule,等等,该怎么办?在此基础上,我们可以为每个字段执行一个更新方法:一个用于生产格式,另一个用于标题,等等:


// separate RPC for every field, not recommended
service ProductionService {
  rpc UpdateProductionFormat (UpdateProductionFormatRequest) {...}
  rpc UpdateProductionTitle (UpdateProductionTitleRequest) {...}
  rpc UpdateProductionSchedule (UpdateProductionScheduleRequest) {...}
  rpc UpdateProductionScripts (UpdateProductionScriptsRequest) {...}
}
message UpdateProductionFormatRequest {...}
message UpdateProductionTitleRequest {...}
message UpdateProductionScheduleRequest {...}
message UpdateProductionScriptsRequest {...}


由于 Production 上的字段数量太多,这将变得越来越难以维护。如果我们想要在单个 RPC 中以原子方式更新多个字段,该怎么办?为不同的字段组合创建额外的方法将导致变更 API 激增,因此这个解决方案是不可扩展的。


与其尝试创建所有可能的单一组合,另一种解决方案可能是定义一个UpdateProduction endpoint,用来处理所有字段:


message Production {
  string id = 1;
  string title = 2;
  ProductionFormat format = 3;
  repeated ProductionScript scripts = 4;
  ProductionSchedule schedule = 5;
  // ... more fields
}
service ProductionService {
  rpc UpdateProduction (UpdateProductionRequest) returns (UpdateProductionResponse);
}
message UpdateProductionRequest {
  Production production = 1;
}


这个解决方案有两个问题。首先,消费者必须知道并提供 Production 中每个必需的字段,即使他们只想更新一个字段,比如 format。其次,由于 Production 有许多字段,所以请求的有效负载可能会变得非常大,尤其是在包含了 schedule 或 script 信息的时候。


如果我们只发送真正想要更新的字段,而不设置所有字段,会怎么样?在示例中,我们只设置 production format 字段(以及引用 production 的 ID):


UpdateProduction updateProduction = UpdateProduction.newBuilder()
    .setProductionFormat(PRODUCTION_FORMAT_HYBRID)
    .build();
// Send the update request
UpdateProductionResponse response = client.updateProduction(LA_CASA_DE_PAPEL_PRODUCTION_ID, 
    updateProductionRequest);


如果我们永远不需要删除字段(或者把字段置为空),那这就可以工作了。但是,如果我们想要删除title字段的值,该怎么办?同样,我们也可以引入一次性方案,如RemoveProductionTitle,但正如上面讨论过的,这种解决方案伸缩性不好。如果我们想从日程计划中删除嵌套字段(如计划启动日期字段)的值,又该怎么办?我们最终会为每个可置空的子字段添加删除 RPC。


利用 FieldMask 进行变更操作


除了定义大量的 RPC,以及承受巨大的消息载荷,我们还可以利用 FieldMask 来实现所有的变更。FieldMask 可以列出我们想明确更新的所有字段。首先,更新 proto 文件,加入UpdateProductionRequest,包含我们想在 Production 中更新的数据,以及应该被更新的 FieldMask。


message ProductionUpdateOperation {
  string production_id = 1;
  string title = 2;
  ProductionFormat format = 3;
  ProductionSchedule schedule = 4;
  repeated ProductionScript scripts = 5;
  ... // more fields
}
message UpdateProductionRequest {
  // contains production ID and fields to be updated
  ProductionUpdateOperation update = 1;
  google.protobuf.FieldMask update_mask = 2;
}


现在,我们可以利用 FieldMask 进行变更,通过使用 FieldMaskUtil.fromStringList()[3]方法为format字段创建一个 FieldMask 来更新 format,该方法为特定类型的字段路径列表构造一个 FieldMask。在本例中,我们设置了一个类型,稍后将在这个示例的基础上进行构建:


FieldMask updateFieldMask = FieldMaskUtil.fromStringList(Production.class, 
    Collections.singletonList(“format”);
// Update the production format type
ProductionUpdateOperation productionUpdateOperation = ProductionUpdateOperation
    .newBuilder()
    .setProductionId(LA_CASA_DE_PAPEL_PRODUCTION_ID)
    .setProductionFormat(PRODUCTION_FORMAT_HYBRID)
    .build();
// Build the UpdateProductionRequest including the updatefieldmask
UpdateProductionRequest updateProductionRequest = UpdateProductionRequest
    .newBuilder()
    .setUpdate(productionUpdateOperation)
    .setUpdateMask(updateFieldMask)
    .build();
// Send the update request
UpdateProductionResponse response = 
    client.updateProduction(LA_CASA_DE_PAPEL_PRODUCTION_ID, updateProductionRequest);


由于我们在 FieldMask 中只指定了format字段,因此即使我们在ProductionUpdateOperation中提供了更多的数据,也只有format会被更新。通过修改路径,可以容易的在 FieldMask 中添加或删除更多字段。在有效负载中提供但没有添加到 FieldMask 路径中的数据将不会被更新,并在操作中被忽略。但是,如果我们省略了一个值,它将在该字段上执行 remove 操作。我们修改上面的例子来展示,更新 format,但删除计划的启动日期,这是ProductionSchedule上的一个嵌套字段“schedule.planned_launch_date”:


FieldMask updateFieldMask = FieldMaskUtil.fromStringList(Production.class,
    Arrays.asList("format", "schedule.planned_launch_date"));
// Update the format, in addition remove schedule.planned_launch_date by not including it in our request
ProductionUpdateOperation productionUpdateOperation = ProductionUpdateOperation
    .newBuilder()
    .setProductionId(LA_CASA_DE_PAPEL_PRODUCTION_ID)
    .setProductionFormat(PRODUCTION_FORMAT_HYBRID)   
    .build();
UpdateProductionRequest updateProductionRequest = UpdateProductionRequest
    .newBuilder()
    .setUpdate(productionUpdateOperation)
    .setUpdateMask(updateFieldMask)
    .build();
// Send the update request
UpdateProductionResponse response = 
    client.updateProduction(LA_CASA_DE_PAPEL_PRODUCTION_ID, updateProductionRequest);


在这个例子中,我们添加了“format”和“schedule.planned_launch_date”,执行了一次更新和一次删除操作。如果我们在有效负载中提供了字段值,对应的字段将被更新为新的值。但是当构建有效负载时,我们只提供了format,而省略了schedule.planned_launch_date,像这样在 FieldMask 中有定义,但是在有效负载中没有,将作为一个 remove 操作:


image.png


空的/缺失的字段掩码


当字段掩码未设置或没有路径时,更新操作将应用于所有有效负载字段。这意味着调用者必须发送整个有效负载,否则,如上所述,任何未设置的字段都将被删除。


这个约定会影响到 schema 的变更:当一个新字段被添加到消息中时,所有的消费者都必须在更新操作上发送它的值,否则它将被删除。


假设我们想添加一个新字段:生产预算。我们将同时扩展Production消息和ProductionUpdateOperation


// update operation with new ‘budget’ field
message ProductionUpdateOperation {
  string production_id = 1;
  string title = 2;
  ProductionFormat format = 3;
  ProductionSchedule schedule = 4;
  repeated ProductionScript scripts = 5;
  ProductionBudget budget = 6;            // new field
}


如果消费者不知道这个新字段或者还没有更新客户端,它可能会由于没有在更新请求中发送 FieldMask 字段而意外的把预算字段置空。


为了避免这种问题,生产者应该考虑为所有更新操作请求设置字段掩码。另一种选择是实现版本控制协议:强制所有调用者发送他们的版本号,并实现自定义逻辑以跳过旧版本中不存在的字段。


最后


在这个系列文章中,介绍了我们如何在 Netflix 使用 FieldMask,以及如何设计一个实用的、可扩展的 API 解决方案。


API 设计者应该以简单为目标,但要考虑 API 的扩展和演进。保持 API 的简单性和可预测性通常并不容易。通过使用 FieldMask,可以帮助我们实现简单和灵活的 API。



References:

[1] https://netflixtechblog.com/practical-api-design-at-netflix-part-1-using-protobuf-fieldmask-35cfdc606518

[2] https://grpc.io/

[3] https://jsonapi.org/format/#fetching-sparse-fieldsets

[4] https://netflixtechblog.com/netflix-studio-engineering-overview-ed60afcfa0ce

[5] https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/field-mask

[6] https://developers.google.com/protocol-buffers

[7] https://en.wikipedia.org/wiki/Filmmaking

[8] https://en.wikipedia.org/wiki/Variable-length_quantity#Zigzag_encoding

[9] https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/util/FieldMaskUtil.html#fromFieldNumbers-java.lang.Class-int...-

[10] https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/util/FieldMaskUtil.html#merge-com.google.protobuf.FieldMask-com.google.protobuf.Message-com.google.protobuf.Message.Builder-

[11] https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/Descriptors

[12] https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/Descriptors.FieldDescriptor.html

[13] https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/util/FieldMaskUtil.html#normalize-com.google.protobuf.FieldMask-

[14] https://google.aip.dev/161

目录
相关文章
|
XML JSON 前端开发
软件测试|Spring Boot 的 RESTful API 设计与实现
软件测试|Spring Boot 的 RESTful API 设计与实现
软件测试|Spring Boot 的 RESTful API 设计与实现
|
SQL 安全 Java
微服务API开放授权平台的设计与实现
微服务API开放授权平台的设计与实现
511 0
微服务API开放授权平台的设计与实现
|
XML SQL JSON
RESTful API 设计指南
RESTful API 设计指南
315 0
RESTful API 设计指南
|
消息中间件 JSON 缓存
如何设计 API 接口,实现统一格式返回?
如何设计 API 接口,实现统一格式返回?
如何设计 API 接口,实现统一格式返回?
|
算法 前端开发 安全
SpringCloud Gateway API接口安全设计(加密 、签名、安全)(一)
SpringCloud Gateway API接口安全设计(加密 、签名、安全)(一)
SpringCloud Gateway API接口安全设计(加密 、签名、安全)(一)
|
JSON 监控 安全
22条API设计的最佳实践
22条API设计的最佳实践
22条API设计的最佳实践
|
分布式计算 安全 API
异步 API 设计之扇入扇出模式
扇出/扇入模式是更高级 API 集成的主要内容。这些应用程序并不总是表现出相同的可用性或性能特征。
异步 API 设计之扇入扇出模式
|
JSON 监控 API
22 条 API 设计最佳实践,快收藏。。(1)
22 条 API 设计最佳实践,快收藏。。(1)
152 0
22 条 API 设计最佳实践,快收藏。。(1)
|
存储 前端开发 Java
一文概览设计Web API 中的细节
一文概览设计Web API 中的细节
191 0
|
JSON 前端开发 安全
SpringCloud Gateway API接口安全设计(加密 、签名、安全)(二)
SpringCloud Gateway API接口安全设计(加密 、签名、安全)(二)
SpringCloud Gateway API接口安全设计(加密 、签名、安全)(二)