Bryan Kane講述了Coursera是如何在他們的生產環境使用GraphQL的。以下內容翻譯自作者的博客,查看原文:Coursera’s journey to GraphQL。
在Coursera,前端開發人員非常喜歡GraphQL的靈活性、類型安全和社區支持,但后端開發人員卻不怎么直接接觸GraphQL。
在過去的一年,我們開發了一些工具將REST API轉成GraphQL,這樣后端開發人員就可以繼續開發他們熟悉的API,而前端開發人員可以通過GraphQL訪問他們想要的數據。
在這篇文章里,我們將介紹我們的GraphQL之旅以及在這一過程中經歷的成功與失敗。
初始調研
Coursera的REST API都是基于資源的,比如課程API、導師API、年級API等。這些API的開發和測試都很容易,而且在后端提供了非常好的關注點分離。不過,隨著產品規模的增長,API的數量也在增長,我們開始面臨一系列問題,如性能問題、文檔問題和易用性問題。我們發現很多頁面需要四到五個網絡來回才能獲取到必要的數據。
那個時候,我們有超過1000個不同的REST端點(現在則更多),從REST到GraphQL的遷移成本是巨大的。所有后端的服務間通信都使用了REST API,而且后端服務為前端和其他后端服務暴露出來的是同一套API。我們有三種客戶端(Web、iOS和Android)。經過調研,我們找到了一種可以采用GraphQL的方案——我們決定在REST API之上增加一個GraphQL代理層。這種方式已經很常見了,并有,所以這里就不再詳述。
在生產環境使用GraphQL
我們先是構建了一個新的GraphQL處理器,然后在生產環境啟動了一個GraphQL服務器用于向REST端點發起調用,并將數據展示在演示頁面上。經過幾天的測試,我們確定這個方案是可行的。
短暫的成功
我們從這個項目中學到了一個教訓,就是不要高興得太早。
GraphQL服務器在頭幾天很穩定,但在我們向團隊演示數據頁面那天,所有的GraphQL查詢都失敗了。這個讓我們有點措手不及,因為從上次確認這個方案可行之后,就沒有動過GraphQL服務器。
后來我們發現,下游的課程目錄服務為了修復一個不相關的bug回滾到了前一個版本,導致GraphQL服務中的schema出現不一致。我們很快修復了schema問題,但我們也意識到,當GraphQL的schema規模增長到1000個并由50多個不同的服務來支撐的時候,要保持一切都同步是不可能的事情。在微服務架構里,如果有多個事實來源(source of truth),那么出現不同步是遲早的事。
自動化流程
于是我們試圖尋找如何能夠實現單個事實來源的解決方案——我們完全可以將REST API作為事實來源,因為我們的GraphQL schema就是基于這些API定義的。所以,我們需要自動化、決策性地構建我們的GraphQL層,體現出當前架構里正在運行的東西,而不是我們的臆想。
幸運的是,我們的REST框架為我們提供了所需的一切:
每個服務為我們提供動態的REST資源清單。對于每一種資源,我們可以檢查它們的端點和參數(比如,課程可以通過id獲取到,或者通過導師查詢到)。我們可以收到由我們的Courier Schema Language為每個模型定義的Pegasus Schema。我們在GraphQL服務器上設置了一個任務,每五分鐘ping一次下游的服務,獲取所有必要的信息,然后在Pegasus Schema和GraphQL類型之間構建一對一的轉換層。
接下來,我們使用之前開發的處理器邏輯在GraphQL查詢和REST請求之間建立映射,得到一個全功能的GraphQL服務器,其更新速度的落后時間不會超過五分鐘。
關聯資源
我們使用GraphQL最主要的原因之一就是希望能夠為頁面一次性獲取到必要的數據。不過,我們最初的方案只提供了REST API到GraphQL之間一對一的映射。如果不將資源關聯起來,我們仍然需要進行多次GraphQL查詢。盡管開發者體驗得到了提升,但在性能方面并沒有獲得實際的好處。
我們的REST API都是一個個孤島,但在使用了GraphQL之后,模型和資源需要對彼此有所了解,因為它們之間存在必要的關聯。
資源之間并不會自動構建鏈接,所以我們定義了一個簡單的注解,開發人員可以將它加在資源上面,用于指定資源之間的關系。例如,一個課程資源需要有一個導師字段,表示教授該課程的導師是誰。我們可以使用課程里的導師ID獲取導師信息。我們稱之為“前向關系”,因為我們知道使用ID可以獲得哪些導師的信息。
courseAPI.addRelation( "instructors" -> ReverseRelation( resourceName = "instructors.v1", finderName = "byCourseId", arguments = Map("courseId" -> "$id", "version" -> "$version"))如果我們想從一個資源跳到另一個資源,但又沒有顯式指定鏈接,那么可以使用反向查找。比如,為了找出某個用戶的某一門課程的注冊信息,我們可以在userEnrollments.v1資源上調用byCourseId,這樣就可以返回某個用戶在某門課程上的注冊信息。
有了這些鏈接,Coursera的所有數據和資源就形成了一個網絡。
結論
我們在Coursera的生產環境運行GraphQL服務器超過六個月的時間,雖然道路仍然崎嶇,但GraphQL為我們提供的幫助無所不在。開發人員操作數據變得更加容易,GraphQL提供的類型安全特性也讓我們的網站變得更可靠,使用GraphQL加載數據也更快。