当前位置:首页 > 公众号精选 > 架构师社区
[导读]背景在Netflix,我们大量使用gRPC来实现后端到后端的通信。当我们处理请求时,知道调用者对哪些字段感兴趣以及忽略哪些字段通常是有益的。某些响应字段的计算成本可能很高,某些字段可能需要远程调用其他服务。远程调用都是有代价的;它们会带来额外的延迟,增加出错的可能性,并消耗网络带...



背景



在 Netflix,我们大量使用 gRPC 来实现后端到后端的通信。当我们处理请求时,知道调用者对哪些字段感兴趣以及忽略哪些字段通常是有益的。某些响应字段的计算成本可能很高,某些字段可能需要远程调用其他服务。远程调用都是有代价的;它们会带来额外的延迟,增加出错的可能性,并消耗网络带宽。那么该如何知道响应中哪些字段不需要提供给调用者,从而避免进行不必要的计算以及远程调用?使用 GraphQL,这是通过使用字段选择器来实现的。在 JSON:API 标准中,类似的技术称为稀疏字段集[1]。在设计 gRPC API 时,我们如何实现类似的功能?我们在 Netflix Studio Engineering 中使用的解决方案是 protobuf FieldMask[2]。


Protobuf FieldMask



Protocol Buffers[3],或简称为 protobuf,是一种数据序列化机制。默认情况下,gRPC 使用 protobuf 作为其 IDL(接口定义语言)和数据序列化协议。FieldMask 是一个 protobuf 消息。当此消息出现在 RPC 请求中时,有关如何使用此消息有许多实用工具(utilities)和约定。FieldMask 消息包含一个名为 paths 的字段,它用于指定字段,这些字段可以由读操作返回或由更新操作来修改。message FieldMask {
// The set of field mask paths.
repeated string paths = 1;
}



案例:Netflix Studio Production



假设有一个 Production 服务来管理 Studio Content Productions(在电影和电视行业中,术语 production[4] 是指制作电影的过程,而不是运行软件的环境)。// Contains Production-related information
message Production {
string id = 1;
string title = 2;
ProductionFormat format = 3;
repeated ProductionScript scripts = 4;
ProductionSchedule schedule = 5;
// ... more fields
}

service ProductionService {
// returns Production by ID
rpc GetProduction (GetProductionRequest) returns (GetProductionResponse);
}

message GetProductionRequest {
string production_id = 1;
}

message GetProductionResponse {
Production production = 1;
}


GetProduction 通过唯一 ID 返回 Production 消息。一个 production 包含多个字段,例如:标题、格式、日程安排日期、脚本又名剧本、预算、剧集等,但让我们保持这个例子简单,并在请求 production时重点过滤日程安排日期和脚本。
读取 Production 详细信息
假设我们想要使用 GetProduction API 获取特定 production 的信息,例如“La Casa De Papel”。虽然 production 有许多字段,但其中一些字段是从其他服务返回的,例如来自 Schedule 服务的 schedule 或来自 Script 服务的 scripts。



每次调用 GetProduction 时,Production 服务都会向 Schedule 和 Script 服务发出 RPC,即使客户端忽略响应中的 schedule 和 scripts 字段。如上所述,远程调用是有代价的。如果服务知道哪些字段对调用者很重要,它可以在是否进行昂贵的调用、启动资源密集型计算和/或调用数据库这些事中做出明智的决定。在这个例子中,如果调用者只需要标题和格式两个字段,Production 服务可以避免远程调用 Schedule 和 Script 服务。
此外,请求大量字段会使响应负载变得庞大。对某些应用程序来说可能是个问题,例如,在网络带宽有限的移动设备上。在这些情况下,消费者只请求他们需要的字段是一种很好的做法。
一个比较笨的解决方法是添加额外的请求参数,例如 includeSchedule 和 includeScripts:// Request with one-off "include" fields, not recommended
message GetProductionRequest {
string production_id = 1;
bool include_format = 2;
bool include_schedule = 3;
bool include_scripts = 4;
}


这种方法需要为每个昂贵的响应字段添加一个自定义的 includeXXX 字段,并且不适用于嵌套字段。它还增加了请求的复杂性,最终使维护和支持更具挑战性。
将 FieldMask 添加到请求消息中
API 设计者可以将 field_mask 字段添加到请求消息中,而不是创建一次性的“包含”字段:import "google/protobuf/field_mask.proto";

message GetProductionRequest {
string production_id = 1;
google.protobuf.FieldMask field_mask = 2;
}


消费者可以为他们希望在响应中收到的字段设置路径。如果消费者只对标题和格式感兴趣,他们可以设置带有“title”和“format”路径的 FieldMask:FieldMask fieldMask = FieldMask.newBuilder()
.addPaths("title")
.addPaths("format")
.build();

GetProductionRequest request = GetProductionRequest.newBuilder()
.setProductionId(LA_CASA_DE_PAPEL_PRODUCTION_ID)
.setFieldMask(fieldMask)
.build();





请注意,即使本博文中的代码示例是用 Java 编写的,演示的概念也适用于任何支持 protocol buffers 的其他语言。
如果消费者只需要最后一个更新日程表的人的标题和电子邮件,他们可以设置不同的字段掩码:FieldMask fieldMask = FieldMask.newBuilder()
.addPaths("title")
.addPaths("schedule.last_updated_by.email")
.build();

GetProductionRequest request = GetProductionRequest.newBuilder()
.setProductionId(LA_CASA_DE_PAPEL_PRODUCTION_ID)
.setFieldMask(fieldMask)
.build();


按照惯例,如果请求中不存在 FieldMask,则应返回所有字段。
Protobuf 字段名称与字段编号
你可能会注意到 FieldMask 中的路径是使用字段名称指定的,而在传输中,编码的 protocol buffers 消息仅包含字段编号,而不包含字段名称。这(以及其他一些技术,如用于签名类型的 ZigZag[5] 编码)会让 protobuf 消息节省空间。
为了理解字段编号和字段名称之间的区别,让我们详细了解一下 protobuf 是如何编码和解码消息的。
我们的 protobuf 消息定义(.proto 文件)包含一个具有五个字段的 Production 消息。每个字段都有一个类型、名称和编号。// Message with Production-related information
message Production {
string id = 1;
string title = 2;
ProductionFormat format = 3;
repeated ProductionScript scripts = 4;
ProductionSchedule schedule = 5;
}


当 protobuf 编译器(protoc)编译此消息定义时,它会以你选择的语言(在我们的示例中为 Java)创建代码。这个生成的代码包含定义消息的类,以及消息和字段描述符。描述符包含将消息编码和解码为其二进制格式所需的所有信息。例如,它们包含字段编号、名称、类型。消息生产者使用描述符将消息转换为传输格式。为提高效率,二进制消息仅包含字段数值对。不包括字段名称。当消费者收到消息时,它通过引用编译的消息定义将字节流解码为一个对象(例如,Java 对象)。



如上所述,FieldMask 列出字段名称,而不是数字。在 Netflix,我们使用字段编号并使用 FieldMaskUtil.fromFieldNumbers()[6] 方法将它们转换为字段名称。此方法利用编译的消息定义将字段编号转换为字段名称并创建 FieldMask。FieldMask fieldMask = FieldMaskUtil.fromFieldNumbers(Production.class,
Production.TITLE_FIELD_NUMBER,
Production.FORMAT_FIELD_NUMBER);

GetProductionRequest request = GetProductionRequest.newBuilder()
.setProductionId(LA_CASA_DE_PAPEL_PRODUCTION_ID)
.setFieldMask(fieldMask)
.build();


但是,有一个容易忽略的限制:使用 FieldMask 会限制你重命名消息字段的能力。重命名消息字段通常被认为是一种安全操作,因为如上所述,字段名称不会被传输发送的,而是使用消费者端的字段编号派生的。使用 FieldMask,字段名称会在消息负载中被发送出去(在路径字段值中)并且还是很重要的部分。
假设我们要将字段 title 重命名为 title_name 并发布消息定义的 2.0 版:// version 2.0, with title field renamed to title_name
message Production {
string id = 1;
string title_name = 2;       // this field used to be "title"
ProductionFormat format = 3;
repeated ProductionScript scripts = 4;
ProductionSchedule schedule = 5;
}





在此图表中,生产者(服务器)使用新的描述符,字段编号 2 名为 title_name。传输发送的二进制消息包含字段编号及其值。消费者仍然使用原始描述符,其中字段编号 2 作为标题。它仍然能够通过字段号对消息进行解码。
如果消费者不使用 FieldMask 来请求字段,那倒是没问题。如果消费者使用 FieldMask 字段中的“title”路径进行调用,生产者将无法找到该字段。生产者在其描述符中没有名为 title 的字段,因此它不知道消费者请求的字段编号为 2。



如我们所见,如果一个字段被重命名,后端应该能够支持新旧字段名称,直到所有调用者都迁移到新字段名称(向后兼容性问题)。
有多种方法可以处理此限制:
  • 使用 FieldMask 时切勿重命名字段。这是最简单的解决方案,但并非总是可行


  • 要求后端支持所有旧的字段名称。这解决了向后兼容性问题,但需要后端额外的代码来跟踪所有历史字段名称


  • 弃用旧字段并创建新字段而不是重命名。在我们的示例中,我们将创建 title_name 字段编号 6。此选项比前一个有一些优点:它允许生产者继续使用生成的描述符而不是自定义转换器;此外,弃用一个字段在消费者端影响更大


message Production {
string id = 1;
string title = 2 [deprecated = true];  // use "title_name" field instead
ProductionFormat format = 3;
repeated ProductionScript scripts = 4;
ProductionSchedule schedule = 5;
string title_name = 6;
}


无论采用哪种解决方案,重要的是要记住 FieldMask 使字段名称成为 API 合约中不可或缺的一部分。
在生产者(服务器)端使用 FieldMask
在生产者(服务器)端,可以使用 FieldMaskUtil.merge()[7] 方法(8 和 9 行)从响应负载中删除不必要的字段:@Override
public void getProduction(GetProductionRequest request,
StreamObserverresponse) {

Production production = fetchProduction(request.getProductionId());
FieldMask fieldMask = request.getFieldMask();

Production.Builder productionWithMaskedFields = Production.newBuilder();
FieldMaskUtil.merge(fieldMask, production, productionWithMaskedFields);

GetProductionResponse response = GetProductionResponse.newBuilder()
.setProduction(productionWithMaskedFields).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}


如果服务端代码还需要知道请求哪些字段以避免进行外部调用、数据库查询或昂贵的计算,则可以从 FieldMask 路径字段中获取此信息:private static final String FIELD_SEPARATOR_REGEX = "\\.";
private static final String MAX_FIELD_NESTING = 2;
private static final String SCHEDULE_FIELD_NAME =                                // (1)
Production.getDescriptor()
.findFieldByNumber(Production.SCHEDULE_FIELD_NUMBER).getName();

@Override
public void getProduction(GetProductionRequest request,
StreamObserverresponse) {

FieldMask canonicalFieldMask =
FieldMaskUtil.normalize(request.getFieldMask());                         // (2)

boolean scheduleFieldRequested =                                             // (3)
canonicalFieldMask.getPathsList().stream()
.map(path -> path.split(FIELD_SEPARATOR_REGEX, MAX_FIELD_NESTING)[0])
.anyMatch(SCHEDULE_FIELD_NAME::equals);

if (scheduleFieldRequested) {
ProductionSchedule schedule =
makeExpensiveCallToScheduleService(request.getProductionId());       // (4)
...
}

...
}


此代码仅在schedule 字段被请求时调用 makeExpensiveCallToScheduleService 方法(第 21 行)。让我们更详细地探索这个代码示例。
  1. SCHEDULE_FIELD_NAME 常量包含字段的名称。此代码示例使用消息类型 Descriptor[8] 和 FieldDescriptor[9] 通过字段编号查找字段名称。protobuf 字段名称和字段编号之间的区别在上面的 Protobuf 字段名称与字段编号部分进行了描述。


  2. FieldMaskUtil.normalize()[10] 返回具有按字母顺序排序和去重的字段路径(又名规范形式)的 FieldMask。


  3. scheduleFieldRequestedvalue 表达式(第14 - 17 行)采用 FieldMask 路径流,将其映射到顶级(top-level)字段流,如果顶级字段包含 SCHEDULE_FIELD_NAME 常量的值,则返回 true。


  4. 仅当 scheduleFieldRequested 为真时才检索 ProductionSchedule。



如果你决定将 FieldMask 用于不同的消息和字段,请考虑创建可重用的实用封装方法。例如,基于 FieldMask 和 FieldDescriptor 返回所有顶级字段的方法,如果字段存在于 FieldMask 中则返回的方法等。


发布预编译的 FieldMask



某些访问模式可能比其他访问模式更常见。如果多个消费者对同一字段子集感兴趣,API 生产者可以提供带有 FieldMask 的客户端库,用于最常用的字段组合。public class ProductionFieldMasks {
/**
* Can be used in {@link GetProductionRequest} to query
* production title and format
*/
public static final FieldMask TITLE_AND_FORMAT_FIELD_MASK =
FieldMaskUtil.fromFieldNumbers(Production.class,
Production.TITLE_FIELD_NUMBER, Production.FORMAT_FIELD_NUMBER);

/**
* Can be used in {@link GetProductionRequest} to query
* production title and schedule
*/
public static final FieldMask TITLE_AND_SCHEDULE_FIELD_MASK =
FieldMaskUtil.fromFieldNumbers(Production.class,
Production.TITLE_FIELD_NUMBER,
Production.SCHEDULE_FIELD_NUMBER);

/**
* Can be used in {@link GetProductionRequest} to query
* production title and scripts
*/
public static final FieldMask TITLE_AND_SCRIPTS_FIELD_MASK =
FieldMaskUtil.fromFieldNumbers(Production.class,
Production.TITLE_FIELD_NUMBER, Production.SCRIPTS_FIELD_NUMBER);

}


提供预编译的字段掩码可以简化最常见场景的 API 使用,并使消费者能够灵活地为更具体的用例构建自己的字段掩码。


限制



  • 使用 FieldMask 会限制重命名消息字段的能力(在 Protobuf 字段名称与字段编号部分中描述)


  • 重复字段只允许出现在路径字符串的最后一个位置。这意味着你不能在列表内的消息中选择(屏蔽)单个子字段。这在可预见的未来可能会发生变化,因为最近批准的 Google API 改进提案 AIP-161 字段掩码[11]包括对重复字段的通配符的支持。



总结



Protobuf FieldMask 是一个简单但功能强大的概念。它可以帮助使 API 更健壮,服务实现更高效。
这篇博文介绍了 Netflix Studio Engineering 如何以及为何将其用于读取数据的 API。第 2 部分将阐明使用 FieldMask 进行更新和删除操作。


本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭