[譯] Coursera 的 GraphQL 之路

Bamboo發表於2017-09-13

Coursera 的 GraphQL 之路

將 GraphQL 新增至 REST + 微服務的後端中

Coursera 的客戶端開發人員喜歡 GraphQL 的靈活性、型別安全性以及社群的支援,這些已經。但是,我們沒有談過多少我們的後端開發者們對於 GraphQL 的感受,這是因為實際上他們大多數並不需要為 GraphQL 考慮太多。

過去的一年中,我們構建了將所有 REST API 動態轉換為 GraphQL 的工具。這使得後端開發者可以繼續編寫他們熟悉的 API,同時客戶端開發者也可以通過 GraphQL 訪問所有資料。

本文中將介紹我們的 GraphQL 之旅,特別是過程中的成功及失敗。

初步調查

Coursera 的 REST API 是基於資源構建的(即課程 API、教師 API、課程成績 API 等)。這樣使得開發和測試都很容易,並且在後端很好地實現了關注分離。然而,隨著產品規模擴大以及 API 數量增長,我們開始面臨效能、文件以及易用性等問題。在許多頁面上,我們發現需要四到五次與伺服器的往返來獲取所有我們需要渲染的資料。

還記得 Facebook 首次推出 GraphQL 時我們團隊非常興奮,因為我們幾乎立刻就意識到 GraphQL 可以解決我們的諸多問題,例如在一次往返獲取所有資料,併為 API 提供結構化的文件等。雖然我們想馬上停止使用 REST 並開始編寫 GraphQL,但事情並非如此簡單,因為:

  • 當時,Coursera 有超過 1000 個不同的 REST 端點(現在更多),即使我們想完全停止使用 REST,GraphQL 的遷移成本將是極大的。

  • 我們所有的後端服務都使用 REST API 進行服務間通訊,所以經常會有給後端服務以及前端提供相同 API 的情況。

  • 我們有三個不同的客戶端(web、iOS 以及 Android),希望能靈活緩慢地推進。

在一些調查之後,我們發現了一個引入 GraphQL 的好方法,那就是在 REST API 上新增 GraphQL 的代理層。這個方法實際上也很常見,並且詳細的文件驗證過了,所以這裡我就不深入展開了。

生產環境上使用 GraphQL

包裝 REST API 是個非常簡單的過程,我們針對下游 REST 呼叫通過解析器獲取資料構建了一些實用程式,並寫了一些將現有模型轉為 GraphQL 的規則。

第一步是構建 GraphQL 解析器,然後在生產環境中啟動一個 GraphQL 伺服器,使下游 REST 呼叫到源端點。一旦完成了這項工作(用 GraphQL 來驗證一切),我們就會在設定的演示頁面展示資料,幾天之內就可以說 GraphQL 的嘗試成功了。

短暫的慶祝

如果說我從這個專案中學到了一件事,那一定是不要高興太早。

我們的 GraphQL 伺服器完美工作了幾天,但是突然之間,在我們準備給團隊演示之前,每個 GraphQL 查詢都失敗了。我們措手不及,因為自從上次驗證它正常工作以來並沒有對 GraphQL 伺服器進行任何更改。

在調查之後,終於發現由於一個不相關的 bug,下游課程目錄服務回滾到了之前的版本,導致 GraphQL 中構建的模式不同步了。我們可以手動更新並修復演示頁面,但很快我們意識到當我們的 GraphQL 架構如果擴充套件到由超過 50 個不同的服務支援的 1000 個不同的資源之後,想保持所有資料都更新到最新幾乎是不可能的。如果在微服務體系中你有多於一個資料來源,那麼問題在於何時,而不是他們是否不同步。

自動化流程

所以我們回到了白板上,試圖找出一個清晰的解決方案獲得真實資料來源。將 REST API 視為真實資料來源是有道理的,因為 GraphQL 是基於它們構建的。為此,我們需要自動地確定性地構建 GraphQL 層,以反映當前體系中正在執行的內容,而不是我們認為正在執行的。

幸運的是(也許算有遠見),我們的 REST 框架給我們提供了構建這個自動化層需要的一切:

  • 基礎架構中的每一個服務都可以動態地提供正在執行的 REST 資源列表。

  • 針對每一個資源,我們可以內省獲取其一系列端點和引數列表(即一個課程可以通過 id 獲取,也可以由講師查詢)

  • 另外,我們接受由 Courier 的模式語言定義的 Pegasus Schemas,用於每個模型返回資料

只要發現不同的部分,我們就需要構建一個 GraphQL 模式,在 GraphQL 伺服器上設定一個任務,每五分鐘對所有下游服務 ping 一次,請求所有資訊。然後,我們就可以在 Pegasus 模式和 GraphQL 型別之間編寫 1:1 的轉化層了。

接下來,我們只需要簡單定義如何將 GraphQL 查詢轉化為 REST 請求,使用以前的解析器中的大部分邏輯,就可以生成功能完整的 GraphQL 伺服器,不再會過期 5 分鐘以上。

關聯資源

我們希望使用 GraphQL 的一個主要原因是在一個往返中獲取某個頁面需要的所有資料。但是一開始我們的方法只能提供 REST API 以及 GraphQL 之間一對一的對映。沒有將資源連線在一起,我們仍然會像使用 REST API 一樣,使用多次 GraphQL 查詢來獲取資料。雖然通過 GraphQL 獲取使用者資料相比使用 REST 來說,開發者體驗有所提升,但如果在獲取更多資料之前必須等待前序查詢返回的話,那麼在效能上沒有實質提升。

我們的每個 REST API 都獨立存活,他們不需要知道其他任何 API 的存在。但是,如果使用 GraphQL,模型和資源確實需要彼此的存在,以及如何連線。

資源之間的連線是不能自動新增的,所以我們定義了一個簡單的標記方法,使得開發者可以新增資源並指定資源之間的關係。例如,我們可以指定一個課程應該有講師欄位,代表教授這門課程的講師。獲取這些講師的時候,需要使用 id 查詢,此時就可以使用課程已經提供的 instructorIds 欄位。我們稱之為「前置關係」,因為我們通過 id 確切知道哪些講師需要獲取。

在想要從一個資源到另一個資源但沒有顯式關聯的情況下,我們新增了反向查詢的支援,也就是獲取一個使用者在一個課程的註冊情況。我們可以在 userEnrollments.v1 資源上通過 byCourseId 進行查詢,就可以返回在指定的課程中指定使用者的註冊資料。

我們開發的語法看起來像這樣:

courseAPI.addRelation(
  "instructors" -> ReverseRelation(
    resourceName = "instructors.v1",
    finderName = "byCourseId",
    arguments = Map("courseId" -> "$id", "version" -> "$version"))複製程式碼

一旦這些關聯到位,我們的 GraphQL 模式就開始彙集在一起了,不再是小量資料碎片,而是整個 Coursera 資料和資源的網路。

結論

我們已經在生產環境中執行 GraphQL 伺服器 6 個月了,這條路有時是顛簸的,但我們切實認識到 GraphQL 帶來的好處。開發人員更容易發現資料及編寫查詢,我們的產品也由於 GraphQL 額外提供的型別安全性更加可靠,使用 GraphQL 獲取資料的頁面載入也更快。

需要重點提出的是,這種遷移並不以開發效率為代價。我們的前端工程師的確需要學習如何使用 GraphQL,但我們並不需要重寫後端 API 或執行復雜的遷移才能享受 GraphQL 帶來的好處。當建立新的應用程式的時候,它就可供開發人員使用了。

總的來說,我們對 GraphQL 為開發人員(最終為使用者)提供的幫助非常滿意,並對 GraphQL 生態的發展充滿期待。

致謝

  • Brennan Saeta,編寫了 Naptime API庫,並幫助 Naptime 編寫了初始的 GraphQL 支援。
  • Oleg Ilyenko,編寫的 Sangria 庫 為我們所有的 GraphQL 工作提供了支柱。如果你正在使用 GraphQL,並正在使用或計劃使用 Scala,那麼你一定要看看 Sangria。
  • Coursera 前端基礎設施團隊提供了幫助將 GraphQL 從測試專案轉移至預備生產環境中。
  • Coursera 的整個工程團隊的耐心以及幫助,我們一起在 GraphQL 層解決了無數 bug 和奇怪的現象。

掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章