本文轉自:高可用架構
目錄
問題背景
問題分析
GraphQL中的資料結構和算法
資料計算行為的歸納總結
解題思路
解決方案
問題描述
方案詳情
總結
後記&感悟
重視提供能力
二八原則
忠于業務
參考資料
問題背景
GraphQL對于資料的聚合治理和按需查詢具有天然的優勢,資料平台可将各個部門的資料映射到一張資料圖上、即GraphQL的Schema,用戶端可通過一次請求查詢資料圖中的多個資源。與傳統sql不同,graphql經常是面向業務的,旨在提供可直接在頁面展示的資料。
真實業務場景除了擷取基礎資料外,還會有業務定制的加工轉換、請求控制和依賴資料編排。目前業界對資料的加工計算方案大緻分為兩種:
- 計算邏輯由用戶端完成,或者在graphql之上建構計算層,本質上都是将計算任務交給其他系統子產品負責;
- 使用schema指令加工後的資料映射為schema中的字段,典型如graphql-tools社群給出的方案。
這兩種方案存在如下問題:
- 将計算邏輯交由其他子產品使得業務資料的産對外連結路變長,且對于資料之間存在依賴的情況仍然需要對GraphQL子產品多次調用,實際上并沒有解決GraphQL計算能力不足導緻的寫死加工問題;
- 使用schema指令将加工後的資料定義為Schema中的字段将導緻業務計算和schema定義相耦合,資料圖會存在噪聲而變得難以維護。
本文從資料和算法分離的角度出發,對問題進行了分析拆解,并對基于查詢指令的方案進行說明。
問題分析
GraphQL中的資料結構和算法
計算機科學家Niklaus Wirth提出
程式=資料結構+算法
,GraphQL系統也不例外。
很多GraphQL使用者将查詢僅僅視為Schema的子圖、從資料圖中比對出要擷取的資料,忽略了查詢更是基于Schema資料結構的、對業務資料需求的算法描述,包括參數驗證、資料聚合和計算處理等。GraphQL提供指令機制描述使用者自定義的計算和驗證行為,規範原文如下:
Directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
In some cases, you need to provide options to alter GraphQL’s execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.
指令按照可用位置可分為schema指令和query指令,前者是對schema額外資訊的描述,後者是對查詢的描述,同一個指令可以既是schema指令、又是query指令。
正如前文所述,使用schema指令對業務計算進行描述會使得Schema定義存在噪聲、增加Schema的維護難度。例如優惠價的展示,不同的業務場景需要轉換為不同的文案,例如“優惠價95.50元”、“限時優惠¥95.50”、“神秘價¥*5.50元”等,這些處理後的資料不應該作為Schema中的字段存在。
我們通過query指令在查詢層面定義産品需求要求的資料計算行為,對Schema中的資料做個性化的處理。
資料計算行為的歸納總結
任何複雜的業務處理都是基于基本的資料結構組合和有限的操作行為組合。針對讀場景(也就是graphql的Query操作),我們對計算行為歸納如下:
- 字段加工:對結果資料進行加工處理,包括對清單的過濾、排序、去重。例如優惠價的不同展示文案、根據商品銷量對商品清單進行排序等;
- 參數轉換:包括參數整體轉換,清單類型參數的過濾、轉換等。例如将userId拼接為redis的key,過濾掉userIdList中為0的參數元素;
- 資料編排:當請求某一字段的參數來自同一查詢其他字段的查詢結果時,資料之間便存在需要編排的依賴關系。例如請求商品清單的參數來源于優惠券綁定的商品id清單,而grpahql查詢變量隻有券id;
- 控制流:根據請求變量或者其他字段結果判斷是否請求某一字段。
解題思路
确認業務計算行為應該放在query指令中完成,并對計算行為進行歸納總結後,我們在簡單分析GraphQL的執行引擎。
執行查詢的本質是從Query節點開始,對其子節點進行周遊解析,并遞歸解析子孫節點,Query節點可了解為Schema資料圖的根節點。GraphQL規範中較長的描述了graphql的執行算法,詳情參考sec-Execution。
GraphQL的java實作GraphQL Java基于
CompatableFuture
架構,對資料圖進行了并行、異步的周遊處理,其Instrumentationj接口可擷取查詢執行各個階段的運作時上下文、包括指令資訊,并具備更改查詢運作時行為和上下文資料的能力。可将其了解為GraphQL執行引擎的切面,其生效的位置包括查詢的解析、驗證以及每個資料節點的請求和完成過程。
綜上,我們對查詢計算問題做如下總結:
- Schema作為GraphQL資料平台的“資料結構”,隻應存在複用性強的領域資料,不可為具體業務做擴充;
- 查詢作為描述業務所需資料和計算行為的“算法載體”,通過指令機制和Instrumentation系統為業務計算行為提供描述和實作;
- 業務涉及的資料類型和計算行為是有限的,可對其進行總結歸納,抽象為對資料圖各元素的原子操作。
解決方案
以電商經典場景“優惠券去使用”為例,我們對基于查詢指令的解決方案進行說明,該方案架構已落地為開源項目GraphQL Calculator。
項目位址:https://github.com/graphql-calculator/graphql-calculator
問題描述
産品需求
當使用者點選店鋪優惠券時跳轉到優惠券承接頁,承接頁包括如下資料:
- 優惠券使用門檻描述文案,例如
對應的描述文案為“以下商品可使用滿300減50的優惠券”;threshold==5000(分);couponAmout=30000(分)
- 優惠券綁定的商品清單,清單按照銷量降序排序;價格從“分”轉換到“元文案”,例如
,則在用戶端展示為“¥185.90”;price=18590分
- 隻有版本大于v10的用戶端才展示商品标簽。

優惠券資訊是營銷部門的服務接口,商品詳情清單和商品标簽是商品部門的兩個服務接口。
傳統方案
基于原生的graphql系統,用戶端需要如下操作:
- 通過用戶端版本計算出是否跳過商品标簽擷取布爾值參數,能力由graphql規範内置指令@skip支援;
- 擷取優惠券詳情,并解析優惠券綁定的商品id清單;
- 根據1、2結果同時請求商品基本資訊、商品标簽;
- 對資料做業務定制的處理,例如生成優惠券描述文案、商品排序、商品價格處理等。
業務方仍需要對資料進行繁雜的解析處理來彌補GraphQL原生查詢計算能力的不足。
方案詳情
基于問題分析,graphql定義一組查詢指令用于資料的計算處理和編排控制,計算行為由計算引擎支援,預設使用aviatorscript。指令的名稱和語義參考
java.util.stream.Stream
,易于了解和使用。如何優雅地擴充 GraphQL 系統能力 對基礎指令的實作進行了說明。
參數處理&依賴編排
參數處理包括過濾掉無效參數,例如userIdList中為0的元素。而當需要将另外一個字段的結果作為入參時,兩者存在依賴關系,例如該例中綁定了商品id清單的優惠券和商品詳情清單。
GraphQL Calculator使用
@argumentTransform
和
@fetchSource
進行參數處理和編排依賴資料。
@argumentTransform
定義了對參數的加工轉換,
@fetchSource
可将指定字段的解析器擷取的結果作為其他計算指令可擷取的上下文,詳情可參考graphql-calculator#fetchsource。兩者定義如下:
directive @fetchSource(name: String!, sourceConvert:String) on FIELD
directive @argumentTransform(argumentName:String!, operateType:ParamTransformType = MAP, expression:String, dependencySources:[String!]) on FIELD
enum ParamTransformType{
# 參數轉換
MAP
# 清單類型參數過濾
FILTER
# 清單類型參數元素轉換
LIST_MAP
}
基于查詢指令的方案與傳統方案對比如下:前者省去了用戶端的寫死解析和二次調用。
技術上,GraphQL Calculator架構會對基于指令的查詢進行解析,識别
@fetchSource
注解的需要儲存的資料,并在
bindingItemIds
節點和
itemList
節點之間建立依賴關系。在執行階段會基于解析資訊,儲存上下文資料、并改變節點之間的排程關系。
GraphQL Calculator提供了校驗該指令集合法性的規則,對包括被編排的資料可能存在循環依賴的情況進行校驗。對于
@fetchSource
注解的節點,架構實際構造了對應的任務樹,該例中為
Query->coupon->bindingItemIds
,來描述
@fetchSource
節點可能存在于數組中的情況,并解決父節點解析失敗時依賴其資料的節點空等的問題。
加工轉換&集合處理
資料的定制加工和清單的排序、過濾是産品需求中常見的計算邏輯。GraphQL Calculator參考
java.util.stream.Stream
,聲明了
@map
、
@sortBy
、
@filter
和
@distinct
對資料進行加工轉換和對清單進行排序、過濾、去重。
以生成優惠券描述文案、對商品清單按照銷量降序排序為例,查詢如下:
query mapAndSortCase{
coupon{
threadHold
couponAmount
desc @map(mapper:"'滿'(threadHold/100)'減'(couponAmount/100)")
}
commodityList
@sortBy(comparator:"soldAmount",isReversed:true)
{
name
price
soldAmount
}
}
當産品需求微調疊代時,修改查詢指令表達式即可。如果出現兩個并存的業務需要對資料進行不同的處理時,也隻需拷貝查詢語句、修改表達式,不用在開發計算邏輯。在實際應用中,查詢指令對業務的快速疊代具有明顯的幫助。
流程控制
有些需求随着用戶端版本進行疊代,需要通過版本号決定是否請求某些字段,例如該例中的商品标簽資訊。GraphQL Calculator實作了内置指令
@skip
和
@include
的擴充版本
@skipBy
和
@includeBy
。與内置指令隻可将布爾類型資料作為判斷是否請求被注解字段的參數不同,
@skipBy
和
@includeBy
可使用以查詢變量為參數的表達式計算結果判斷是否請求被注解的字段。示例如下:
query mapAndSortCase($clientVersion:String, ...){
# ...
commodityList
# ...
{
name
price
soldAmount
# 用戶端版本号大于1.2.3時才會請求商品标簽清單
tagList @skipBy("greaterThan(clientVersion,'1.2.3')")
{
text
icon
}
}
}
總結
相比于将計算邏輯交由其他子產品系統,通過查詢指令定義計算的優勢如下:
- 快速響應:修改查詢dsl即可,即時生效,無需編碼、部署;
- 配置簡單:指令命名和語義簡單,基于結構化的查詢dsl使得計算表意更加友善明确;
- 性能優勢:通過查詢指令在GraphQL引擎層面完成資料的加工排程,不用等待整個查詢結束,且盡可能減少與用戶端的互動次數;
- 解耦業務計算行為和領域資料定義:查詢通過指令對要擷取的資料和進行的加工行為進行直覺的表示,Schema專注于領域資料治理。
在業務實踐中,查詢指令集可以輕易的覆寫80%以上的計算需求,很大程度上減少了業務方因為業務定制邏輯産生的寫死解析計算工作。尤其是當産品需求沒有過于定制的複雜邏輯或者産品邏輯微調時,隻需配置查詢語句即可滿足資料和計算需求,不必在編碼上線,實作了業務的快速疊代。
後記&感悟
重視提供能力
資料平台經常會同時對接很多産品需求,該因素決定了平台如果隻是作為提供特定資料的部門存在,将會耗費大量時間精力進行業務的了解和對接。
建設者應關注到業務疊代時擷取預期資料時遇到的問題,并将這些問題及其處理方案進行歸納,抽象為一種通用的能力提供給平台使用者。相比于提供可複用的資料,有時候提供可複用的能力對資料平台更加重要。
将問題進行合理的抽象并實作為業務可用的通用能力,将能有效減少團隊對接具體業務的工作量。
二八原則
将問題範圍内80%工作的效率提升80%即是很有價值的提升,不必要求平台100%滿足業務方的資料和能力需求。一味追求大而全可能導緻過于複雜的系統設計、使得平台的了解和使用成本更加高昂,團隊也可能要付出遠超20%的時間成本去實作維護“剩下20%的能力”。
平台應該有明确的能力邊界,在嘗試對平台做能力拓展之前應該進行審慎地分析評估。能力擴充經常意味着資料結構的變化和維護成本的增加。
忠于業務
不同于學術研究,工程領域項目的建設往往始于一定的業務背景,平台的價值和意義最終都要回歸到具體的業務問題上進行評估。
參考資料
- [1] https://spec.graphql.org
- [2] https://tech.meituan.com/2021/05/06/bff-graphql.html
- [3] https://www.infoq.cn/article/uqQ20tkA6eELUQec4o97
- [4] https://www.graphql-tools.com/docs/schema-directives
- [5] https://www.graphql-java.com/documentation/v17/instrumentation
- [6] https://www.graphql-java.com/blog/threads
- [7] https://github.com/graphql-calculator/graphql-calculator