Netflix實(shí)用API設(shè)計(jì)(下)

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)容制作

《紙鈔屋》(La casa de papel) / Netflix

上一篇文章我們概述了什么是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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末坷澡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子含蓉,更是在濱河造成了極大的恐慌频敛,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件馅扣,死亡現(xiàn)場(chǎng)離奇詭異斟赚,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)差油,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)拗军,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蓄喇,你說(shuō)我怎么就攤上這事发侵。” “怎么了公罕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵器紧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我楼眷,道長(zhǎng),這世上最難降的妖魔是什么熊尉? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任罐柳,我火速辦了婚禮,結(jié)果婚禮上狰住,老公的妹妹穿的比我還像新娘张吉。我一直安慰自己,他們只是感情好催植,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布肮蛹。 她就那樣靜靜地躺著,像睡著了一般创南。 火紅的嫁衣襯著肌膚如雪伦忠。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天稿辙,我揣著相機(jī)與錄音昆码,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛赋咽,可吹牛的內(nèi)容都是我干的旧噪。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼脓匿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼淘钟!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起陪毡,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤日月,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后缤骨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體爱咬,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年绊起,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了精拟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡虱歪,死狀恐怖蜂绎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情笋鄙,我是刑警寧澤师枣,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站萧落,受9級(jí)特大地震影響践美,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜找岖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一陨倡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧许布,春花似錦兴革、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至袁余,卻和暖如春擎勘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背泌霍。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工货抄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留述召,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓蟹地,卻偏偏與公主長(zhǎng)得像积暖,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子怪与,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • gRPC如今被很多公司應(yīng)用在大規(guī)模生產(chǎn)環(huán)境中夺刑,很多時(shí)候我們并不需要通過(guò)RPC請(qǐng)求所有數(shù)據(jù),而只關(guān)心響應(yīng)數(shù)據(jù)中的部分...
    DeepNoMind閱讀 673評(píng)論 0 0
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理分别,服務(wù)發(fā)現(xiàn)遍愿,斷路器,智...
    卡卡羅2017閱讀 134,628評(píng)論 18 139
  • 去年有段時(shí)間得空耘斩,就把谷歌GAE的API權(quán)威指南看了一遍沼填,收獲頗豐,特別是在自己幾乎獨(dú)立開(kāi)發(fā)了公司的云數(shù)據(jù)中心之后...
    騎單車(chē)的勛爵閱讀 20,471評(píng)論 0 41
  • API 與類型系統(tǒng) 由于眾所周知的原因括授,至今仍有大量生產(chǎn)環(huán)境的代碼跑在 Python 2.7 之上坞笙,在 Pytho...
    滴滴啊閱讀 422評(píng)論 1 0
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭荚虚,有人歡樂(lè)有人憂愁薛夜,有人驚喜有人失落,有的覺(jué)得收獲滿滿有...
    陌忘宇閱讀 8,528評(píng)論 28 53