當前位置:首頁 > 公眾號精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]背景在Netflix,我們大量使用gRPC來實現(xiàn)后端到后端的通信。當我們處理請求時,知道調(diào)用者對哪些字段感興趣以及忽略哪些字段通常是有益的。某些響應(yīng)字段的計算成本可能很高,某些字段可能需要遠程調(diào)用其他服務(wù)。遠程調(diào)用都是有代價的;它們會帶來額外的延遲,增加出錯的可能性,并消耗網(wǎng)絡(luò)帶...



背景



在 Netflix,我們大量使用 gRPC 來實現(xiàn)后端到后端的通信。當我們處理請求時,知道調(diào)用者對哪些字段感興趣以及忽略哪些字段通常是有益的。某些響應(yīng)字段的計算成本可能很高,某些字段可能需要遠程調(diào)用其他服務(wù)。遠程調(diào)用都是有代價的;它們會帶來額外的延遲,增加出錯的可能性,并消耗網(wǎng)絡(luò)帶寬。那么該如何知道響應(yīng)中哪些字段不需要提供給調(diào)用者,從而避免進行不必要的計算以及遠程調(diào)用?使用 GraphQL,這是通過使用字段選擇器來實現(xiàn)的。在 JSON:API 標準中,類似的技術(shù)稱為稀疏字段集[1]。在設(shè)計 gRPC API 時,我們?nèi)绾螌崿F(xiàn)類似的功能?我們在 Netflix Studio Engineering 中使用的解決方案是 protobuf FieldMask[2]。


Protobuf FieldMask



Protocol Buffers[3],或簡稱為 protobuf,是一種數(shù)據(jù)序列化機制。默認情況下,gRPC 使用 protobuf 作為其 IDL(接口定義語言)和數(shù)據(jù)序列化協(xié)議。FieldMask 是一個 protobuf 消息。當此消息出現(xiàn)在 RPC 請求中時,有關(guān)如何使用此消息有許多實用工具(utilities)和約定。FieldMask 消息包含一個名為 paths 的字段,它用于指定字段,這些字段可以由讀操作返回或由更新操作來修改。message FieldMask {
// The set of field mask paths.
repeated string paths = 1;
}



案例:Netflix Studio Production



假設(shè)有一個 Production 服務(wù)來管理 Studio Content Productions(在電影和電視行業(yè)中,術(shù)語 production[4] 是指制作電影的過程,而不是運行軟件的環(huán)境)。// 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 包含多個字段,例如:標題、格式、日程安排日期、腳本又名劇本、預(yù)算、劇集等,但讓我們保持這個例子簡單,并在請求 production時重點過濾日程安排日期和腳本。
讀取 Production 詳細信息
假設(shè)我們想要使用 GetProduction API 獲取特定 production 的信息,例如“La Casa De Papel”。雖然 production 有許多字段,但其中一些字段是從其他服務(wù)返回的,例如來自 Schedule 服務(wù)的 schedule 或來自 Script 服務(wù)的 scripts。



每次調(diào)用 GetProduction 時,Production 服務(wù)都會向 Schedule 和 Script 服務(wù)發(fā)出 RPC,即使客戶端忽略響應(yīng)中的 schedule 和 scripts 字段。如上所述,遠程調(diào)用是有代價的。如果服務(wù)知道哪些字段對調(diào)用者很重要,它可以在是否進行昂貴的調(diào)用、啟動資源密集型計算和/或調(diào)用數(shù)據(jù)庫這些事中做出明智的決定。在這個例子中,如果調(diào)用者只需要標題和格式兩個字段,Production 服務(wù)可以避免遠程調(diào)用 Schedule 和 Script 服務(wù)。
此外,請求大量字段會使響應(yīng)負載變得龐大。對某些應(yīng)用程序來說可能是個問題,例如,在網(wǎng)絡(luò)帶寬有限的移動設(shè)備上。在這些情況下,消費者只請求他們需要的字段是一種很好的做法。
一個比較笨的解決方法是添加額外的請求參數(shù),例如 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;
}


這種方法需要為每個昂貴的響應(yīng)字段添加一個自定義的 includeXXX 字段,并且不適用于嵌套字段。它還增加了請求的復(fù)雜性,最終使維護和支持更具挑戰(zhàn)性。
將 FieldMask 添加到請求消息中
API 設(shè)計者可以將 field_mask 字段添加到請求消息中,而不是創(chuàng)建一次性的“包含”字段:import "google/protobuf/field_mask.proto";

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


消費者可以為他們希望在響應(yīng)中收到的字段設(shè)置路徑。如果消費者只對標題和格式感興趣,他們可以設(shè)置帶有“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 的其他語言。
如果消費者只需要最后一個更新日程表的人的標題和電子郵件,他們可以設(shè)置不同的字段掩碼: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,則應(yīng)返回所有字段。
Protobuf 字段名稱與字段編號
你可能會注意到 FieldMask 中的路徑是使用字段名稱指定的,而在傳輸中,編碼的 protocol buffers 消息僅包含字段編號,而不包含字段名稱。這(以及其他一些技術(shù),如用于簽名類型的 ZigZag[5] 編碼)會讓 protobuf 消息節(jié)省空間。
為了理解字段編號和字段名稱之間的區(qū)別,讓我們詳細了解一下 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)創(chuàng)建代碼。這個生成的代碼包含定義消息的類,以及消息和字段描述符。描述符包含將消息編碼和解碼為其二進制格式所需的所有信息。例如,它們包含字段編號、名稱、類型。消息生產(chǎn)者使用描述符將消息轉(zhuǎn)換為傳輸格式。為提高效率,二進制消息僅包含字段數(shù)值對。不包括字段名稱。當消費者收到消息時,它通過引用編譯的消息定義將字節(jié)流解碼為一個對象(例如,Java 對象)。



如上所述,F(xiàn)ieldMask 列出字段名稱,而不是數(shù)字。在 Netflix,我們使用字段編號并使用 FieldMaskUtil.fromFieldNumbers()[6] 方法將它們轉(zhuǎn)換為字段名稱。此方法利用編譯的消息定義將字段編號轉(zhuǎn)換為字段名稱并創(chuàng)建 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 會限制你重命名消息字段的能力。重命名消息字段通常被認為是一種安全操作,因為如上所述,字段名稱不會被傳輸發(fā)送的,而是使用消費者端的字段編號派生的。使用 FieldMask,字段名稱會在消息負載中被發(fā)送出去(在路徑字段值中)并且還是很重要的部分。
假設(shè)我們要將字段 title 重命名為 title_name 并發(fā)布消息定義的 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;
}





在此圖表中,生產(chǎn)者(服務(wù)器)使用新的描述符,字段編號 2 名為 title_name。傳輸發(fā)送的二進制消息包含字段編號及其值。消費者仍然使用原始描述符,其中字段編號 2 作為標題。它仍然能夠通過字段號對消息進行解碼。
如果消費者不使用 FieldMask 來請求字段,那倒是沒問題。如果消費者使用 FieldMask 字段中的“title”路徑進行調(diào)用,生產(chǎn)者將無法找到該字段。生產(chǎn)者在其描述符中沒有名為 title 的字段,因此它不知道消費者請求的字段編號為 2。



如我們所見,如果一個字段被重命名,后端應(yīng)該能夠支持新舊字段名稱,直到所有調(diào)用者都遷移到新字段名稱(向后兼容性問題)。
有多種方法可以處理此限制:
  • 使用 FieldMask 時切勿重命名字段。這是最簡單的解決方案,但并非總是可行


  • 要求后端支持所有舊的字段名稱。這解決了向后兼容性問題,但需要后端額外的代碼來跟蹤所有歷史字段名稱


  • 棄用舊字段并創(chuàng)建新字段而不是重命名。在我們的示例中,我們將創(chuàng)建 title_name 字段編號 6。此選項比前一個有一些優(yōu)點:它允許生產(chǎn)者繼續(xù)使用生成的描述符而不是自定義轉(zhuǎn)換器;此外,棄用一個字段在消費者端影響更大


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 合約中不可或缺的一部分。
在生產(chǎn)者(服務(wù)器)端使用 FieldMask
在生產(chǎn)者(服務(wù)器)端,可以使用 FieldMaskUtil.merge()[7] 方法(8 和 9 行)從響應(yīng)負載中刪除不必要的字段:@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();
}


如果服務(wù)端代碼還需要知道請求哪些字段以避免進行外部調(diào)用、數(shù)據(jù)庫查詢或昂貴的計算,則可以從 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 字段被請求時調(diào)用 makeExpensiveCallToScheduleService 方法(第 21 行)。讓我們更詳細地探索這個代碼示例。
  1. SCHEDULE_FIELD_NAME 常量包含字段的名稱。此代碼示例使用消息類型 Descriptor[8] 和 FieldDescriptor[9] 通過字段編號查找字段名稱。protobuf 字段名稱和字段編號之間的區(qū)別在上面的 Protobuf 字段名稱與字段編號部分進行了描述。


  2. FieldMaskUtil.normalize()[10] 返回具有按字母順序排序和去重的字段路徑(又名規(guī)范形式)的 FieldMask。


  3. scheduleFieldRequestedvalue 表達式(第14 - 17 行)采用 FieldMask 路徑流,將其映射到頂級(top-level)字段流,如果頂級字段包含 SCHEDULE_FIELD_NAME 常量的值,則返回 true。


  4. 僅當 scheduleFieldRequested 為真時才檢索 ProductionSchedule。



如果你決定將 FieldMask 用于不同的消息和字段,請考慮創(chuàng)建可重用的實用封裝方法。例如,基于 FieldMask 和 FieldDescriptor 返回所有頂級字段的方法,如果字段存在于 FieldMask 中則返回的方法等。


發(fā)布預(yù)編譯的 FieldMask



某些訪問模式可能比其他訪問模式更常見。如果多個消費者對同一字段子集感興趣,API 生產(chǎn)者可以提供帶有 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);

}


提供預(yù)編譯的字段掩碼可以簡化最常見場景的 API 使用,并使消費者能夠靈活地為更具體的用例構(gòu)建自己的字段掩碼。


限制



  • 使用 FieldMask 會限制重命名消息字段的能力(在 Protobuf 字段名稱與字段編號部分中描述)


  • 重復(fù)字段只允許出現(xiàn)在路徑字符串的最后一個位置。這意味著你不能在列表內(nèi)的消息中選擇(屏蔽)單個子字段。這在可預(yù)見的未來可能會發(fā)生變化,因為最近批準的 Google API 改進提案 AIP-161 字段掩碼[11]包括對重復(fù)字段的通配符的支持。



總結(jié)



Protobuf FieldMask 是一個簡單但功能強大的概念。它可以幫助使 API 更健壯,服務(wù)實現(xiàn)更高效。
這篇博文介紹了 Netflix Studio Engineering 如何以及為何將其用于讀取數(shù)據(jù)的 API。第 2 部分將闡明使用 FieldMask 進行更新和刪除操作。


本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉