馬斯克都不懂的 GraphQL,API 網關又能對其如何理解?

語言: CN / TW / HK

作者,羅澤軒

上個月馬斯克評論 Twitter App 濫用 RPC 後,與一些 Twitter 的技術主管發生了矛盾 —— 直言馬斯克不懂技術。那這個馬斯克都不懂的 GraphQL 到底是什麼?

image (3).png

什麼是 GraphQL?它有多流行?

GraphQL 是一套由 Facebook 在 2015 年發佈的一套面向 API 的查詢操作語言。相比於其他的 API 設計方式,GraphQL 允許客户端根據事先約定的數據結構組建查詢語句,由服務端解析這一語句並只返回所需的內容。這麼一來,GraphQL 在提供豐富性和靈活性的同時,避免了宂餘數據帶來的性能損耗。

GraphQL 的這一特性,讓它在需要跟許多複雜數據對象打交道的應用場景裏大行其道,成為該環境下的不二之選。

2018 年 GraphQL 完成了規範的制定工作,並推出了穩定版本。同年,Facebook 將 GraphQL 項目捐獻給了 Linux 基金會下屬的 GraphQL 基金會。自那以後,GraphQL 已經在許許多多的開源項目和商業機構中落地。到目前為止,市面上已經有了多個 GraphQL 的主流客户端實現。而服務端的實現遍佈各大服務端編程語言,甚至連一些小眾編程語言如 D 和 R 都有對應的實現。

GraphQL 的一些真實場景和挑戰

最為知名的採用 GraphQL 的例子,莫過於 GitHub 的 GraphQL API 了。

在擁抱 GraphQL 之前,GitHub 提供了 REST API 來暴露千千萬萬託管項目所產生的豐富數據。GitHub 的 REST API 是如此的成功,以致於它成為了人們設計 REST API 時競相模仿的典範。

然而隨着數據對象的變多和對象內字段的變大,REST API 開始暴露出越來越多的弊端。在服務端,由於每次調用都會產生大量的數據,GitHub 為了降低成本不得不對調用頻率設置嚴格的限制。

而在開發者這邊,他們則不得不與這一限制做鬥爭。因為雖然單次調用會返回繁多的數據,但是絕大部分都是無用的。開發者要想獲取某一特定的信息,往往需要發起多個查詢,然後編寫許多膠水代碼把查詢結果中有意義的數據拼接成所需的內容。在這一過程中,他們還不得不帶上“調用次數”的鐐銬。

所以 GraphQL 的出現,立刻就讓 GitHub 皈依了。GitHub 成為了 GraphQL 的使者保羅,為萬千開發者傳遞福音。目前 GraphQL API 已經是 GitHub API 的首選。從第一次宣佈對 GraphQL 的支持之後,GitHub 每一年都會發幾篇關於 GraphQL 的文章。為了讓開發者能夠遷移到 GraphQL 上來,GitHub 專門寫了個交互式查詢應用,開發者可以通過這個應用學習怎麼編寫 GraphQL。

然而 GraphQL 並非靈丹妙藥。就在最近,GitHub 廢棄自己 package API 的 GraphQL 實現。許多人也開始熱議 GraphQL 的一些缺點

GraphQL 的許多問題源自於它跟 HTTP 標準的結構差別較大,沒辦法簡單地將 GraphQL 的一些概念映射到諸如 HTTP path/header 這樣的結構中。把 GraphQL 當作普通的 HTTP API 來處理,需要額外的開發工作。如此一來,開發者如果要管理自己的 GraphQL API,就必須採用支持 GraphQL 的 API 網關才行。

APISIX 現在對 GraphQL 的支持

Apache APISIX 是一個動態、實時、高性能的 API 網關,提供負載均衡、動態上游、灰度發佈、精細化路由、限流限速、服務降級、服務熔斷、身份認證、可觀測性等數百項功能。作為 Apache 的頂級項目,APISIX 一直致力於周邊生態的擴展與跟進。

APISIX 目前支持通過 GraphQL 的一些屬性進行動態路由。通過該能力,我們可以只接受特定的 GraphQL 請求,或者讓不同的 GraphQL 轉發到不同的上游。

以下面的 GraphQL 語句為例:

  query getRepo {
      owner {
          name
      }
      repo {
          created
      }
  }

APISIX 會提取 GraphQL 以下三個屬性,用在路由當中:

  • graphql_operation
  • graphql_name
  • graphql_root_fields

在上面的 GraphQL 語句中:

  • graphql_operation 對應 query
  • graphql_name 對應 getRepo
  • graphql_root_fields 對應 ["owner", "repo"]

讓我們來創建一個路由,展示下 APISIX 對 GraphQL 的精細化路由能力。

curl http://127.0.0.1:9180/apisix/admin/routes/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
  {
      "methods": ["POST"],
      "uri": "/graphql",
      "vars": [
          ["graphql_operation", "==", "query"],
          ["graphql_name", "==", "getRepo"],
          ["graphql_root_fields", "has", "owner"]
      ],
      "upstream": {
          "type": "roundrobin",
          "nodes": {
              "127.0.0.1:2022": 1
          }
      }
  }'

接下來使用帶有 GraphQL 語句的請求去訪問:

curl -i -H 'content-type: application/graphql' \
-X POST http://127.0.0.1:9080/graphql -d '
query getRepo {
    owner {
        name
    }
    repo {
        created
    }
}'
HTTP/1.1 200 OK
...

我們可以看到請求到達了上游,這是因為查詢語句匹配了全部三個條件。 反之,如果我們使用不匹配的語句來訪問,比如不包含 owner 字段:

curl -i -H 'content-type: application/graphql' \
-X POST http://127.0.0.1:9080/graphql -d '
query getRepo {
    repo {
        created
    }
}'
HTTP/1.1 404 Not Found
...

則不會匹配對應的路由規則。

接下來,我們可以另外創建一個路由,讓不包含 owner 字段的語句路由到別的上游:

curl http://127.0.0.1:9180/apisix/admin/routes/2 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
  {
      "methods": ["POST"],
      "uri": "/graphql",
      "vars": [
          ["graphql_operation", "==", "query"],
          ["graphql_name", "==", "getRepo"],
          ["graphql_root_fields", "!", "has", "owner"]
      ],
      "upstream": {
          "type": "roundrobin",
          "nodes": {
              "192.168.0.1:2022": 1
          }
      }
  }'
curl -i -H 'content-type: application/graphql' \
-X POST http://127.0.0.1:9080/graphql -d '
query getRepo {
    repo {
        created
    }
}'
HTTP/1.1 200 OK
..

展望 APISIX 未來對 GraphQL 的支持

除了動態路由之外,APISIX 在未來也可能會根據 GraphQL 的具體字段推出更多的操作。比如説,GitHub 的 GraphQL API 有專門一套針對限流的計算公式,我們也可以應用類似的規則來把單個 GraphQL 請求轉成相應的“虛擬調用”次數,來完成 GraphQL 專屬的限流工作。

當然,我們也可以換個思路解決問題。即應用自身還是提供 REST API,由網關在最外層把 GraphQL 請求轉成 REST 請求,把 REST 響應轉成 GraphQL 響應。這種方式提供的 GraphQL API 無需開發專門的插件,就可以完成諸如 RBAC、限流、緩存等功能。

從插件角度來看,它就是個平平無奇的 REST API。從技術角度上講,這個思路並不難實現。畢竟在 2022 年的現在,REST API 也會提供 OpenAPI spec 來作為 schema,無非是 GraphQL schema 和 OpenAPI schema 之間的互轉,外加 GraphQL 特有的字段篩選罷了(當然,我必須承認,我並沒有親自實踐過,或者在一些細節上存在尚待克服的挑戰)。

細心的讀者會發現,這種方式轉換得來的 GraphQL API,每次只能操作一個模型,顯然無法滿足 GraphQL 靈活性的要求,無非是披着 GraphQL 外衣的 REST API。**且慢,我還沒有把話説完呢!**GraphQL 有一個叫 schema stitch 的概念,允許實現者把多個 schema 組合在一起。

舉個例子,現在有兩個 API。一個叫 GetEvent,另一個叫 GetLocation。他們返回的類型分別是 Event 和 Location。

type Event {
  id: string
  location_id: string
}

type Location {
  id: string
  city: string
}

type Query {
    GetEvent(id: string): Event
    GetLocation(id: string): Location
}

我們可以加一個配置,由這兩個 API 組合成新的 API 叫 GetEventWithLocation。新的 API 是這樣的:

type EventWithLocation {
  id: string
  location: Location
}

type Query {
    GetEventWithLocation(id: string): EventWithLocation
}

整體 schema stitch 的過程都由網關來完成。在上面的例子中,網關會把 API 拆分成兩個,先調用 GetEvent 得到 location_id,再調用 GetLocation 得到組合後的數據。

總而言之,通過 REST 轉 GraphQL,每個 REST API 可以變成對應的 GraphQL 模型;再借助 schema stitch,可以把多個模型組合成一個 GraphQL API。

這樣一來,我們就能在現有的 REST API 上構建起豐富靈活的 GraphQL API,且在 REST API 的粒度上完成具體的插件管理。這一設計順帶解決了部分 API 編排的問題。就像上面的例子中,我們把一個 API 的輸出(Event.location_id)作為另一個 API 的輸入(Location.id)。