GraphQL是Facebook提出的一種數據查詢語言,核心特性是數據聚合和按需索取,目前被普遍應用於先後端之間,解決客戶端靈活使用數據問題。本文介紹的是GraphQL的另外一種實踐,咱們將GraphQL下沉至後端BFF層之下,結合元數據技術,實現數據和加工邏輯的按需查詢和執行。這樣不只解決了後端BFF層靈活使用數據的問題,這些字段加工邏輯還能夠直接複用,大幅度提高了研發的效率。本文介紹的實踐方案已經在美團部分業務場景中落地,並取得不錯效果,但願這些經驗可以對你們有幫助。
BFF一詞來自Sam Newman的一篇博文《Pattern:Backends For Frontends》,指的是服務於前端的後端。BFF是解決什麼問題的呢?據原文描述,隨着移動互聯網的興起,原適應於桌面Web的服務端功能但願同時提供給移動App使用,而在這個過程當中存在這樣的問題:html
由於端的差別性存在,服務端的功能要針對端的差別進行適配和裁剪,而服務端的業務功能自己是相對單一的,這就產生了一個矛盾——服務端的單一業務功能和端的差別性訴求之間的矛盾。那麼這個問題怎麼解決呢?這也是文章的副標題所描述的"Single-purpose Edge Services for UIs and external parties",引入BFF,由BFF來針對多端差別作適配,這也是目前業界普遍使用的一種模式。前端
在實際業務的實踐中,致使這種端差別性的緣由有不少,有技術的緣由,也有業務的緣由。好比,用戶的客戶端是Android仍是iOS,是大屏仍是小屏,是什麼版本。再好比,業務屬於哪一個行業,產品形態是什麼,功能投放在什麼場景,面向的用戶羣體是誰等等。這些因素都會帶來面向端的功能邏輯的差別性。java
在這個問題上,筆者所在團隊負責的商品展現業務有必定的發言權,一樣的商品業務,在C端的展現功能邏輯,深入受到商品類型、所在行業、交易形態、投放場所、面向羣體等因素的影響。同時,面向消費者端的功能頻繁迭代的屬性,更是加重並深化了這種矛盾,使其演化成了一種服務端單一穩定與端的差別靈活之間的矛盾,這也是商品展現(商品展現BFF)業務系統存在的必然性緣由。本文主要在美團到店商品展現場景的背景下,介紹面臨的一些問題及解決思路。git
BFF這層的引入是解決服務端單一穩定與端的差別靈活訴求之間的矛盾,這個矛盾並非不存在,而是轉移了。由原來後端和前端之間的矛盾轉移成了BFF和前端之間的矛盾。筆者所在團隊的主要工做,就是和這種矛盾做鬥爭。下面以具體的業務場景爲例,結合當前的業務特色,說明在BFF的生產模式下,咱們所面臨的具體問題。下圖是兩個不一樣行業的團購貨架展現模塊,這兩個模塊咱們認爲是兩個商品的展現場景,它們是兩套獨立定義的產品邏輯,而且會各自迭代。程序員
在業務發展初期,這樣的場景很少。BFF層系統「煙囪式」建設,功能快速開發上線知足業務的訴求,在這樣的狀況下,這種矛盾表現的不明顯。而隨着業務發展,行業的開拓,造成了許許多多這樣的商品展現功能,矛盾逐漸加重,主要表如今如下兩個方面:github
if…else…
,代碼過程式編寫,系統複雜度較高,難以修改和維護。那麼這些問題是怎麼產生的呢?這要結合「煙囪式」系統建設的背景和商品展現場景所面臨的業務,以及系統特色來進行理解。算法
特色一:外部依賴多、場景間取數存在差別、用戶體驗要求高後端
圖例展現了兩個不一樣行業的團購貨架模塊,這樣一個看似不大的模塊,後端在BFF層要調用20個以上的下游服務才能把數據拿全,這是其一。在上面兩個不一樣的場景中,須要的數據源集合存在差別,並且這種差別廣泛存在,這是其二,好比足療團購貨架須要的某個數據源,在麗人團購貨架上不須要,麗人團購貨架須要的某個數據源,足療團購貨架不須要。儘管依賴下游服務多,同時還要保證C端的用戶體驗,這是其三。設計模式
這幾個特色給技術帶來了不小的難題:1)聚合大小難控制,聚合功能是分場景建設?仍是統一建設?若是分場景建設,必然存在不一樣場景重複編寫相似聚合邏輯的問題。若是統一建設,那麼一個大而全的數據聚合中必然會存在無效的調用。2)聚合邏輯的複雜性控制問題,在這麼多的數據源的狀況下,不只要考慮業務邏輯怎麼寫,還要考慮異步調用的編排,在代碼複雜度未能良好控制的狀況下,後續聚合的變動修改將會是一個難題。緩存
特色二:展現邏輯多、場景之間存在差別,共性個性邏輯耦合
咱們能夠明顯地識別某一類場景的邏輯是存在共性的,好比團單相關的展現場景。直觀能夠看出基本上都是展現團單維度的信息,但這只是表象。實際上在模塊的生成過程當中存在諸多的差別,好比如下兩種差別:
諸如此類的展現邏輯的差別性還有不少。相似的場景實際上在內部存在不少差別的邏輯,後端如何應對這種差別性是一個難題,下面是最多見的一種寫法,經過讀取具體的條件字段來作判斷實現邏輯路由,以下所示:
if(category == "麗人") { title = "[" + category + "]" + productTitle; } else if (category == "足療") { title = productTitle; }
這種方案在功能實現方面沒有問題,也可以複用共同的邏輯。可是實際上在場景很是多的狀況下,將會有很是多的差別性判斷邏輯疊加在一塊兒,功能一直會被持續迭代的狀況下,能夠想象,系統將會變得愈來愈複雜,愈來愈難以修改和維護。
總結:在BFF這層,不一樣商品展現場景存在差別。在業務發展初期,系統經過獨立建設的方式支持業務快速試錯,在這種狀況下,業務差別性帶來的問題不明顯。而隨着業務的不斷髮展,須要搭建及運營的場景愈來愈多,呈規模化趨勢。此時,業務對技術效率提出了更高的要求。在這種場景多、場景間存在差別的背景下,如何知足場景拓展效率同時可以控制系統的複雜性,就是咱們業務場景中面臨的核心問題。
目前業界針對此類的解決方案主要有兩種模式,一種是後端BFF模式,另外一種是前端BFF模式。
後端BFF模式指的是BFF由後端同窗負責,這種模式目前最普遍的實踐是基於GraphQL搭建的後端BFF方案,具體是:後端將展現字段封裝成展現服務,經過GraphQL編排以後暴露給前端使用。以下圖所示:
這種模式最大的特性和優點是,當展現字段已經存在的狀況下,後端不須要關心前端差別性需求,按需查詢的能力由GraphQL支持。這個特性能夠很好地應對不一樣場景存在展現字段差別性這個問題,前端直接基於GraphQL按需查詢數據便可,後端不須要變動。同時,藉助GraphQL的編排和聚合查詢能力,後端能夠將邏輯分解在不一樣的展現服務中,所以在必定程度上可以化解BFF這層的複雜性。
可是基於這種模式,仍然存在幾個問題:展現服務顆粒度問題、數據圖劃分問題以及字段擴散問題,下圖是基於當前模式的具體案例:
1)展現服務顆粒度設計問題
這種方案要求展現邏輯和取數邏輯封裝在一個模塊中,造成一個展現服務(Presentation Service),如上圖所示。而實際上展現邏輯和取數邏輯是多對多的關係,仍是之前文提到的例子說明:
背景:有兩個展現服務,分別封裝了商品標題和商品標籤的查詢能力。
情景:此時PM提了一個需求,但願商品在某個場景的標題以「[類型]+商品標題」的形式展現,此時商品標題的拼接依賴類型數據,而此時類型數據商品標籤展現服務中已經調用了。
問題:商品標題展現服務本身調用類型數據仍是將兩個展現服務合併到一塊兒?
以上描述的問題的是展現服務顆粒度把控的問題,咱們能夠懷疑上述的示例是否是由於展現服務的顆粒度太小?那麼反過來看一看,若是將兩個服務合併到一塊兒,那麼勢必又會存在冗餘。這是展現服務設計的難點,核心緣由在於,展現邏輯和取數邏輯自己是多對多的關係,結果卻被設計放在了一塊兒。
2)數據圖劃分問題
經過GraphQL將多個展現服務的數據聚合到一張圖(GraphQL Schema)中,造成一個數據視圖,須要數據的時候只要數據在圖中,就能夠基於Query按需查詢。那麼問題來了,這個圖應該怎麼組織?是一張圖仍是多張圖?圖過大的話,勢必帶來複雜的數據關係維護問題,圖太小則將會下降方案自己的價值。
3)展現服務內部複雜性 + 模型擴散問題
上文提到過一個商品標題的展現存在不一樣拼接邏輯的狀況,在商品展現場景,這種邏輯特別廣泛。好比一樣是價格,A行業展現優惠後價格,B行業展現優惠前價格;一樣是標籤位置,C行業展現服務時長,而D行業展現商品特性等。那麼問題來了,展現模型如何設計?以標題字段爲例,是在展現模型上放個title
字段就能夠,仍是分別放個title
和titleWithCategory
?若是是前者那麼服務內部必然會存在if…else…
這種邏輯,用於區分title
的拼接方式,這一樣會致使展現服務內部的複雜性。若是是多個字段,那麼能夠想象,展現服務的模型字段也將會不斷擴散。
總結:後端BFF模式可以在必定程度上化解後端邏輯的複雜性,同時提供一個展現字段的複用機制。可是仍然存在未決問題,如展現服務的顆粒度設計問題,數據圖的劃分問題,以及展現服務內部的複雜性和字段擴散問題。目前這種模式實踐的表明有Facebook、愛彼迎、eBay、愛奇藝、攜程、去哪兒等等。
前端BFF模式在Sam Newman的文章中的"And Autonomy"部分有特別的介紹,指的是BFF自己由前端團隊本身負責,以下示意圖所示:
這種模式的理念是,原本能一個團隊交付的需求,不必拆成兩個團隊,兩個團隊自己帶來較大的溝通協做成本。本質上,也是一種將「敵我矛盾」轉化爲「人民內部矛盾」的思路。前端徹底接手BFF的開發工做,實現數據查詢的自給自足,大大減小了先後端的協做成本。可是這種模式沒有提到咱們關心的一些核心問題,如:複雜性如何應對、差別性如何應對、展現模型如何設計等等問題。除此以外,這種模式也存在一些前提條件及弊端,好比較爲完備的前端基礎設施;前端不只僅須要關心渲染、還須要瞭解業務邏輯等。
總結:前端BFF模式經過前端自主查詢和使用數據,從而達到下降跨團隊協做的成本,提高BFF研發效率的效果。目前這種模式的實踐表明是阿里巴巴。
經過對後端BFF和前端BFF兩種模式的分析,咱們最終選擇後端BFF模式,前端BFF這個方案對目前的研發模式影響較大,不只須要大量的前端資源,並且須要建設完善的前端基礎設施,方案實施成本比較高昂。
前文提到的後端GraphQL BFF模式代入咱們的具體場景雖然存在一些問題,可是整體有很是大的參考價值,好比展現字段的複用思路、數據的按需查詢思路等等。在商品展現場景中,有80%的工做集中在數據的聚合和集成部分,而且這部分具備很強的複用價值,所以信息的查詢和聚合是咱們面臨的主要矛盾。所以,咱們的思路是:基於GraphQL+後端BFF方案改進,實現取數邏輯和展現邏輯的可沉澱、可組合、可複用,總體架構以下示意圖所示:
從上圖可看出,與傳統GraphQL BFF方案最大的差異在於咱們將GraphQL下放至數據聚合部分,因爲數據來源於商品領域,領域是相對穩定的,所以數據圖規模可控且相對穩定。除此以外,總體架構的核心設計還包括如下三個方面:1)取數展現分離;2)查詢模型歸一;3)元數據驅動架構。
咱們經過取數展現分離解決展現服務顆粒度問題,同時使得展現邏輯和取數邏輯可沉澱、可複用;經過查詢模型歸一化設計解決展現字段擴散的問題;經過元數據驅動架構實現能力的可視化,業務組件編排執行的自動化,這可以讓業務開發同窗聚焦於業務邏輯的自己。下面將針對這三個部分逐一展開介紹。
上文提到,在商品展現場景中,展現邏輯和取數邏輯是多對多的關係,而傳統的基於GraphQL的後端BFF實踐方案把它們封裝在一塊兒,這是致使展現服務顆粒度難以設計的根本緣由。思考一下取數邏輯和展現邏輯的關注點是什麼?取數邏輯關注怎麼查詢和聚合數據,而展現邏輯關注怎麼加工生成須要的展現字段,它們的關注點不同,放在一塊兒也會增長展現服務的複雜性。所以,咱們的思路是將取數邏輯和展現邏輯分離開來,單獨封裝成邏輯單元,分別叫取數單元和展現單元。在取數展現分離以後,GraphQL也隨之下沉,用於實現數據的按需聚合,以下圖所示:
那麼取數和展現邏輯的封裝顆粒度是怎麼樣的呢?不能過小也不能太大,在顆粒度的設計上,咱們有兩個核心考量:1)複用,展現邏輯和取數邏輯在商品展現場景中,都是能夠被複用的資產,咱們但願它們能沉澱下來,被單獨按需使用;2)簡單,保持簡單,這樣容易修改和維護。基於這兩點考慮,顆粒度的定義以下:
分開的好處是簡單且可被組合使用,那麼具體如何實現組合使用呢?咱們的思路是經過元數據來描述它們之間的關係,基於元數據由統一的執行框架來關聯運行,具體設計下文會展開介紹。經過取數和展現的分離,元數據的關聯和運行時的組合調用,能夠保持邏輯單元的簡單,同時又知足複用訴求,這也很好地解決了傳統方案中存在的展現服務的顆粒度問題。
展現單元的加工結果經過什麼樣的接口透出呢?接下來,咱們介紹一下查詢接口設計的問題。
1)查詢接口設計的難點
常見查詢接口的設計模式有如下兩種:
以上兩種模式在業界都有普遍應用,且它們都有明確的優缺點。強類型模式對開發者友好,可是業務是不斷迭代的,與此同時,系統沉澱的展現單元會不斷豐富,在這樣的狀況下,接口返回的DTO中的字段將會越來越多,每次新功能的支持,都要伴隨着接口查詢模型的修改,JAR版本的升級。而JAR的升級涉及數據提供方和數據消費兩方,存在明顯效率問題。另外,能夠想象,查詢模型的不斷迭代,最終將會包括成百上千個字段,難以維護。
而弱類型模式剛好能夠彌補這一缺點,可是弱類型模式對於開發者來講很是不友好,接口查詢模型中有哪些查詢結果對於開發者來講在開發的過程當中徹底沒有感受,可是程序員的天性就是喜歡經過代碼去理解邏輯,而非配置和文檔。其實,這兩種接口設計模式都存在着一個共性問題——缺乏抽象,下面兩節,咱們將介紹在接口返回的查詢模型設計方面的抽象思路及框架能力支持。
2)查詢模型歸一化設計
回到商品展現場景中,一個展現字段有多種不一樣的實現,如商品標題的兩種不一樣實現方式:1)商品標題;2)[類目]+商品標題。商品標題和這兩種展現邏輯的關係本質上是一種抽象-具體的關係。識別這個關鍵點,思路就明瞭了,咱們的思路是對查詢模型作抽象。查詢模型上都是抽象的展現字段,一個展現字段對應多個展現單元,以下圖所示:
在實現層面一樣基於元數據描述展現字段和展現單元之間的關係,基於以上的設計思路,能夠在必定程度上減緩模型的擴散,可是還不能避免擴展。好比除了價格、庫存、銷量等每一個商品都有的標準屬性以外,不一樣的商品類型通常還會有這個商品特有的屬性。好比密室主題拼場商品纔有「幾人拼」這樣的描述屬性,這種字段自己抽象的意義不大,且放在商品查詢模型中做爲一個單獨的字段會致使模型擴張,針對這類問題,咱們的解決思路是引入擴展屬性,擴展屬性專門承載這類非標準的字段。經過標準字段 + 擴展屬性的方式創建查詢模型,可以較好地解決字段擴散的問題。
到目前爲止,咱們定義瞭如何分解業務邏輯單元以及如何設計查詢模型,並提到用元數據描述它們之間的關係。基於以上定義實現的業務邏輯及模型,都具有很強的複用價值,能夠做爲業務資產沉澱下來。那麼,爲何用元數據描述業務功能及模型之間的關係呢?
咱們引入元數據描述主要有兩個目的:1)代碼邏輯的自動編排,經過元數據描述業務邏輯之間的關聯關係,運行時能夠自動基於元數據實現邏輯之間的關聯執行,從而能夠消除大量的人工邏輯編排代碼;2)業務功能的可視化,元數據自己描述了業務邏輯所提供的功能,以下面兩個示例:
團單基礎售價字符串展現,例:30元。
團單市場價展現字段,例:100元。
這些元數據上報到系統中,能夠用於展現當前系統所提供的功能。經過元數據描述組件及組件之間關聯關係,經過框架解析元數據自動進行業務組件的調用執行,造成了以下的元數據架構:
總體架構由三個核心部分組成:
經過以上三個部分有機的組合在一塊兒,造成了一個元數據驅動風格的架構。
1)GraphQL直接使用問題
引入GraphQL,會引入一些額外的複雜性,好比會涉及到GraphQL帶來的一些概念如:Schema、RuntimeWiring,下面是基於GraphQL原生Java框架的開發過程:
這些概念對於未接觸過GraphQL的同窗來講,增長了學習和理解的成本,而這些概念和業務領域一般沒有什麼關係。而咱們僅僅但願使用GraphQL的按需查詢特性,卻被GraphQL自己拖累了,業務開發同窗的關注點應該聚焦在業務邏輯自己纔對,這個問題如何解決呢?
著名計算機科學家David Wheeler說了一句名言,"All problems in computer science can be solved by another level of indirection"。沒有加一層解決不了的問題,本質上是須要有人來對這事負責,所以咱們在原生GraphQL之上增長了一層執行引擎層來解決這些問題,目標是屏蔽GraphQL的複雜性,讓開發人員只須要關注業務邏輯。
2)取數接口標準化
首先要簡化數據的接入,原生的DataFetcher
和DataLoader
都是處在一個比較高的抽象層次,缺乏業務語義,而在查詢場景,咱們可以概括出,全部的查詢都屬於如下三種模式:
由此,咱們對查詢接口進行了標準化,業務開發同窗基於場景判斷是那種,按需選擇使用便可,取數接口標準化設計以下:
業務開發同窗按需選擇所須要使用的取數器,經過泛型指定結果類型,1查1和1查N比較簡單,N查N咱們對其定義爲批量查詢接口,用於知足"N+1"的場景,其中batchSize
字段用於指定分片大小,batchKey
用於指定查詢Key,業務開發只須要指定參數,其餘的框架會自動處理。除此以外,咱們還約束了返回結果必須是CompleteFuture
,用於知足聚合查詢的全鏈路異步化。
3)聚合編排自動化
取數接口標準化使得數據源的語義更清晰,開發過程按需選擇便可,簡化了業務的開發。可是此時業務開發同窗寫好Fetcher
以後,還須要去另外一個地方去寫Schema
,並且寫完Schema
還要再寫Schema
和Fetcher
的映射關係,業務開發更享受寫代碼的過程,不太願意寫完代碼還要去另一個地方取配置,而且同時維護代碼和對應配置也提升了出錯的可能性,可否將這些冗雜的步驟移除掉?
Schema
和RuntimeWiring
本質上是想描述某些信息,若是這些信息換一種方式描述是否是也能夠,咱們的優化思路是:在業務開發過程當中標記註解,經過註解標註的元數據描述這些信息,其餘的事情交給框架來作。解決思路示意圖以下:
雖然GraphQL已經開源了,可是Facebook只開源了相關標準,並無給出解決方案。GraphQL-Java框架是由社區貢獻的,基於開源的GraphQL-Java做爲按需查詢引擎的方案,咱們發現了GraphQL應用方面的一些問題,這些問題有部分是因爲使用姿式不當所致使的,也有部分是GraphQL自己實現的問題,好比咱們遇到的幾個典型的問題:
Schema
的解析和Query
的解析。DataLoader
的層級調度問題。因而,咱們對使用方式和框架作了一些優化與改造,以解決上面列舉的問題。本章着重介紹咱們在GraphQL-Java方面的優化和改造思路。
1)GraphQL語言原理概述
GraphQL是一種查詢語言,目的是基於直觀和靈活的語法構建客戶端應用程序,用於描述其數據需求和交互。GraphQL屬於一種領域特定語言(DSL),而咱們所使用的GraphQL-Java客戶端在語言編譯層面是基於ANTLR 4實現的,ANTLR 4是一種基於Java編寫的語言定義和識別工具,ANTLR是一種元語言(Meta-Language),它們的關係以下:
GraphQL執行引擎所接受的Schema
及Query
都是基於GraphQL定義的語言所表達的內容,GraphQL執行引擎不能直接理解GraphQL,在執行以前必須由GraphQL編譯器翻譯成GraphQL執行引擎可理解的文檔對象。而GraphQL編譯器是基於Java的,經驗代表在大流量場景實時解釋的狀況下,這部分代碼將會成爲CPU熱點,並且還佔用響應延遲,Schema
或Query
越複雜,性能損耗越明顯。
2)Schema及Query編譯緩存
Schema
表達的是數據視圖和取數模型同構,相對穩定,個數也很少,在咱們的業務場景一個服務也就一個。所以,咱們的作法是在啓動的時候就將基於Schema
構造的GraphQL執行引擎構造好,做爲單例緩存下來,對於Query
來講,每一個場景的Query
有些差別,所以Query
的解析結果不能做爲單例,咱們的作法是實現PreparsedDocumentProvider
接口,基於Query
做爲Key將Query
編譯結果緩存下來。以下圖所示:
1)GraphQL執行機制及問題
咱們先一塊兒瞭解一下GraphQL-Java執行引擎的運行機制是怎麼樣的。假設在執行策略上咱們選取的是AsyncExecutionStrategy
,來看看GraphQL執行引擎的執行過程:
以上時序圖作了些簡化,去除了一些與重點無關的信息,AsyncExecutionStrategy
的execute
方法是對象執行策略的異步化模式實現,是查詢執行的起點,也是根節點查詢的入口,AsyncExecutionStrategy
對對象的多個字段的查詢邏輯,採起的是循環+異步化的實現方式,咱們從AsyncExecutionStrategy
的execute
方法觸發,理解GraphQL查詢過程以下:
DataFetcher
的get
方法,若是字段沒有綁定DataFetcher
,則經過默認的PropertyDataFetcher
查詢字段,PropertyDataFetcher
的實現是基於反射從源對象中讀取查詢字段。DataFetcher
查詢獲得結果包裝成CompletableFuture
,若是結果自己是CompletableFuture
,那麼不會包裝。結果CompletableFuture
完成以後,調用completeValue
,基於結果類型分別處理。
completeValue
。execute
,又回到了起點,也就是AsyncExecutionStrategy的execute
。以上是GraphQL的執行過程,這個過程有什麼問題呢?下面基於圖上的標記順序一塊兒看看GraphQL在咱們的業務場景中應用和實踐所遇到的問題,這些問題不表明在其餘場景也是問題,僅供參考:
問題1:PropertyDataFetcher
CPU熱點問題,PropertyDataFetcher
在整個查詢過程當中屬於熱點代碼,而其自己的實現也有一些優化空間,在運行時PropertyDataFetcher
的執行會成爲CPU熱點。(具體問題可參考GitHub上的commit和Conversion:https://github.com/graphql-java/graphql-java/pull/1815)
問題2:列表的計算耗時問題,列表計算是循環的,對於查詢結果中存在大列表的場景,此時循環會形成總體查詢明顯的延遲。咱們舉個具體的例子,假設查詢結果中存在一個列表大小是1000,每一個元素的處理是0.01ms,那麼整體耗時就是10ms,基於GraphQL的查機制,這個10ms會阻塞整個鏈路。
2)類型轉換優化
經過GraphQL查詢引擎拿到的GraphQL模型,和業務實現的DataFetcher
返回的取數模型是同構,可是全部字段的類型都會被轉換成GraphQL內部類型。PropertyDataFetcher
之因此會成爲CPU熱點,問題就在於這個模型轉換過程,業務定義的模型到GraphQL類型模型轉換過程示意圖以下圖所示:
當查詢結果模型中的字段很是多的時候,好比上萬個,意味着每次查詢有上萬次的PropertyDataFetcher
操做,實際就反映到了CPU熱點問題上,這個問題咱們的解決思路是保持原有業務模型不變,將非PropertyDataFetcher
查詢的結果反過來填充到業務模型上。以下示意圖所示:
基於這個思路,咱們經過GraphQL執行引擎拿到的結果就是業務Fetcher
返回的對象模型,這樣不只僅解決了因字段反射轉換帶來的CPU熱點問題,同時對於業務開發來講增長了友好性。由於GraphQL模型相似JSON模型,這種模型是缺乏業務類型的,業務開發直接使用起來很是麻煩。以上優化在一個場景上試點測試,結果顯示該場景的平均響應時間縮短1.457ms,平均99線縮短5.82ms,平均CPU利用率下降約12%。
3)列表計算優化
當列表元素比較多的時候,默認的單線程遍歷列表元素計算的方式所帶來的延遲消耗很是明顯,對於響應時間比較敏感的場景這個延遲優化頗有必要。針對這個問題咱們的解決思路是充分利用CPU多核心計算的能力,將列表拆分紅任務,經過多線程並行執行,實現機制以下:
1)DataLoader基本原理
先簡單介紹一下DataLoader的基本原理,DataLoader有兩個方法,一個是load
,一個是dispatch
,在解決N+1問題的場景中,DataLoader是這麼用的:
總體分爲2個階段,第一個階段調用load
,調用N次,第二個階段調用dispatch
,調用dispatch
的時候會真正的執行數據查詢,從而達到批量查詢+分片的效果。
2)DataLoader調度問題
GraphQL-Java對DataLoader的集成支持的實如今FieldLevelTrackingApproach
中,FieldLevelTrackingApproach
的實現會存在怎樣的問題呢?下面基於一張圖表達原生DataLoader調度機制所產生的問題:
問題很明顯,基於FieldLevelTrackingApproach
的實現,下一層級的DataLoader
的dispatch
是須要等到本層級的結果都回來以後才發出。基於這樣的實現,查詢總耗時的計算公式等於:TOTAL = MAX(Level 1 Latency)+ MAX(Level 2 Latency)+ MAX(Level 3 Latency)+ … ,總查詢耗時等於每層耗時最大的值加起來,而實際上若是鏈路編排由業務開發同窗本身來寫的話,理論上的效果是總耗時等於全部鏈路最長的那個鏈路所耗的時間,這個纔是合理的。而FieldLevelTrackingApproach
的實現所表現出來的結果是反常識的,至於爲何這麼實現,目前咱們理解多是設計者基於簡單和通用方面的考慮。
問題在於以上的實如今有些業務場景下是不能接受的,好比咱們的列表場景的響應時間約束一共也就不到100ms,其中幾十ms是由於這個緣由搭進去的。針對這個問題的解決思路,一種方式是對於響應時間要求特別高的場景獨立編排,不採用GraphQL;另外一種方式是在GraphQL層面解決這個問題,保持架構的統一性。接下來,介紹一下咱們是如何擴展GraphQL-Java執行引擎來解決這個問題的。
3)DataLoader調度優化
針對DataLoader調度的性能問題,咱們的解決思路是在最後一次調用某個DataLoader
的load
以後,當即調用dispatch
方法發出查詢請求,問題是咱們怎麼知道哪一次的load是最後一次load呢?這個問題也是解決DataLoader調度問題的難點,如下舉個例子來解釋咱們的解決思路:
假設咱們查詢到的模型結構以下:根節點是Query
下的字段,字段名叫subjects
,subject
引用的是個列表,subject
下有兩個元素,都是ModelA
的對象實例,ModelA
有兩個字段,fieldA
和fieldB
,subjects[0]
的fieldA
關聯是ModelB
的一個實例,subjects[0]
的fieldB
關聯多個ModelC
實例。
爲了方便理解,咱們定義一些概念,字段、字段實例、字段實例執行完、字段實例值大小、字段實例值對象執行大小、字段實例值對象執行完等等:
subjects
和subjects/fieldA
。subjects[0]/fieldA
和subjects[1]/fieldA
是字段subjects/fieldA
的實例。subjects[0]/fieldA
字段實例值大小是1,subjects[0]/fieldB
字段實例值大小是3。除了以上定義以外,咱們的業務場景還知足如下條件:
DataLoader
必定屬於某個字段,某個字段下的DataLoader
應該被執行次數等於其下的對象實例個數。基於以上信息,咱們能夠得出如下問題分析:
DataLoader
在當前實例下須要執行load
的次數,所以在執行load
以後,咱們能夠知道當前對象實例是不是其所在字段實例的最後一個對象。subjects
的大小是2,那麼就知道subjects
字段有兩個字段實例subjects[0]
和subjects[1]
,也就知道字段subjects/fieldA
有兩個實例,subjects[0]/fieldA
和subjects[1]/fieldA
,所以咱們從根節點能夠往下推斷出某個字段實例是否執行完。經過以上分析,咱們能夠得出,一個對象執行完的條件是其所在的字段實例以及其所在的字段全部的父親字段實例都執行完,且當前執行的對象實例是其所在字段實例的最後一個對象實例的時候。基於這個判斷邏輯,咱們的實現方案是在每次調用完DataFetcher
的時候,判斷是否須要發起dispatch
,若是是則發起。另外,以上時機和條件存在漏發dispatch
的問題,有個特殊狀況,噹噹前對象實例不是最後一個,可是剩下的對象大小都爲0的時候,那麼就永遠不會觸發當前對象關聯的DataLoader
的load
了,因此在對象大小爲0的時候,須要額外再判斷一次。
根據以上邏輯分析,咱們實現了DataLoader
調用鏈路的最優化,達到理論最優的效果。
生產力決定生產關係,元數據驅動信息聚合架構是展現場景搭建的核心生產力,而業務開發模式和過程是生產關係,所以也會隨之改變。下面咱們將會從開發模式和流程兩個角度來介紹新架構對研發帶來的影響。
新架構提供了一套基於業務抽象出的標準化代碼分解約束。之前開發同窗對系統的理解極可能就是「查一查服務,把數據粘在一塊兒」,而如今,研發同窗對於業務的理解及代碼分解思路將會是一致的。好比展現單元表明的是展現邏輯,取數單元表明的是取數邏輯。同時,不少冗雜且容易出錯的邏輯已經被框架屏蔽掉了,研發同窗可以有更多的精力聚焦於業務邏輯自己,好比:業務數據的理解和封裝,展現邏輯的理解和編寫,以及查詢模型的抽象和建設。以下示意圖所示:
新架構不只僅影響了研發的代碼編寫,同時也影響着研發流程的改進,基於元數據架構實現的可視化及配置化能力,現有研發流程和以前研發流程相比有了明顯的區別,以下圖所示:
之前是「一杆子捅到底」的開發模式,每一個展現場景的搭建須要經歷過從接口的溝通到API的開發整個過程,基於新架構以後,系統自動具有多層複用及可視化、配置化能力。
狀況一:這是最好的狀況,此時取數功能和展現功能都已經被沉澱下來,研發同窗須要作的只是建立查詢方案,基於運營平臺按需選擇須要的展現單元,拿着查詢方案ID基於查詢接口就能夠查到須要的展現信息了,可視化、配置化界面以下示意圖所示:
狀況二:此時可能沒有展現功能,可是經過運營平臺查看到,數據源已經接入過,那麼也不難,只須要基於現有的數據源編寫一段加工邏輯便可,這段加工邏輯是很是爽的一段純邏輯的編寫,數據源列表以下示意圖所示:
狀況三:最壞的狀況是此時系統不能知足當前的查詢能力,這種狀況比較少見,由於後端服務是比較穩定的,那麼也無需驚慌,只須要按照標準規範將數據源接入進來,而後編寫加工邏輯片斷便可,以後這些能力是能夠被持續複用的。
商品展現場景的複雜性體如今:場景多、依賴多、邏輯多,以及不一樣場景之間存在差別。在這樣的背景下,若是是業務初期,怎麼快怎麼來,採用「煙囪式」個性化建設的方式沒必要有過多的質疑。可是隨着業務的不斷髮展,功能的不斷迭代,以及場景的規模化趨勢,「煙囪式」個性化建設的弊端會慢慢凸顯出來,包括代碼複雜度高、缺乏能力沉澱等問題。
本文以基於對美團到店商品展現場景所面臨的核心矛盾分析,介紹了:
目前,筆者所在團隊負責的核心商品展現場景都已遷入新架構,基於新的研發模式,咱們實現了50%以上的展現邏輯複用以及1倍以上的效率提高,但願本文對你們可以有所幫助。
美團到店綜合研發中心長期招聘前端、後端、數據倉庫、機器學習/數據挖掘算法工程師,座標上海,歡迎感興趣的同窗發送簡歷至:tech@meituan.com(郵件標題註明:美團到店綜合研發中心—上海)。
閱讀美團技術團隊更多技術文章合集
前端 | 算法 | 後端 | 數據 | 安全 | 運維 | iOS | Android | 測試
| 在公衆號菜單欄對話框回覆【2020年貨】、【2019年貨】、【2018年貨】、【2017年貨】等關鍵詞,可查看美團技術團隊歷年技術文章合集。
| 本文系美團技術團隊出品,著做權歸屬美團。歡迎出於分享和交流等非商業目的轉載或使用本文內容,敬請註明「內容轉載自美團技術團隊」。本文未經許可,不得進行商業性轉載或者使用。任何商用行爲,請發送郵件至tech@meituan.com申請受權。