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工作室内容制作

《纸钞屋》(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操作:

空的/缺失的字段掩码

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

这个约定会影响到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-2-protobuf-fieldmask-for-mutation-operations-2e75e1d230e4
[2] https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/field-mask
[3] https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/util/FieldMaskUtil.html#fromStringList-java.lang.Class-java.lang.Iterable-

你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。
微信公众号:DeepNoMind

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • gRPC如今被很多公司应用在大规模生产环境中,很多时候我们并不需要通过RPC请求所有数据,而只关心响应数据中的部分...
    DeepNoMind阅读 657评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 去年有段时间得空,就把谷歌GAE的API权威指南看了一遍,收获颇丰,特别是在自己几乎独立开发了公司的云数据中心之后...
    骑单车的勋爵阅读 20,427评论 0 41
  • API 与类型系统 由于众所周知的原因,至今仍有大量生产环境的代码跑在 Python 2.7 之上,在 Pytho...
    滴滴啊阅读 421评论 1 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,520评论 28 53