你好Wunderfans!如果您喜欢我们的内容,请订阅我们的时事通讯甚至更好,加入我们的Discord,千万不要错过任何博客文章或故事!希望您喜欢我们的最新作品和愉快的编码!

Analyzing public GraphQL APIs 是一系列博客文章,用于学习大型公共 GraphQL 实现,从流行的流媒体平台 Twitch.tv 开始。

我们通常假设 GraphQL 就是 GraphQL。对于 REST,它实际上是什么存在很多混淆。构建一个 REST API,你得到的第一个响应是有人说这不是真正的 REST,而只是基于 HTTP 的 JSON,等等......

但这真的是 REST 独有的吗? GraphQL 真的只有一种方法吗?

我查看了许多您熟悉的公司的公开可用的 GraphQL API,并分析了他们如何“执行 GraphQL”。我很快意识到每个人的做法都有些不同。通过这一系列文章,我想从大型 GraphQL 生产部署中提取好的和坏的模式。

在本系列的最后,我们将以白皮书结束,总结有关如何在生产中运行 GraphQL 的所有最佳实践。确保注册我们的白皮书早期访问列表。我们将在本系列的下一篇文章中及时通知您,并在发布后向您发送WhitePaper。

我没有使用任何特殊设备来做到这一点。您可以将首选浏览器与浏览器开发工具一起使用。

让我们深入了解第一个候选人:Twitch.tv

分析 Twitch.tv 的 GraphQL API

您注意到的第一件事是 twitch 在子域https://gql.twitch.tv/gql上托管他们的 GraphQL API。查看 URL 模式和标头,似乎 twitch 没有对其 API 进行版本控制。

如果您查看 Chrome Devtools 或类似工具,您会注意到对于网站上的每个新“路由”,都会向 gql 子域发出多个请求。就我而言,我可以在网站的初始负载上计算 12 个请求。

有趣的是,这些请求是按顺序排列的。从第一个在 313 毫秒开始,然后是 1.27 秒、1.5 秒、2.15 秒、...,最后一个在 4.33 秒。 GraphQL 的承诺之一是解决瀑布问题。但是,这仅适用于网站所需的所有数据都在单个 GraphQL 操作中可用的情况。

在 twitch 的情况下,我们统计了 12 个请求,但我们还没有达到操作级别。 Twitch 批量请求,但我们将在一分钟内讨论。

我注意到 twitch API 的另一个问题。它对所有请求使用 HTTP/1.1,而不是 HTTP/2。为什么会出现问题? HTTP/2 通过单个 TCP 连接多路复用多个请求,HTTP/1.1 没有。如果您查看 Chrome DevTools 中的时间安排,您可以看到这一点。大多数请求可以(重新)使用现有的 TCP 连接,而其他请求则启动一个新连接。大多数请求有大约 300 毫秒的延迟,而具有连接初始化和 TLS 握手的请求大约有 430 毫秒。

现在让我们仔细看看请求本身。 Twitch 使用 HTTP POST 发送 GraphQL 查询。他们首选的响应内容编码是 gzip,他们不支持 brotli。

如果您没有登录,客户端会发送 Header “Authorization: undefined”,这看起来像是前端故障。请求的 Content-Type 是“text/plain”,尽管负载是 JSON。

他们的一些请求是带有 JSON 对象的单个 GraphQL 请求。其他人正在使用批处理机制,这意味着他们将多个操作作为数组发送。响应也以数组的形式返回,因此客户端将所有批处理操作匹配到相同的响应索引。

这是此类批处理请求的示例:

[
  {
    "operationName": "ConnectAdIdentityMutation",
    "variables": {
      "input": {
        "targetDeviceID": "2a38ce069ff87bd4"
      }
    },
    "extensions": {
      "persistedQuery": {
        "version": 1,
        "sha256Hash": "aeb02ffde95392868a9da662631090526b891a2972620e6b6393873a39111564"
      }
    }
  },
  {
    "operationName": "VideoPreviewOverlay",
    "variables": {
      "login": "dason"
    },
    "extensions": {
      "persistedQuery": {
        "version": 1,
        "sha256Hash": "3006e77e51b128d838fa4e835723ca4dc9a05c5efd4466c1085215c6e437e65c"
      }
    }
  }
]

进入全屏模式 退出全屏模式

计算初始网站负载的所有 GraphQL 操作,我总共得到 74 个操作。

以下是按出现顺序列出的所有操作:

Single 1 (1.2kb Response gzip)
    PlaybackAccessToken_Template
Batch 1 (5.9kb Response gzip)
    Consent
    Ads_Components_AdManager_User
    Prime_PrimeOffers_CurrentUser
    TopNav_CurrentUser
    PersonalSections
    PersonalSections (different arguments)
    SignupPromptCategory
    ChannelShell
    ChannelVideoLength
    UseLive
    ActiveWatchParty
    UseViewCount
    UseHosting
    DropCurrentSessionContext
    VideoPreviewOverlay
    VideoAdBanner
    ExtensionsOverlay
    MatureGateOverlayBroadcaster
    VideoPlayer_AgeGateOverlayBroadcaster
    CountessData
    VideoPlayer_VideoSourceManager
    StreamTagsTrackingChannel
    ComscoreStreamingQuery
    StreamRefetchManager
    AdRequestHandling
    NielsenContentMetadata
    ExtensionsForChannel
    ExtensionsUIContext_ChannelID
    PlayerTrackingContextQuery
    VideoPlayerStreamMetadata
Batch 2 (0.7kb Response gzip)
    WatchTrackQuery
    VideoPlayerStatusOverlayChannel
Batch 3 (20.4 Response gzip)
    ChatRestrictions
    MessageBuffer_Channel
    PollsEnabled
    CommunityPointsRewardRedemptionContext
    ChannelPointsPredictionContext
    ChannelPointsPredictionBadges
    ChannelPointsContext
    ChannelPointsGlobalContext
    ChatRoomState
    Chat_ChannelData
    BitsConfigContext_Global
    BitsConfigContext_Channel
    StreamRefetchManager
    ExtensionsForChannel
Batch 4 (0.5kb Response gzip)
    RadioCurrentlyPlaying
Batch 5 (15.7kb Response gzip)
    ChannelPollContext_GetViewablePoll
    AvailableEmotesForChannel
    TrackingManager_RequestInfo
    Prime_PrimeOffers_PrimeOfferIds_Eligibility
    ChatList_Badges
    ChatInput
    VideoPlayerPixelAnalyticsUrls
    VideoAdRequestDecline
Batch 6 (2kb Response gzip)
    ActiveWatchParty
    UseLive
    RealtimeStreamTagList
    StreamMetadata
    UseLiveBroadcast
Batch 7 (1.1kb Response gzip)
    ChannelRoot_AboutPanel
    GetHypeTrainExecution
    DropsHighlightService_AvailableDrops
    CrowdChantChannelEligibility
Batch 8 (1.5kb Response gzip)
    ChannelPage_SubscribeButton_User
    ConnectAdIdentityMutation
Batch 9 (1.0kb Response gzip)
    RealtimeStreamTagList
    RadioCurrentlyPlaying
    ChannelPage_SubscribeButton_User
    ReportMenuItem
Batch 10 (1.3kb Response gzip)
    AvailableEmotesForChannel
    EmotePicker_EmotePicker_UserSubscriptionProducts
Batch 11 (11.7kb Response gzip)
    ChannelLeaderboards

进入全屏模式 退出全屏模式

所有响应都以 63kb gzip 压缩。

请注意,所有这些请求都是 HTTP POST,因此不使用任何缓存控制标头。批处理请求使用分块传输编码。

但是,在后续路由上,似乎发生了一些客户端缓存。如果我将路由更改为另一个通道,我只能数 69 个 GraphQL 操作。

我可以做的另一个观察是 twitch 使用 APQ,即自动持久查询。在第一次请求时,客户端将完整的查询发送到服务器。服务器然后使用响应对象上的“扩展”字段来告诉客户端持久操作哈希。随后的客户端请求将忽略查询有效负载,而只发送持久操作的哈希。这为后续请求节省了带宽。

查看批处理请求,似乎操作的“注册”发生在构建时。所以没有初始注册步骤。客户端仅使用 JSON 请求中的扩展字段发送操作名称以及查询哈希。 (请参阅上面的示例请求)

接下来,我尝试使用 Postman 与 GraphQL Endpoint 对话。

我得到的第一个响应是 400,错误请求。

{
    "error": "Bad Request",
    "status": 400,
    "message": "The \"Client-ID\" header is missing from the request."
}

进入全屏模式 退出全屏模式

我已经从 Chrome Devtools 复制粘贴了 Client-ID 来解决“问题”。

然后我想探索他们的模式。不幸的是,我无法使用自省查询,它似乎被静默地阻止了。

但是,您仍然可以使用流行的 graphql-js 库漏洞从他们的 API 中轻松提取模式。

如果您发送以下查询:

query Query {
    contextUser {
        id
    }
}

进入全屏模式 退出全屏模式

你会得到这样的回应:

{
    "errors": [
        {
            "message": "Cannot query field \"contextUser\" on type \"Query\". Did you mean \"currentUser\"?",
            "locations": [
                {
                    "line": 2,
                    "column": 5
                }
            ]
        }
    ]
}

进入全屏模式 退出全屏模式

使用这些建议,我们能够重建 Schema。不过,我真的不认为这是一个安全风险。他们将所有 GraphQL 查询存储在客户端中,并且他们的 API 是公开的。

最后,我试图弄清楚他们的聊天是如何工作的,以及他们是否也在使用 GraphQL 订阅。将 Chrome 开发工具视图切换到“WS”(WebSocket)向我们展示了两个 WebSocket 连接。

一个托管在 URL wss://pubsub-edge.twitch.tv/v1 上。它似乎正在使用版本控制,或者至少他们希望对这个 API 进行版本控制。查看客户端和服务器之间来回传递的消息,我可以说通信协议不是 GraphQL。通过此连接交换的信息主要围绕视频播放、服务器时间和观看次数,因此它使播放器信息保持同步。

示例消息:

{
    "data": {
        "message": "{\"type\":\"viewcount\",\"server_time\":1634212649.543356,\"viewers\":1574}",
        "topic": "video-playback-by-id.31239503",
        "type": "MESSAGE"
    }
}

进入全屏模式 退出全屏模式

第二个 WebSocket 连接连接到这个 URL:wss://irc-ws.chat.twitch.tv/ IRC 代表“Internet Relay Chat”。我只能假设这个 WebSocket 连接是一个连接到 IRC 服务器的桥梁,该服务器托管所有用于 twitch 的聊天。该协议也不是 GraphQL。这是一个示例消息:

@badge-info=;badges=;client-nonce=9989568f3c0ac4c1376b3d2394c5421e;color=;display-name=Kawazaki32;emotes=;flags=;id=282886fb-7321-46a7-9c7c-6fd994777244;mod=0;room-id=57292293;subscriber=0;tmi-sent-ts=1634212378678;turbo=0;user-id=711847782;user-type= :kawazaki32!kawazaki32@kawazaki32.tmi.twitch.tv PRIVMSG #ratirl :KEKW

进入全屏模式 退出全屏模式

讨论

让我们从最让我惊讶的事情开始。

HTTP 1.1 与 HTTP2 - GraphQL 请求批处理

如果您需要运行 70 多个 GraphQL 操作,很明显,当每个频道可能有数十万甚至数百万观众时,您必须实施某种优化来处理负载。

批处理可以通过不同的方式实现。一种批处理方式利用了 HTTP 协议,但在应用程序层本身也可以进行批处理。

批处理的好处是可以减少 HTTP 请求的数量。在 twitch 的情况下,他们通过 12 个 HTTP 请求对 70 多个操作进行批处理。如果没有批处理,瀑布可能会更加极端。所以,减少Requests的数量是一个很好的解决方案。

但是,应用层中的批处理也有其缺点。如果将 20 个操作批处理到一个请求中,则始终必须等待所有操作解决,然后才能将响应的第一个字节发送到客户端。如果单个解析器很慢或超时,我假设有超时,所有其他操作必须等待超时,直到可以将响应传递给客户端。

另一个缺点是批处理请求几乎总是会破坏 HTTP 缓存的可能性。由于 twitch 的 API 使用 HTTP POST 来处理 READ(查询)请求,但这个选项已经不存在了。

此外,批处理还可能导致感知用户体验变慢。客户端可以非常快速地解析和处理一个小的响应。具有 20+ kb 压缩 JSON 的大型响应需要更长的时间来解析,导致处理时间更长,直到数据可以在 UI 中呈现。

因此,批处理可以减少网络延迟,但它不是免费的。

另一种批处理方式使用 HTTP/2。这是一种非常优雅的方式,几乎看不见。

HTTP/2 允许浏览器通过同一个 TCP 连接发送数百个单独的 HTTP 请求。此外,该协议还实现了 Header Compression,这意味着客户端和服务器除了一些众所周知的术语之外,还可以构建一个单词字典,以显着减小 Headers 的大小。

这意味着,如果您将 HTTP/2 用于您的 API,那么“在应用程序层进行批处理”并没有真正的好处。

实际情况恰恰相反,HTTP/2 上的“批处理”比 HTTP/1.1 应用层批处理具有很大的优势。

首先,您不必等待所有请求完成或超时。每个单独的请求都可以返回一小部分所需数据,然后客户端可以立即呈现这些数据。

其次,通过 HTTP GET 服务 READ 请求允许一些额外的优化。您可以使用 Cache-Control Headers 以及 ETags。让我们在下一节讨论这些。

HTTP POST,读取请求的错误方式

Twitch 正在通过 HTTP/1.1 POST 发送他们所有的 GraphQL 请求。我调查了有效负载,发现许多请求正在加载使用当前通道作为变量的公共数据。对于所有用户,这些数据似乎总是相同的。

在数百万用户正在观看游戏的高流量场景中,我假设成千上万的观看者将不断离开并加入同一个频道。使用 HTTP POST 并且没有 Cache-Control 或 ETag 标头,所有这些请求都将到达源服务器。根据后端的复杂性,这实际上可以工作,例如带有 REST API 和内存数据库。

但是,这些 POST 请求会到达源服务器,然后源服务器会执行持久化的 GraphQL 操作。这只能与数千台服务器一起使用,并结合使用数据加载器模式和应用程序端缓存的明确定义的解析器架构,例如使用 Redis。

我查看了响应时间,它们很快就回来了!因此,twitch 工程师必须做一些非常好的事情才能以如此低的延迟处理这种负载。

假设 twitch 使用 HTTP GET 请求通过 HTTP/2 进行查询。即使 MaxAge 只有 1 秒,我们也可以使用像 Cloudflare 这样的 CDN,它可以将 50k“通道加入”变成一个请求。减少 GraphQL 源的 50k RPS 可以显着降低成本,我们只是在谈论单个 twitch 频道。

然而,这还不是故事的结局。如果我们将 ETags 添加到我们的环境中,我们可以进一步减少负载。使用 ETags,浏览器可以发送一个“If-None-Match”标头,其中包含从先前的网络请求中接收到的值。如果响应没有改变,因此 ETag 也没有改变,服务器只返回一个没有正文的 304 Not Modified 响应。

因此,如果在通道之间跳转时没有太大变化,我们可以保存每个通道切换的 60kb gzip 压缩 JSON 中的大部分。

请记住,这只有在我们不在应用层进行批处理时才有可能。批次越大,整个批次的 ETag 不变的可能性就越小。

正如您所了解的,将 HTTP/2 与 GET 用于 READS 可以减少源站的负载以及减少加载网站的带宽。对于那些通过手机或低带宽连接观看抽搐的人来说,这可能会有所不同。

GraphQL 真的能解决瀑布问题吗?

我最讨厌的事情之一是开发人员赞美 GraphQL。其中一项荣耀是 GraphQL 解决了 REST API 的瀑布问题。

我在许多关于 GraphQL 与 REST 的博客文章中都读到,查询语言允许您在一个请求中查询所有数据并以这种方式解决瀑布问题。

然后告诉我为什么工程师决定在 12 个批处理请求中发送 70 个 GraphQL 操作,瀑布流超过 4 秒?难道他们不了解 GraphQL 的功能吗?如果他们仍然陷入与使用 REST API 相同的陷阱,为什么还要使用 GraphQL?

现实情况是,开发网站的可能不是一个由 3 个前端开发人员和 2 个后端开发人员组成的团队。

如果您是构建简单博客的单个开发人员,您可能能够在单个 GraphQL 请求中请求您需要的所有数据。像 Relay 这样的客户可以帮助实现这一目标。

但是,我认为每一个更大(不是全部)的批量 Request 都可以理解为指向康威定律的指针。

网站的不同部分可以由不同的团队实施。每个组件,例如聊天,有一些特定的操作,这些操作是一起批处理的。

显然,这些只是假设,但我想公平一点,而不是仅仅从外部观察来判断它们的实施。

就瀑布问题而言,GraphQL 并没有真正解决 twitch 的问题。也就是说,我不认为这是他们最大的问题。我只是想指出,如果组织结构不允许,就不可能充分利用技术。

如果您想改进应用程序的架构,请先查看组织。

两个团队可能会构建一个两步编译器。团队可能会构建一个包含三个大批量请求的应用程序。如果您想优化应用程序各个部分的通信方式,请首先考虑公司内部的通信。

APQ - 自动持久查询,值得吗?

使用 APQ,GraphQL 操作将存储在服务器上,以减少带宽并提高性能。客户端不发送完整的 Query,只发送已注册 Operation 的 Hash。上面有一个例子。

虽然 APQ 略微减少了请求大小,但我们已经了解到它们对 响应大小没有帮助,就像 ETags 所做的那样。

在服务器端,大多数实现并没有真正优化。他们从字典中查找操作,解析并执行它。该操作不会是预处理或任何东西。

twitch GraphQL API 也允许你发送任意的、非持久化的操作,所以它们没有使用 APQ 作为安全机制。

我个人的看法是 APQ 增加了复杂性并没有太多好处。

如果您已经做到了这一点,为什么不直接将我们的故事发送到您的收件箱?

聊聊

在不修复建议错误的情况下禁用自省

我不想在这篇文章中深入探讨安全性,所以这只是关于禁用自省的简要说明。

一般来说,禁用自省以不允许每个 API 用户探索您的 GraphQL Schema 可能是有意义的。架构可能会泄漏敏感信息。也就是说,某些实现存在问题,例如 graphql-js 参考实现,即使禁用了自省,也会泄漏 Schema 信息。

如果您的实现使用了这些建议,并且您想完全禁用自省,请务必解决此问题。我们将在本文的建议部分讨论解决方案。

你应该使用 GraphQL 订阅进行实时更新吗?

GraphQL 订阅允许您使用查询语言将更新流式传输到客户端。不过,Twitch 并未利用此功能。

在聊天方面,看起来他们在下面使用 IRC。他们可能在查看 GraphQL 之前就已经开始使用它了。用 GraphQL 订阅包装这个实现可能不会增加任何额外的好处。

如果所有流量都由 GraphQL 处理,显然会更干净,但进行切换可能不值得。

要记住的一件事是 twitch 使用 WebSockets 进行实时更新。我在另一篇博文中讨论了这个话题,要点是 WebSockets 是一个糟糕的实时更新解决方案,原因有很多。作为替代方案,我建议使用 HTTP/2 流。

讨论就够了。接下来,我将分享一些关于如何使用 twitch API 构建生产级 GraphQL API 的建议。

建议

READ 请求应始终使用 HTTP GET over HTTP/2

READ 请求或 GraphQL 查询应始终使用 HTTP/2 上的 HTTP GET 请求。这几乎解决了我上面描述的所有问题。

有了这个,就不需要进行应用层批处理。

你怎么能做到这一点?

对于您在应用程序中定义的每个 GraphQL 操作,创建一个专用的 JSON API 端点并让您的 API 客户端对查询使用 GET 请求,可以使用查询参数发送变量。

然后,您可以为每个 Endpoint 添加特定的 Cache-Control 配置和处理 ETag 的中间件,以在不牺牲良好用户体验的情况下提高单个操作的性能。

您可能会认为这会增加应用程序的复杂性。保持客户端和服务器同步可能很复杂。这不会破坏所有现有的 GraphQL 客户端吗?

是的,它确实增加了复杂性。它不仅破坏了现有的客户端,而且还与您可能听说过的有关 GraphQL 的所有内容背道而驰。

然而,充分利用 HTTP 是非常有意义的,它允许浏览器完成它们的工作以及代理和 CDN。他们都了解 Cache-Control Headers 和 ETags,让他们做他们的工作吧!

但是,请不要增加额外的复杂性。至少,我们是这么想的,所以我们解决了这个问题,解决方案太简单了。

首先,定义应用程序所需的所有操作,就像 twitch 工程师所做的那样。 WunderGraph 然后生成一个 GraphQL 网关,该网关公开一个安全的 JSON RPC API。此外,我们以任何语言生成类型安全的 API 客户端/SDK,以便您可以轻松“调用”到您的预定义操作。

此设置使用 HTTP/2 并利用浏览器、CDN 和代理的所有功能。因为我们不是在网上谈论 GraphQL,所以它也提高了安全性。内省泄露?不可能的。使用复杂查询的拒绝服务攻击?不可能的。

您仍在定义 GraphQL 操作,它仍然感觉像 GraphQL,只是没有通过 POST 请求发送查询。

APQ < 编译操作

自动持久化查询是提高性能的好主意,但是,它们并没有经过深思熟虑。

在 hashmap 中查找持久化的 Operation 以解析并执行它们仍然意味着您正在“解释”它的所有缺点。

使用 WunderGraph,我们将走一条不同的路线。当您定义一个操作时,我们实际上是在运行时对其进行验证并将其编译为极其高效的代码。

在 WunderGraph 中执行预定义的操作时,我们所做的就是插入变量,然后执行操作树。运行时不会发生解析和验证。

WunderGraph 就像一个带有准备好的语句的数据库,它只是不使用表作为存储,而是与 API 对话。

这样,我们在运行时几乎不会增加任何开销。相反,使用 ETag 和缓存中间件,我们可以轻松加速您的 GraphQL API。

通过 HTTP/2 流订阅

我们在上面链接了另一篇文章,概述了 WebSockets 的问题。简而言之,WebSocket 是有状态的,使身份验证变得复杂,并且每个套接字都需要一个额外的 TCP 连接。

为了为您解决这个问题,WunderGraph 客户端和服务器都通过 HTTP/2 实现订阅和实时流。

不过,在与您的来源交谈时,我们完全兼容使用 WebSockets 的“标准”GraphQL 订阅实现。我们只是将这些隐藏在我们的安全 JSON RPC API 后面,通过 HTTP/2 流式传输对客户端的响应。

这样,您的订阅将保持无状态,并为您正确处理身份验证。另一个你不必解决的问题。

结论

我希望这个新系列可以帮助您看透美化的博客文章,并且您会意识到现实看起来不同。

我认为在生产中运行 GraphQL 需要一个标准。如果你关注这个系列,你会发现所有大玩家的做法都不一样。如果每家公司都试图找到自己的方式来构建他们的 API 基础设施,那真的是低效的。

这就是我们在这里的原因!我们正在建立这个标准。我们可以为您提供一个工具,让您可以利用在本系列中发现的所有最佳实践。问问自己,解决所有这些问题是否是您业务的核心领域。您的回答应该是“否”,否则您可能是 API 或开发工具供应商。

如果您在 GraphQL 实施方面需要帮助,请与我们联系!

如果您喜欢这个新系列,请务必注册白皮书或在Twitter和Discord 上关注我们!随意提出另一个我们应该分析的 API。

顺便说一句,如果您在 twitch 工作,我们很乐意与您交谈并获得有关 GraphQL API 内部的更多见解。

Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐