gRPC如今被很多公司應(yīng)用在大規(guī)模生產(chǎn)環(huán)境中攒霹,很多時(shí)候我們并不需要通過(guò)RPC請(qǐng)求所有數(shù)據(jù),而只關(guān)心響應(yīng)數(shù)據(jù)中的部分字段扣泊,Protobuf FieldMask就可以幫助我們實(shí)現(xiàn)這一目的近范。本文介紹了Netflix基于FieldMask設(shè)計(jì)更高效健壯的API的實(shí)踐,全文分兩個(gè)部分延蟹,這是第二部分评矩。原文:Practical API Design at Netflix, Part 2: Protobuf FieldMask for Mutation Operations[1]
背景
在上一篇文章中,我們討論了在設(shè)計(jì)API時(shí)如何利用FieldMask[2]作為解決方案阱飘,以便消費(fèi)者可以只請(qǐng)求他們需要的數(shù)據(jù)斥杜。在這篇文章中,我們將繼續(xù)介紹Netflix Studio Engineering如何基于FieldMask進(jìn)行更新和刪除等變更操作沥匈。
示例:Netflix工作室內(nèi)容制作
上一篇文章我們概述了什么是Production蔗喂,以及Production服務(wù)如何對(duì)其他微服務(wù)(如Schedule服務(wù)和Script服務(wù))進(jìn)行g(shù)RPC調(diào)用,以檢索特定產(chǎn)品(如《紙鈔屋》)的日程和腳本(即劇本)咐熙。我們將繼續(xù)利用這個(gè)示例并展示如何在生產(chǎn)中改變特定字段弱恒。
改變制作細(xì)節(jié)
假設(shè)我們?yōu)閯〖砑恿艘恍﹦?dòng)畫(huà)元素,因此想將format
字段從LIVE_ACTION
更新為HYBRID
棋恼。解決這個(gè)問(wèn)題的簡(jiǎn)單方法是添加一個(gè)updateProductionFormatRequest方法以及對(duì)應(yīng)的gRPC endpoint來(lái)更新productionFormat:
message UpdateProductionFormatRequest {
string id = 1;
ProductionFormat format = 2;
}
service ProductionService {
rpc UpdateProductionFormat (UpdateProductionFormatRequest)
returns (UpdateProductionFormatResponse);
}
這允許我們更新特定產(chǎn)品的生產(chǎn)格式返弹,但如果我們想要更新其他字段锈玉,如title
,甚至多個(gè)字段义起,如productionFormat
, schedule
拉背,等等,該怎么辦?在此基礎(chǔ)上默终,我們可以為每個(gè)字段執(zhí)行一個(gè)更新方法:一個(gè)用于生產(chǎn)格式椅棺,另一個(gè)用于標(biāo)題,等等:
// 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上的字段數(shù)量太多齐蔽,這將變得越來(lái)越難以維護(hù)两疚。如果我們想要在單個(gè)RPC中以原子方式更新多個(gè)字段,該怎么辦含滴?為不同的字段組合創(chuàng)建額外的方法將導(dǎo)致變更API激增诱渤,因此這個(gè)解決方案是不可擴(kuò)展的。
與其嘗試創(chuàng)建所有可能的單一組合谈况,另一種解決方案可能是定義一個(gè)UpdateProduction
endpoint勺美,用來(lái)處理所有字段:
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;
}
這個(gè)解決方案有兩個(gè)問(wèn)題。首先碑韵,消費(fèi)者必須知道并提供Production中每個(gè)必需的字段赡茸,即使他們只想更新一個(gè)字段,比如format祝闻。其次占卧,由于Production有許多字段,所以請(qǐng)求的有效負(fù)載可能會(huì)變得非常大治筒,尤其是在包含了schedule或script信息的時(shí)候屉栓。
如果我們只發(fā)送真正想要更新的字段,而不設(shè)置所有字段耸袜,會(huì)怎么樣友多?在示例中,我們只設(shè)置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);
如果我們永遠(yuǎn)不需要?jiǎng)h除字段(或者把字段置為空)堤框,那這就可以工作了域滥。但是,如果我們想要?jiǎng)h除title
字段的值蜈抓,該怎么辦启绰?同樣,我們也可以引入一次性方案沟使,如RemoveProductionTitle
委可,但正如上面討論過(guò)的,這種解決方案伸縮性不好。如果我們想從日程計(jì)劃中刪除嵌套字段(如計(jì)劃啟動(dòng)日期字段)的值着倾,又該怎么辦拾酝?我們最終會(huì)為每個(gè)可置空的子字段添加刪除RPC。
利用FieldMask進(jìn)行變更操作
除了定義大量的RPC卡者,以及承受巨大的消息載荷蒿囤,我們還可以利用FieldMask來(lái)實(shí)現(xiàn)所有的變更。FieldMask可以列出我們想明確更新的所有字段崇决。首先材诽,更新proto文件,加入UpdateProductionRequest
恒傻,包含我們想在Production中更新的數(shù)據(jù)脸侥,以及應(yīng)該被更新的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;
}
現(xiàn)在碌冶,我們可以利用FieldMask進(jìn)行變更湿痢,通過(guò)使用FieldMaskUtil.fromStringList()[3]方法為format
字段創(chuàng)建一個(gè)FieldMask來(lái)更新format涝缝,該方法為特定類型的字段路徑列表構(gòu)造一個(gè)FieldMask扑庞。在本例中,我們?cè)O(shè)置了一個(gè)類型拒逮,稍后將在這個(gè)示例的基礎(chǔ)上進(jìn)行構(gòu)建:
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);
由于我們?cè)贔ieldMask中只指定了format
字段罐氨,因此即使我們?cè)?code>ProductionUpdateOperation中提供了更多的數(shù)據(jù),也只有format
會(huì)被更新滩援。通過(guò)修改路徑栅隐,可以容易的在FieldMask中添加或刪除更多字段。在有效負(fù)載中提供但沒(méi)有添加到FieldMask路徑中的數(shù)據(jù)將不會(huì)被更新玩徊,并在操作中被忽略租悄。但是,如果我們省略了一個(gè)值恩袱,它將在該字段上執(zhí)行remove操作泣棋。我們修改上面的例子來(lái)展示,更新format畔塔,但刪除計(jì)劃的啟動(dòng)日期潭辈,這是ProductionSchedule
上的一個(gè)嵌套字段“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);
在這個(gè)例子中,我們添加了“format”和“schedule.planned_launch_date”澈吨,執(zhí)行了一次更新和一次刪除操作把敢。如果我們?cè)谟行ж?fù)載中提供了字段值,對(duì)應(yīng)的字段將被更新為新的值谅辣。但是當(dāng)構(gòu)建有效負(fù)載時(shí)修赞,我們只提供了format
,而省略了schedule.planned_launch_date
桑阶,像這樣在FieldMask中有定義柏副,但是在有效負(fù)載中沒(méi)有熙尉,將作為一個(gè)remove操作:
空的/缺失的字段掩碼
當(dāng)字段掩碼未設(shè)置或沒(méi)有路徑時(shí),更新操作將應(yīng)用于所有有效負(fù)載字段搓扯。這意味著調(diào)用者必須發(fā)送整個(gè)有效負(fù)載检痰,否則,如上所述锨推,任何未設(shè)置的字段都將被刪除铅歼。
這個(gè)約定會(huì)影響到schema的變更:當(dāng)一個(gè)新字段被添加到消息中時(shí),所有的消費(fèi)者都必須在更新操作上發(fā)送它的值换可,否則它將被刪除椎椰。
假設(shè)我們想添加一個(gè)新字段:生產(chǎn)預(yù)算。我們將同時(shí)擴(kuò)展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
}
如果消費(fèi)者不知道這個(gè)新字段或者還沒(méi)有更新客戶端沾鳄,它可能會(huì)由于沒(méi)有在更新請(qǐng)求中發(fā)送FieldMask字段而意外的把預(yù)算字段置空慨飘。
為了避免這種問(wèn)題,生產(chǎn)者應(yīng)該考慮為所有更新操作請(qǐng)求設(shè)置字段掩碼译荞。另一種選擇是實(shí)現(xiàn)版本控制協(xié)議:強(qiáng)制所有調(diào)用者發(fā)送他們的版本號(hào)瓤的,并實(shí)現(xiàn)自定義邏輯以跳過(guò)舊版本中不存在的字段。
最后
在這個(gè)系列文章中吞歼,介紹了我們?nèi)绾卧贜etflix使用FieldMask圈膏,以及如何設(shè)計(jì)一個(gè)實(shí)用的、可擴(kuò)展的API解決方案篙骡。
API設(shè)計(jì)者應(yīng)該以簡(jiǎn)單為目標(biāo)稽坤,但要考慮API的擴(kuò)展和演進(jìn)。保持API的簡(jiǎn)單性和可預(yù)測(cè)性通常并不容易糯俗。通過(guò)使用FieldMask尿褪,可以幫助我們實(shí)現(xiàn)簡(jiǎn)單和靈活的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做過(guò)研發(fā),現(xiàn)在在Mavenir做技術(shù)工作忽刽,對(duì)通信天揖、網(wǎng)絡(luò)、后端架構(gòu)跪帝、云原生今膊、DevOps、CICD伞剑、區(qū)塊鏈斑唬、AI等技術(shù)始終保持著濃厚的興趣,平時(shí)喜歡閱讀、思考恕刘,相信持續(xù)學(xué)習(xí)缤谎、終身成長(zhǎng),歡迎一起交流學(xué)習(xí)褐着。
微信公眾號(hào):DeepNoMind