mirror of
https://github.com/LCTT/TranslateProject.git
synced 2025-01-25 23:11:02 +08:00
PRF
@MjSeven 辛苦了,这篇文章很有价值
This commit is contained in:
parent
2d8ebba9ea
commit
dbd29bb274
@ -10,6 +10,8 @@
|
||||
扩展一个 GraphQL 网站
|
||||
======
|
||||
|
||||
![](https://img.linux.net.cn/data/attachment/album/202111/14/113411shrp6jpp3a8x1cjq.jpg)
|
||||
|
||||
我通常会抽象地总结我为他人所做的工作(出于显而易见的原因),但是我被允许公开谈论一个网站:[Vocal][1] 。我去年为它做了一些 SRE 工作。实际上早在 2 月份,我就在 [GraphQL 悉尼会议上做过一次演讲][2],不过这篇博客推迟了一点才发表。
|
||||
|
||||
Vocal 是一个基于 GraphQL 的网站,它获得了人们的关注,然后就遇到了可扩展性问题,而我是来解决这个问题的。这篇文章会讲述我的工作。显然,如果你正在扩展一个 GraphQL 站点,你会发现这篇文章很有用,但其中大部分内容讲的都是当一个站点获得了足够的流量而出现的必须解决的技术问题。如果你对站点可扩展性有兴趣,你可能想先阅读 [最近我发表的一系列关于可扩展性的文章][3]。
|
||||
@ -71,104 +73,104 @@ Thinkmill 在使用 MongoDB 时遇到了几个可扩展性问题,因此决定
|
||||
|
||||
#### 架构
|
||||
|
||||
V1 是几个 Node.js 应用,运行在 Cloudflare(作为 CDN)背后的单个虚拟专用服务器(VPS)上。避免过度设计作为优先事项,我是这个准则的粉丝。然而,在 V2 开始开发时,很明显,Vocal 已经超越了这个简单的架构。在处理巨大峰值流量时,它没有给 Thinkmill 开发人员提供很多选择,而且它很难在不停机情况下安全部署更新。
|
||||
V1 是几个 Node.js 应用,运行在 Cloudflare(作为 CDN)背后的单个虚拟专用服务器(VPS)上。我喜欢把避免过度工程化作为一个高度优先事项,所以我对这一架构竖起了大拇指。然而,在 V2 开始开发时,很明显,Vocal 已经超越了这个简单的架构。在处理巨大峰值流量时,它没有给 Thinkmill 开发人员提供很多选择,而且它很难在不停机情况下安全部署更新。
|
||||
|
||||
这是 V2 的新架构:
|
||||
|
||||
![Vocal V2 的技术架构,请求从 CDN 进入,然后经过 AWS 的负载均衡。负载均衡将流量分配到两个应用程序 "Platform" 和 "Website"。"Platform" 是一款 Keystone 应用程序,将数据存储在 Redis 和 Postgres 中。][19]
|
||||
![Vocal V2 的技术架构,请求从 CDN 进入,然后经过 AWS 的负载均衡。负载均衡将流量分配到两个应用程序 “Platform” 和 “Website”。“Platform” 是一款 Keystone 应用程序,将数据存储在 Redis 和 Postgres 中。][19]
|
||||
|
||||
基本上就是两个 Node.js 应用程序复制放在负载均衡器后面,非常简单。有些人认为可扩展架构要比这复杂得多,但是我曾经在一些比 Vocal 规模大得多的网站工作过,它们仍然只是在负载均衡器后面复制服务,带有 DB 后端。你仔细想想,如果平台架构需要随着站点的增长而变得越来越复杂,那么它就不是真正“可扩展的”。网站可扩展性主要是关于解决那些破坏可扩展的实现细节。
|
||||
基本上就是两个 Node.js 应用程序复制放在负载均衡器后面,非常简单。有些人认为可扩展架构要比这复杂得多,但是我曾经在一些比 Vocal 规模大几个数量级的网站工作过,它们仍然只是在负载均衡器后面复制服务,带有 DB 后端。你仔细想想,如果平台架构需要随着站点的增长而变得越来越复杂,那么它就不是真正“可扩展的”。网站可扩展性主要是解决那些破坏可扩展的实现细节。
|
||||
|
||||
如果流量增长得足够多,Vocal 的架构可能需要一些补充,但它变得更加复杂的主要原因是新功能。例如,如果(出于某种原因)Vocal 将来需要处理实时地理空间数据,那将是一个与博客文章截然不同的技术,所以我希望对它进行架构上的更改。大型网站架构的复杂性主要来自于复杂的功能。
|
||||
如果流量增长得足够多,Vocal 的架构可能需要一些补充,但它变得更加复杂的主要原因是新功能。例如,如果(出于某种原因)Vocal 将来需要处理实时地理空间数据,那将是一个与博客文章截然不同的技术,所以我预期它会进行架构上的更改。大型网站架构的复杂性主要来自于复杂的功能。
|
||||
|
||||
不知道未来的架构应该是什么样子很正常,所以我总是建议你尽可能从简单开始。修复一个简单架构要比复杂架构更容易,成本也更低。此外,不必要的复杂架构更有可能出现错误,而这些错误将更难调试。
|
||||
|
||||
顺便说一下,Vocal 分成了两个应用程序,但这并不重要。一个常见的扩展错误是,以可扩展的名义过早地将应用分割成更小的服务,但将应用分割在错误的位置,从而导致更多的可扩展性问题。作为一个独立的应用,Vocal 的规模还不错,但它的分割做的也很好。
|
||||
顺便说一下,Vocal 分成了两个应用程序,但这并不重要。一个常见的扩展错误是,以可扩展的名义过早地将应用分割成更小的服务,但将应用分割在错误的位置,从而导致更多的可扩展性问题。作为一个单体应用,Vocal 可以扩展的很好,但它的分割做的也很好。
|
||||
|
||||
#### 基础设施
|
||||
|
||||
Thinkmill 有一些人有 AWS 经验,但它主要是一个开发车间,需要一些比之前的 Vocal 部署更容易上手的东西。我最终在 AWS Fargate 上部署了新的 Vocal,这是一个相对较新的弹性容器服务(ECS)后端。在过去,许多人希望 ECS 作为一个“托管服务运行 Docker 容器”的简单产品,但人们仍然必须构建和管理自己的服务器集群,这让人感到失望。使用 ECS Fargate,AWS 会管理集群。它支持运行带有复制、健康检查、滚动更新、自动缩放和简单警报等基本功能的 Docker 容器。
|
||||
Thinkmill 有一些人有使用 AWS 经验,但它主要是一个开发车间,需要一些比之前的 Vocal 部署更容易上手的东西。我最终在 AWS Fargate 上部署了新的 Vocal,这是弹性容器服务(ECS)的一个相对较新的后端。在过去,许多人希望 ECS 作为一个“托管服务运行 Docker 容器”的简单产品,但人们仍然必须构建和管理自己的服务器集群,这让人感到失望。使用 ECS Fargate,AWS 就可以管理集群了。它支持运行带有复制、健康检查、滚动更新、自动缩放和简单警报等基本功能的 Docker 容器。
|
||||
|
||||
一个好的替代方案是平台即服务(PaaS),比如 App Engine 或 Heroku。Thinkmill 已经在简单的项目中使用它们,但通常在其他项目中需要更大的灵活性。有很多大型网站运行在 PaaS 上,但 Vocal 的规模决定了使用自定义云部署是有经济意义的。
|
||||
一个好的替代方案是平台即服务(PaaS),比如 App Engine 或 Heroku。Thinkmill 已经在简单的项目中使用它们,但通常在其他项目中需要更大的灵活性。在 PaaS 上运行的网站规模要大得多,但 Vocal 的规模决定了使用自定义云部署是有经济意义的。
|
||||
|
||||
另一个明显的替代方案是 Kubernetes。Kubernetes 比 ECS Fargate 拥有更多的功能,但它的成本要高得多 -- 无论是资源开销还是维护所需的人员(例如定期节点升级)。一般来说,我不向任何没有专门 DevOps 员工的地方推荐 Kubernetes。Fargate 具有 Vocal 需要的功能,使得 Thinkmill 和 Creatd 专心于网站改进,而不是忙碌于搭建基础设施。
|
||||
另一个明显的替代方案是 Kubernetes。Kubernetes 比 ECS Fargate 拥有更多的功能,但它的成本要高得多 —— 无论是资源开销还是维护(例如定期节点升级)所需的人员。一般来说,我不向任何没有专门 DevOps 员工的地方推荐 Kubernetes。Fargate 具有 Vocal 需要的功能,使得 Thinkmill 和 Creatd 能专心于网站改进,而不是忙碌于搭建基础设施。
|
||||
|
||||
另一种选择是“无服务器”功能产品,例如 AWS Lambda 或 Google 云。它们非常适合处理流量很低或很不规则的服务,但是 ECS Fargate 的自动伸缩功能足以满足 Vocal 的后端。这些产品的另一个优点是,它们允许开发人员在云环境中部署东西,但无需了解很多关于云环境的知识。权衡的结果是,无服务器产品与开发过程、测试以及调试过程紧密耦合。Thinkmill 内部已经有足够的 AWS 专业知识来管理 Fargate 的部署,任何知道如何制作 Node.js Express Hello World 应用程序的开发人员都可以在 Vocal 上工作,而无需了解无服务器功能或 Fargate 的知识。
|
||||
另一种选择是“无服务器”功能产品,例如 AWS Lambda 或 Google 云。它们非常适合处理流量很低或很不规则的服务,但是 ECS Fargate 的自动伸缩功能足以满足 Vocal 的后端。这些产品的另一个优点是,它们允许开发人员在云环境中部署东西,但无需了解很多关于云环境的知识。权衡的结果是,无服务器产品与开发过程、测试以及调试过程紧密耦合。Thinkmill 内部已经有足够的 AWS 专业知识来管理 Fargate 的部署,任何知道如何开发 Node.js 简单的 Hello World 应用程序的开发人员都可以在 Vocal 上工作,而无需了解无服务器功能或 Fargate 的知识。
|
||||
|
||||
ECS Fargate 的一个明显缺点是供应商唯一。但是,避免供应商唯一是一种权衡,就像避免停机一样。如果你担心迁移,那么花费在平台独立性比迁移上更多是没有意义的。在 Vocal 中,依赖于 Fargate 的代码总量小于 500 行 [Terraform][23]。最重要的是 Vocal 应用程序代码本身与平台无关,它可以在普通开发人员的机器上运行,然后打包成一个 Docker 容器,几乎可以运行在任何可以运行 Docker 容器的地方,包括 ECS Fargate。
|
||||
ECS Fargate 的一个明显缺点是供应商锁定。但是,避免供应商锁定是一种权衡,就像避免停机一样。如果你担心迁移,那么在平台独立性花费比迁移上更多的钱是没有意义的。在 Vocal 中,依赖于 Fargate 的代码总量小于 500 行 [Terraform][23]。最重要的是 Vocal 应用程序代码本身与平台无关,它可以在普通开发人员的机器上运行,然后打包成一个 Docker 容器,几乎可以运行在任何可以运行 Docker 容器的地方,包括 ECS Fargate。
|
||||
|
||||
Fargate 的另一个缺点是设置复杂。与 AWS 中的大多数东西一样,它处于 VPC、子网、IAM 策略世界中。幸运的是,这类东西是静态的(不像服务器集群一样需要维护)。
|
||||
Fargate 的另一个缺点是设置复杂。与 AWS 中的大多数东西一样,它处于一个 VPC、子网、IAM 策略世界中。幸运的是,这类东西是相对静态的(不像服务器集群一样需要维护)。
|
||||
|
||||
### 制作一个可扩展的应用程序
|
||||
|
||||
如果你想轻松地运行一个大规模的应用程序,需要做一大堆正确的事情。如果你遵循[应用程序设计的十二个守则][22],事情会变得容易,所以我不会在这里重复。
|
||||
如果你想轻松地运行一个大规模的应用程序,需要做一大堆正确的事情。如果你遵循 <ruby>[应用程序设计的十二个守则][22]<rt>the Twelve-Factor App design</rt></ruby>,事情会变得容易,所以我不会在这里赘述。
|
||||
|
||||
如果员工无法大规模操作一个“可扩展”的系统,那么构建“可扩展”系统就毫无意义 -- 就像在独轮车上安装喷气发动机一样。使 Vocal 可扩展的一个重要部分是将 CI/CD 和[基础设施即代码][23]之类的东西作为代码的一部分。同样,有些部署想法也不值得,因为它们使生产与开发环境相差太大(参阅[应用程序设计守则第十点][24])。生产和开发之间的任何差异都会降低应用程序的开发速度(实践得来),并且最终可能会导致错误。
|
||||
如果员工无法规模化操作,那么构建一个“可扩展”系统就毫无意义 —— 就像在独轮车上安装喷气式发动机一样。使 Vocal 可扩展的一个重要部分是建立 CI/CD 和 [基础设施即代码][23] 之类的东西。同样,有些部署的思路也不值得考虑,因为它们使生产与开发环境相差太大(参阅 [应用程序设计守则第十守则][24])。生产和开发之间的任何差异都会降低应用程序的开发速度,并且(在实践中)最终可能会导致错误。
|
||||
|
||||
### 缓存
|
||||
|
||||
缓存是一个很大的话题 -- 我曾经做过[一个关于 HTTP 缓存的演讲][25],相对比较基础。我将在这里谈论缓存对于 GraphQL 的重要性。
|
||||
缓存是一个很大的话题 —— 我曾经做过 [一个关于 HTTP 缓存的演讲][25],相对比较基础。我将在这里谈论缓存对于 GraphQL 的重要性。
|
||||
|
||||
首先,一个重要的警告:每当遇到性能问题时,你可能会想:“我可以将这个值放入缓存中吗,以便再次使用时速度更快?”**微基准测试_总是_告诉你:是的。**然而,由于缓存一致性等问题,随处设置缓存往往会使整个系统**变慢**。以下是我对于缓存的理想备忘录:
|
||||
首先,一个重要的警告:每当遇到性能问题时,你可能会想:“我可以将这个值放入缓存中吗,以便再次使用时速度更快?”**微基准测试 _总是_ 告诉你:是的。** 然而,由于缓存一致性等问题,随处设置缓存往往会使整个系统 **变慢**。以下是我对于缓存的检查表:
|
||||
|
||||
1. 是否需要通过缓存解决性能问题
|
||||
2. 再仔细想想(不缓存性能往往更加健壮)
|
||||
2. 再仔细想想(非缓存的性能往往更加健壮)
|
||||
3. 是否可以通过改进现有的缓存来解决问题
|
||||
4. 如果所有都失败了,那么可以考虑添加新的缓存
|
||||
|
||||
在 Web 系统中,你经常使用的一个缓存是 HTTP 缓存系统,因此,在添加额外缓存之前,试着使用 HTTP 缓存是一个好主意。我将在这篇文章中重点讨论这一点。
|
||||
|
||||
另一个常见的陷阱是使用哈希映射或应用程序内部其他东西进行缓存。[它在本地开发中效果很好,但在扩展时表现糟糕。][26]最好的办法是使用支持显式缓存库,支持 Redis 或 Memcached 这样的可插拔后端。
|
||||
另一个常见的陷阱是使用哈希映射或应用程序内部其他东西进行缓存。[它在本地开发中效果很好,但在扩展时表现糟糕][26]。最好的办法是使用支持显式缓存库,支持 Redis 或 Memcached 这样的可插拔后端。
|
||||
|
||||
#### 基础知识
|
||||
|
||||
HTTP 规范中有两种类型缓存:私有和公共。私有缓存不会和多个用户共享数据 -- 实际上是用户的浏览器缓存。其余的就是公共缓存。它们包括受你控制的(例如 CDN、Varnish 或 Nginx 等服务器)和不受你控制的(代理)。代理缓存在当今的 HTTPS 世界中很少见,但一些公司网络会有。
|
||||
HTTP 规范中有两种类型缓存:私有和公共。私有缓存不会和多个用户共享数据 —— 实际上就是用户的浏览器缓存。其余的就是公共缓存。它们包括受你控制的(例如 CDN、Varnish 或 Nginx 等服务器)和不受你控制的(代理)。代理缓存在当今的 HTTPS 世界中很少见,但一些公司网络会有。
|
||||
|
||||
![][27]
|
||||
|
||||
缓存查找键通常基于 URL,因此如果你遵循“相同的内容,相同的 URL;不同的内容,不同的 URL” 规则,即,给每个页面一个规范的 URL,避免从一个 URL 返回不同的内容这样“聪明”的技巧,缓存就会强壮一点。显然,这对 GraphQL API 端点有影响(我将在后面讨论)。
|
||||
缓存查找键通常基于 URL,因此如果你遵循“相同的内容,相同的 URL;不同的内容,不同的 URL” 规则,即,给每个页面一个规范的 URL,避免从同一个 URL 返回不同的内容这样“聪明”的技巧,缓存就会强壮一点。显然,这对 GraphQL API 端点有影响(我将在后面讨论)。
|
||||
|
||||
你的服务器可以采用自定义配置,但配置 HTTP 缓存的主要方法是在 Web 响应上设置 HTTP 头。最重要的头是 `cache-control`。下面这一行说明所有缓存都可以缓存页面长达 3600 秒(一小时):
|
||||
你的服务器可以采用自定义配置,但配置 HTTP 缓存的主要方法是在 Web 响应上设置 HTTP 头。最重要的标头是 `cache-control`。下面这一行说明所有缓存都可以缓存页面长达 3600 秒(一小时):
|
||||
|
||||
```http
|
||||
```
|
||||
cache-control: max-age=3600, public
|
||||
```
|
||||
|
||||
对于有关用户的页面(例如用户设置页面),使用 `private` 而不是 `public` 来告诉公共缓存不要存储响应,防止其提供给其他用户是很重要的。
|
||||
对于针对用户的页面(例如用户设置页面),重要的是使用 `private` 而不是 `public` 来告诉公共缓存不要存储响应,防止其提供给其他用户。
|
||||
|
||||
另一个常见的头是 `vary`,它告诉缓存响应基于 URL 之外的一些内容而变化。(实际上,它将 HTTP 头添加到缓存建中,和 URL 一起。)这是一个非常生硬的工具,这就是为什么尽可能使用良好 URL 结构的原因,但一个重要的示例是告诉浏览器响应取决于登录的 cookie,以便在登录或注销时更新页面。
|
||||
另一个常见的标头是 `vary`,它告诉缓存,响应是基于 URL 之外的一些内容而变化。(实际上,它将 HTTP 头和 URL 一起添加到缓存键中。)这是一个非常生硬的工具,这就是为什么尽可能使用良好 URL 结构的原因,但一个重要的用例是告诉浏览器响应取决于登录的 cookie,以便在登录或注销时更新页面。
|
||||
|
||||
```http
|
||||
```
|
||||
vary: cookie
|
||||
```
|
||||
|
||||
如果页面根据登录状态而变化,即使在正式注销版本上,你也需要 `cache-control:private` (和 `vary:cookie`),确保响应不会混淆。
|
||||
如果页面根据登录状态而变化,你需要 `cache-control:private`(和 `vary:cookie`),即使是在公开的、已登出的页面版本上,以确保响应不会混淆。
|
||||
|
||||
其他有用的头包括 `etag` 和 `last-modified`,但我不会在这里介绍它们。你可能仍然会看到一些诸如 `expires` 和 `pragma:cache` 这种老旧的 HTTP 头。它们早在 1997 年就被 HTTP/1.1 淘汰了,所以我只在我想禁用缓存或者执意时才使用它们。
|
||||
其他有用的标头包括 `etag` 和 `last-modified`,但我不会在这里介绍它们。你可能仍然会看到一些诸如 `expires` 和 `pragma:cache` 这种老旧的 HTTP 标头。它们早在 1997 年就被 HTTP/1.1 淘汰了,所以我只在我想禁用缓存或者我感到偏执时才使用它们。
|
||||
|
||||
#### 客户端头
|
||||
#### 客户端标头
|
||||
|
||||
鲜为人知的是,HTTP 规范允许在客户端请求中使用 `cache-control` 头以减少缓存时间并获得最新响应。不幸的是,似乎大多数浏览器都不支持大于 0 的 `max-age` ,但如果你有时在更新后需要一个最新响应,`no-cache` 会很有用。
|
||||
鲜为人知的是,HTTP 规范允许在客户端请求中使用 `cache-control` 标头以减少缓存时间并获得最新响应。不幸的是,似乎大多数浏览器都不支持大于 0 的 `max-age` ,但如果你有时在更新后需要一个最新响应,`no-cache` 会很有用。
|
||||
|
||||
#### HTTP 缓存和 GraphQL
|
||||
|
||||
如上,正常的缓存建是 URL。但是 GraphQL API 通常只使用一个端点(让我们称之为 `/api/`)。如果你希望 GraphQL 查询可以缓存,那么查询参数将出现在 URL 路径中,例如 `/api/?query={user{id}}&variables={"x":99}`(忽略 URL 转义)。诀窍是将 GraphQL 客户端配置为使用 HTTP GET 请求进行查询(例如,[将 `apollo-link-http` 设置为 `useGETForQueries`][28] )。
|
||||
如上,正常的缓存键是 URL。但是 GraphQL API 通常只使用一个端点(比如说 `/api/`)。如果你希望 GraphQL 查询可以缓存,你需要查询参数出现在 URL 路径中,例如 `/api/?query={user{id}}&variables={"x":99}`(忽略了 URL 转义)。诀窍是将 GraphQL 客户端配置为使用 HTTP GET 请求进行查询(例如,[将 `apollo-link-http` 设置为 `useGETForQueries`][28] )。
|
||||
|
||||
突变不能缓存,所以它们仍然需要使用 HTTP POST 请求。对于 POST 请求,缓存只会看到 `/api/` 作为 URL 路径,但缓存将拒绝缓存 POST 请求。请记住,GET 用于非突变查询(即幂等),POST 用于非突变(即非幂等)。在一种情况下,你可能希望避免使用 GET 查询:如果查询变量包含敏感信息。URL 经常出现在日志文件、浏览器历史记录和聊天中,因此 URL 中包含敏感信息通常不是一个好主意。无论如何,像身份验证这种事情应该作为不可缓存的更改来完成,这是一个特殊的情况,值得记住。
|
||||
Mutation 不能缓存,所以它们仍然需要使用 HTTP POST 请求。对于 POST 请求,缓存只会看到 `/api/` 作为 URL 路径,但缓存会直接拒绝缓存 POST 请求。请记住,GET 用于非 Mutation 查询(即幂等),POST 用于 Mutation(即非幂等)。在一种情况下,你可能希望避免使用 GET 查询:如果查询变量包含敏感信息。URL 经常出现在日志文件、浏览器历史记录和聊天中,因此 URL 中包含敏感信息通常是一个坏主意。无论如何,像身份验证这种事情应该作为不可缓存的 Mutation 来完成,这是一个特殊的情况,值得记住。
|
||||
|
||||
不幸的是,有一个问题:GraphQL 查询往往比 REST API URL 大得多。如果你简单地打开基于 GET 的查询,你会得到一些非常长的 URL,很容易超过 2000 字节的限制,目前一些流行的浏览器和服务器还不会接受它们。一种解决方案是发送某种查询 ID,而不是发送整个查询,即类似于 `/api/?queryId=42&variables={"x":99}`。Apollo GraphQL 服务器支持这两种方式。
|
||||
不幸的是,有一个问题:GraphQL 查询往往比 REST API URL 大得多。如果你简单地切换到基于 GET 的查询,你会得到一些非常长的 URL,很容易超过 2000 字节的限制,目前一些流行的浏览器和服务器还不会接受它们。一种解决方案是发送某种查询 ID,而不是发送整个查询,即类似于 `/api/?queryId=42&variables={"x":99}`。Apollo GraphQL 服务器对此支持两种方式:
|
||||
|
||||
一种方法是[从代码中提取所有 GraphQL 查询并构建一个服务器端和客户端共享的查找表][29]。缺点之一是这会使构建过程更加复杂,另一个缺点是它将客户端项目与服务器项目耦合,这与 GraphQL 的卖点背道而驰。还有一个缺点是 X 版本和 Y 版本可能对于同一组查询有会识别出不同的涵义,这会成为一个问题,因为 1:复制的应用程序将在更新推出或回滚期间提供多个版本,2:客户端可能会使用缓存的 JavaScript,即使你升级或降级服务器。
|
||||
一种方法是 [从代码中提取所有 GraphQL 查询,并构建一个服务器端和客户端共享的查询表][29]。缺点之一是这会使构建过程更加复杂,另一个缺点是它将客户端项目与服务器项目耦合,这与 GraphQL 的一个主要卖点背道而驰。还有一个缺点是 X 版本和 Y 版本的代码可能识别一组不同的查询,这会成为一个问题,因为 1:复制的应用程序将在更新推出或回滚期间提供多个版本,2:客户端可能会使用缓存的 JavaScript,即使你升级或降级服务器。
|
||||
|
||||
另一种方式是 Apollo GraphQL 所宣称的 [自动持久查询(APQ)][30]。对于 APQ 而言,查询 ID 是查询的哈希值。客户端向服务器发出请求,通过哈希查询。如果服务器无法识别该查询,则客户端会在 POST 请求中发送完整的查询,服务器会保存此次查询的散列值,以便下次识别。
|
||||
另一种方式是 Apollo GraphQL 所宣称的 [自动持久查询(APQ)][30]。对于 APQ 而言,查询 ID 是查询的哈希值。客户端向服务器发出请求,通过哈希查询。如果服务器无法识别该查询,则客户端会在 POST 请求中发送完整的查询,服务器会保存此次查询的散列值,以便下次识别。
|
||||
|
||||
![][31]
|
||||
|
||||
#### HTTP 缓存和 Keystone 5
|
||||
|
||||
如上所述,Vocal 使用 Keystone 5 生成 GraphQL API,Keystone 5 和 Apollo GraphQL 服务器配合一起工作。那么我们是如何设置缓存头的呢?
|
||||
如上所述,Vocal 使用 Keystone 5 生成 GraphQL API,而 Keystone 5 和 Apollo GraphQL 服务器配合一起工作。那么我们是如何设置缓存标头的呢?
|
||||
|
||||
Apollo 支持 GraphQL 模式的缓存提示。巧妙地是,Apollo 会收集查询涉及的所有内容的所有提示,然后它会自动计算适当的缓存头值。例如,以这个查询为例:
|
||||
Apollo 支持 GraphQL 模式的<ruby>缓存提示<rt>cache hint</rt></ruby>。巧妙地是,Apollo 会收集查询涉及的所有内容的所有缓存提示,然后它会自动计算适当的缓存标头值。例如,以这个查询为例:
|
||||
|
||||
```
|
||||
query userAvatarUrl {
|
||||
@ -181,7 +183,7 @@ query userAvatarUrl {
|
||||
|
||||
如果 `name` 的最长期限为 1 天,而 `avatar_url` 的最长期限为 1 小时,则整体缓存最长期限将是最小值,即 1 小时。`authenticatedUser` 取决于登录 cookie,因此它需要一个 `private` 提示,它会覆盖其他字段的 `public`,因此生成的 HTTP 头将是 `cache-control:max-age=3600,private`。
|
||||
|
||||
我[对 Keystone 列表和字段添加了缓存提示][32]。以下是一个简单例子,在文档的待办列表演示中给一个字段添加缓存提示:
|
||||
我 [对 Keystone 列表和字段添加了缓存提示支持][32]。以下是一个简单例子,在文档的待办列表演示中给一个字段添加缓存提示:
|
||||
|
||||
```
|
||||
const keystone = new Keystone({
|
||||
@ -207,67 +209,67 @@ keystone.createList('Todo', {
|
||||
|
||||
#### 另一个问题:CORS
|
||||
|
||||
跨域资源共享(CORS)规则会与基于 API 网站中的缓存产生冲突。
|
||||
令人沮丧的是,<ruby>跨域资源共享<rt>Cross-Origin Resource Sharing</rt></ruby>(CORS)规则会与基于 API 网站中的缓存产生冲突。
|
||||
|
||||
在深入问题细节之前,让我们跳到最简单的解决方案:将主站点和 API 放在一个域中。如果你的站点和 API 位于同一个域,就不必担心 CORS 规则(但你可能需要考虑[限制 cookie][33])。如果你的 API 专门用于网站,这是最简单的解决方案,你可以愉快地跳过这一节。
|
||||
在深入问题细节之前,让我们跳到最简单的解决方案:将主站点和 API 放在一个域名上。如果你的站点和 API 位于同一个域名上,就不必担心 CORS 规则(但你可能需要考虑 [限制 cookie][33])。如果你的 API 专门用于网站,这是最简单的解决方案,你可以愉快地跳过这一节。
|
||||
|
||||
在 Vocal V1 中,网站(Next.js)和平台(Keystone GraphQL)应用程序处于不同域(`vocal.media` 和 `api.vocal.media`)。为了保护用户免受恶意网站的侵害,现代浏览器不会让一个网站与另一个网站进行交互。因此,在允许 `vocal.media` 向 `api.vocal.media` 发出请求之前,浏览器会对 `api.vocal.media` 进行“预检”。这是一个使用 `OPTIONS` 方法的 HTTP 请求,主要是询问跨域资源共享是否允许。预检通过后,浏览器会发出最初的正常请求。
|
||||
在 Vocal V1 中,网站(Next.js)和平台(Keystone GraphQL)应用程序处于不同域(`vocal.media` 和 `api.vocal.media`)。为了保护用户免受恶意网站的侵害,现代浏览器不会随便让一个网站与另一个网站进行交互。因此,在允许 `vocal.media` 向 `api.vocal.media` 发出请求之前,浏览器会对 `api.vocal.media` 进行“预检”。这是一个使用 `OPTIONS` 方法的 HTTP 请求,主要是询问跨域资源共享是否允许。预检通过后,浏览器会发出最初的正常请求。
|
||||
|
||||
令人沮丧的是,预检是针对每个 URL 的。浏览器为每个 URL 发出一个新的 `OPTIONS` 请求,服务器每次都会响应。[服务器没法说 `vocal.media` 是所有 `api.vocal.media` 请求的可信来源][34] 。当所有内容都是对一个 API 端点的 POST 请求时,这个问题并不严重,但是在为每个查询提供 Get URL 后,每个查询都因预检而延迟。更令人沮丧的是,HTTP 规范说 `OPTIONS` 请求不能被缓存,所以你会发现你所有的 GraphQL 数据都被完美地缓存在用户身旁的 CDN 中,但浏览器仍然必须发出所有的预检请求。
|
||||
令人沮丧的是,预检是针对每个 URL 的。浏览器为每个 URL 发出一个新的 `OPTIONS` 请求,服务器每次都会响应。[服务器没法说 `vocal.media` 是所有 `api.vocal.media` 请求的可信来源][34] 。当所有内容都是对一个 API 端点的 POST 请求时,这个问题并不严重,但是在为每个查询提供 GET 式 URL 后,每个查询都因预检而延迟。更令人沮丧的是,HTTP 规范说 `OPTIONS` 请求不能被缓存,所以你会发现你所有的 GraphQL 数据都被完美地缓存在用户身旁的 CDN 中,但浏览器仍然必须向源服务器发出所有的预检请求。
|
||||
|
||||
如果你不能使用共享域,有几种解决方案。
|
||||
如果你不能只使用一个共享的域,有几种解决方案。
|
||||
|
||||
如果你的 API 足够简单,你或许可以利用 [CORS 规则的例外][35]。
|
||||
|
||||
某些缓存服务器可以配置为忽略 HTTP 规范,任何情况都会缓存 `OPTIONS` 请求。例如,基于 Varnish 的缓存和 AWS CloudFrone。这不如完全避免预检那么有效,但比默认的要好。
|
||||
|
||||
另一个选项是 [JSONP][36],很巧妙。当心:如果做错了,那么可能会创建安全漏洞。
|
||||
另一个很魔改的选项是 [JSONP][36]。当心:如果做错了,那么可能会创建安全漏洞。
|
||||
|
||||
#### Vocal 更好地利用缓存
|
||||
#### 让 Vocal 更好地利用缓存
|
||||
|
||||
HTTP 缓存在底层工作之后,我需要让应用程序更好地利用它。
|
||||
让 HTTP 缓存在底层工作之后,我需要让应用程序更好地利用它。
|
||||
|
||||
HTTP 缓存的一个限制是它在响应级别上是全部或没有。大多数响应都可以缓存,但如果一个字节不能缓存,那整个页面就无法缓存。作为一个博客平台,大多数 Vocal 数据都是可缓存的,但在旧网站上,由于右上角的菜单栏,几乎没有页面可以缓存。对于匿名用户,菜单栏将显示邀请用户登录或创建账号的链接。对于已登录用户,它会变成用户头像和用户个人资料菜单,因为页面会根据用户登录状态而变化,所以无法在 CDN 中缓存任何页面。
|
||||
HTTP 缓存的一个限制是它在响应级别上要么是全有要么是全无的。大多数响应都可以缓存,但如果一个字节不能缓存,那整个页面就无法缓存。作为一个博客平台,大多数 Vocal 数据都是可缓存的,但在旧网站上,由于右上角的菜单栏,几乎没有页面可以缓存。对于匿名用户,菜单栏将显示邀请用户登录或创建账号的链接。对于已登录用户,它会变成用户头像和用户个人资料菜单,因为页面会根据用户登录状态而变化,所以无法在 CDN 中缓存任何页面。
|
||||
|
||||
![A typical page from Vocal. Most of the page is highly cachable content, but in the old site none of it was actually cachable because of a little menu in the top right corner.][37]
|
||||
![Vocal 的一个典型页面。该页面的大部分内容都是高度可缓存的内容,但在旧网站中,由于右上角有一个小菜单,实际上没有一个内容是可缓存的。][37]
|
||||
|
||||
这些页面是由 React 组件的服务器端渲染(SSR)的。解决方法是将所有依赖于登录 cookie 的 React 组件强制设置为[仅延迟呈现客户端][38],现在,服务器会返回完全通用的页面,其中包含用于个性化组件(如登录菜单栏)的占位符。当页面在浏览器中加载时,这些占位符将通过调用 GraphQL API 在客户端填充。通用页面可以安全地缓存到 CDN 中。
|
||||
这些页面是由 React 组件的服务器端渲染(SSR)的。解决方法是将所有依赖于登录 cookie 的 React 组件,强制它们 [只在客户端进行延迟呈现][38]。现在,服务器会返回完全通用的页面,其中包含用于个性化组件(如登录菜单栏)的占位符。当页面在浏览器中加载时,这些占位符将通过调用 GraphQL API 在客户端填充。通用页面可以安全地缓存到 CDN 中。
|
||||
|
||||
这一技巧不仅提高了缓存命中率,还帮助改善了人们感知的页面加载时间。空白屏幕和旋转动画让我们不耐烦,但一旦第一个内容出现,它会分散我们几百毫秒的注意力。如果人们在社交媒体上点击一个 Vocal 帖子的链接,主要内容就会立即从 CDN 中出现,很少有人会注意到,有些组件直到几百毫秒后才会完全出现。
|
||||
|
||||
顺便说一下,另一个让用户更快地看到第一个内容的技巧是[流渲染][39],而不是等待整个页面渲染完成后再发送。不幸的是,[Node.js 还不支持这个功能][40]。
|
||||
顺便说一下,另一个让用户更快地看到第一个内容的技巧是 [流式渲染][39],而不是等待整个页面渲染完成后再发送。不幸的是,[Node.js 还不支持这个功能][40]。
|
||||
|
||||
拆分响应来提高可缓存性也适用于 GraphQL。通过一个请求查询多个数据片段的能力通常是 GraphQL 的一个优势,但如果响应的不同部分具有不同的缓存,那么最好将它们分开。举个简单的例子,Vocal 的分页组件需要知道当前页面的页数和内容。最初,组件在一个查询中同时获取两个页面,但由于页面的总数是所有页面的一个常量,所有我将其设置为一个单独的查询,以便缓存它。
|
||||
拆分响应来提高可缓存性也适用于 GraphQL。通过一个请求查询多个数据片段的能力通常是 GraphQL 的一个优势,但如果响应的不同部分具有差别很大的缓存,那么最好将它们分开。举个简单的例子,Vocal 的分页组件需要知道当前页面的页数和内容。最初,组件在一个查询中同时获取两个页面,但由于页面的总数是所有页面的一个常量,所有我将其设置为一个单独的查询,以便缓存它。
|
||||
|
||||
#### 缓存的好处
|
||||
|
||||
缓存的明显好处是它减轻了 Vocal 后端服务器的负载。效果很好,但是依赖缓存来获得容量是危险的,因为当有一天你不可避免地放弃缓存时,你仍然需要一个备份计划。
|
||||
缓存的明显好处是它减轻了 Vocal 后端服务器的负载。这很好。但是依赖缓存来获得容量是危险的,你仍然需要一个备份计划,以便当有一天你不可避免地放弃缓存。
|
||||
|
||||
提高页面响应速度而使用缓存是一个好理由。
|
||||
提高页面响应速度是使用缓存是一个更好的理由。
|
||||
|
||||
其他一些好处可能不那么明显。峰值流量往往是高度本地化的。如果一个有很多社交媒体粉丝的人分享了一个页面的链接,那么 Vocal 的流量就会大幅上升,但主要是指向分享的那个页面及其资产。这就是为什么缓存擅长吸收最糟糕的流量峰值,它使后端流量模式相对更平滑,更容易自动伸缩处理。
|
||||
其他一些好处可能不那么明显。峰值流量往往是高度本地化的。如果一个有很多社交媒体粉丝的人分享了一个页面的链接,那么 Vocal 的流量就会大幅上升,但主要是指向分享的那个页面及其元素。这就是为什么缓存擅长吸收最糟糕的流量峰值,它使后端流量模式相对更平滑,更容易被自动伸缩处理。
|
||||
|
||||
另一个好处是优雅的回滚。即使后端由于某些原因出现了严重的问题,站点最受欢迎的部分仍然可以通过 CDN 缓存来提供服务。
|
||||
另一个好处是<ruby>优雅的退化<rt>graceful degradation</rt></ruby>。即使后端由于某些原因出现了严重的问题,站点最受欢迎的部分仍然可以通过 CDN 缓存来提供服务。
|
||||
|
||||
### 其他的性能调整
|
||||
|
||||
正如我常说的,可扩展的秘诀不是让事情变得更复杂。它只是让事情变得不比需要的更复杂,然后彻底解决所有防止扩展的东西。扩展 Vocal 涉及到许多不适合这篇文章的小事情。
|
||||
正如我常说的,可扩展的秘诀不是让事情变得更复杂。它只是让事情变得不比需要的更复杂,然后彻底解决所有阻碍扩展的东西。扩展 Vocal 的规模涉及到许多小事,在这篇文章中无法一一说明。
|
||||
|
||||
一个经验:对于分布式系统中难以调试的问题,最困难的部分通常是获取正确的数据,从而了解发生的原因。我能想到很多时候,我被困住了,只能靠猜测来“即兴发挥”,而不是找出如何找到正确的数据。有时这行得通,但对复杂的问题却不行。
|
||||
一个经验:对于分布式系统中难以调试的问题,最困难的部分通常是获取正确的数据,从而了解发生的原因。我能想到很多时候,我被困住了,只能靠猜测来“即兴发挥”,而不是想办法找到正确的数据。有时这行得通,但对复杂的问题却不行。
|
||||
|
||||
一个相关技巧是,你可以通过获取系统中每个组件的实时数据(甚至只是 **tail -F** ),在不同的窗口中显示,然后在另一个窗口中单击网站来了解很多信息。比如:“为什么切换这个复选框会在后台产生这么多的 DB 查询?”
|
||||
一个相关技巧是,你可以通过获取系统中每个组件的实时数据(甚至只是 `tail -F` 的日志),在不同的窗口中显示,然后在另一个窗口中单击网站来了解很多信息。比如:“为什么切换这个复选框会在后台产生这么多的 DB 查询?”
|
||||
|
||||
这里有个例子。有些页面需要几秒钟以上的时间来呈现,但这个情况只会在部署环境中使用 SSR 时会出现。监控仪表盘没有显示任何 CPU 使用量峰值,应用程序也没有使用磁盘,所以这表明应用程序可能正在等待网络请求,可能是后端请求。在开发环境中,我可以使用 [sysstat 工具][42]来记录 CPU、RAM、磁盘使用情况,以及 Postgres 语句日志和正常的应用日志来观察应用程序是如何工作的。[Node.js 支持探测跟踪 HTTP 请求][42],比如使用 [bpftrace][44],但它们不能在开发环境中工作,所以我在源代码中找到了探测功能,并创建了一个带有请求日志的 Node.js 版本。我使用 [tcpdump][45] 记录网络数据,这让我找到了问题所在:对于网站发出的每一个 API 请求,都要创建一个新的网络连接到平台。如果这都不起作用,我想我会在应用程序中添加请求跟踪。
|
||||
这里有个修复的例子。有些页面需要几秒钟以上的时间来呈现,但这个情况只会在部署环境中使用 SSR 时会出现。监控仪表盘没有显示任何 CPU 使用量峰值,应用程序也没有使用磁盘,所以这表明应用程序可能正在等待网络请求,可能是对后端的请求。在开发环境中,我可以使用 [sysstat 工具][42]来记录 CPU、RAM、磁盘使用情况,以及 Postgres 语句日志和正常的应用日志来观察应用程序是如何工作的。[Node.js 支持探测跟踪 HTTP 请求][42],比如使用 [bpftrace][44],但处于某些无聊的原因,它们不能在开发环境中工作,所以我在源代码中找到了探针,并创建了一个带有请求日志的 Node.js 版本。我使用 [tcpdump][45] 记录网络数据,这让我找到了问题所在:对于网站发出的每一个 API 请求,都要创建一个新的网络连接到 “Platform”。(如果这都不起作用,我想我会在应用程序中添加请求跟踪功能。)
|
||||
|
||||
网络连接在本地机器上速度很快,但在现实网络上却不可忽略。设置加密连接(比在生产环境中)需要更长时间。如果你向一个服务器(比如一个 API)发出大量请求,保持连接打开并重用它是很重要的。浏览器会自动这么做,但 Node.js 默认不会,因为它不知道你是否发出了很多请求,所以这个问题只出现在 SSR 上。与许多长时间的调试会话一样,修复非常简单:只需将 SSR 配置为 [保持连接存活][46],这样会使页面的呈现时间大幅下降。
|
||||
网络连接在本地机器上速度很快,但在现实网络上却不可忽略。建立加密连接(比在生产环境中)需要更长时间。如果你向一个服务器(比如一个 API)发出大量请求,保持连接打开并重用它是很重要的。浏览器会自动这么做,但 Node.js 默认不会,因为它不知道你是否发出了很多请求,所以这个问题只出现在 SSR 上。与许多漫长的调试过程一样,修复却非常简单:只需将 SSR 配置为 [保持连接存活][46],这样会使页面的呈现时间大幅下降。
|
||||
|
||||
如果你想了解更多这方面的知识,我强烈建议你阅读[高性能浏览器网络][47]这本书(免费在线阅读),并跟随 [Brendan Gregg 出版的指南][48]。
|
||||
如果你想了解更多这方面的知识,我强烈建议你阅读《[高性能浏览器网络][47]》这本书(可免费在线阅读),并跟进 [Brendan Gregg 发表的指南][48]。
|
||||
|
||||
### 你的站点?
|
||||
### 你的站点呢?
|
||||
|
||||
实际上,我们还可以做很多事情来提升 Vocal 的速度,但我们没有全做。在初创公司和在大公司身为一个固定员工做 SRE 工作还是有很大区别的。我们的目标、预算和发布日期都很紧张,但最终我们的网站得到了很大改善,给了用户他们想要的东西。
|
||||
实际上,我们还可以做很多事情来提升 Vocal 的速度,但我们没有全做。这是因为在初创公司和在大公司身为一个固定员工做 SRE 工作还是有很大区别的。我们的目标、预算和发布日期都很紧张,但最终我们的网站得到了很大改善,给了用户他们想要的东西。
|
||||
|
||||
同样的,你的站点有它自己的目标,并且可能与 Vocal 有很大的不同。然而,我希望这篇文章和它的链接至少能给你一些有用的想法,为用户创造更好的东西。
|
||||
同样的,你的站点有它自己的目标,并且可能与 Vocal 有很大的不同。然而,我希望这篇文章和它的链接至少能给你一些有用的思路,为用户创造更好的东西。
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@ -276,7 +278,7 @@ via: https://theartofmachinery.com/2020/06/29/scaling_a_graphql_site.html
|
||||
作者:[Simon Arneaud][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[MjSeven](https://github.com/MjSeven)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
校对:[wxy](https://github.com/wxy)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user