平面部落美文网

您现在的位置是: 首页 > 美文佳篇

好属妞这是只有精品,如何解决GraphQL网站的可扩展性问题?

2020-10-15 13:52:41平面部落美文网
人声Vocal是一个博客平台,内容广泛,包罗万象,甚至还有很多猫和狗。作者可以因在Vocal中发布内容而获得报酬。页面上的每次点击都会给作者带来一些回报,作者也可以接受其他用户的捐赠。一些专业人士使用此平台展示他们的工作,但是对于大多数用户而言,这只是一个有趣的爱好,只是用来赚点零花钱。据报道,Vocal的母公司是J

  人声

  Vocal是一个博客平台,内容广泛,包罗万象,甚至还有很多猫和狗。

  

  作者可以因在Vocal中发布内容而获得报酬。页面上的每次点击都会给作者带来一些回报,作者也可以接受其他用户的捐赠。一些专业人士使用此平台展示他们的工作,但是对于大多数用户而言,这只是一个有趣的爱好,只是用来赚点零花钱。

  

  据报道,Vocal的母公司是Jerrick Media,该网站的开发是与悉尼一家名为Thinkmill的公司合作进行的。

  背景

  Thinkmill使用Next。js(基于React的Web框架)构建网站并与Keystone在MongoDB上提供的GraphQL API进行通信。Keystone是基于GraphQL的无头CMS库:您可以在JavaScript中定义架构,将其连接到数据存储,并获取自动生成的GraphQL API来访问数据。这是一个免费的开源软件项目,由Thinkmill提供商业支持。

  1。 人声V2

  Vocal的第一个版本受到关注。它吸引了喜欢它的用户,并且继续增长。 最后,Jerrick Media要求Thinkmill帮助开发V2版本,该版本已于去年9月成功发布。

  V2版本主要涉及用户界面和功能更改,本文中未提及。我所做的是使新站点更加强大和可扩展。

  2。 数据库迁移

  Thinkmill在将MongoDB用于Vocal时遇到了一些可伸缩性问题,因此决定将Keystone升级到V5,以利用新版本增加的Postgres支持。

  如果您已经在技术领域工作了很长时间,您可能还记得2000年代后期的“ NoSQL”趋势。当时的宣传:关系(SQL)数据库(如Postgres)不如“ Webscale” NoSQL数据库(如MongoDB)可扩展。

  从技术上讲这是正确的,但是NoSQL数据库的可伸缩性来自可以有效处理的各种查询。简单的非关系数据库(例如文档和键值数据库)有其自己的用途,但是当用作应用程序的通用后端时,应用程序通常会在超出关系数据库的理论扩展限制之前达到数据库的上限。。

  Vocal的大多数数据库查询在MongoDB上都可以使用,但是随着时间的流逝,越来越多的查询需要手动调整才能正常工作。

  在技术要求方面,Vocal与Wikipedia类似,后者是世界上最大的网站之一。维基百科使用MySQL(或更确切地说,它的分支MariaDB)。当然,需要一些关键的工程设计来适应Vocal场景,但是我认为在可预见的将来,关系数据库将不会成为Vocal扩展之路的绊脚石。

  我曾经检查过一条数据。 托管的AWS RDS Postgres实例的成本不到旧MongoDB实例成本的五分之一,但Postgres实例的CPU使用率仍不到10%,流量也比旧站点高。这主要是由于文档数据库体系结构中某些重要查询的效率较低。

  迁移是另一个大话题,但是基本上,Thinkmill开发人员使用MoSQL来构建ETL管道来完成繁重的任务。由于Keystone是一个FOSS项目,因此我也可以对其GraphQL到SQL的映射做出一些性能改进。

  对于此类问题,建议您参考Markus Winand的SQL博客:使用Index Luke和Modern SQL。他的写作非常友好,并且非专业人员可以理解,并且他还提供了编写快速高效的SQL所需的大多数理论知识。对于其余的理论知识,您可以找到有关数据库性能主题的优秀教程来学习。

  平台

  1。构架

  Vocal的V1版本有几个节点。一组js应用程序,它们作为CDN在Cloudflare后面的虚拟专用服务器(VPS)上运行。我尊重避免过度设计的想法,因此我非常喜欢这种架构。但是,当开始开发V2版本时,很明显,这种简单的体系结构负担不起Vocal的流量。在高峰期处理大量流量时,这为Thinkmiller开发人员留下了很少的空间,并且很难安全地在线部署更新。

  以下是V2版本的新体系结构:

  

  Vocal V2的体系结构。该请求通过CDN到达AWS中的负载均衡器。负载平衡器将流量分配给两个应用程序“平台”和“网站”。“平台”是Keystone应用程序,负责在Redis和Postgres中存储数据。

  基本上是两个节点。复制js应用程序并将其放置在负载均衡器的后面,就这么简单。在SRE工作生涯中,我经常见到有工程师预期出比这个复杂很多的可扩展架构,但是我遇到过比Vocal规模大几个数量级的站点,这些站点仍然只是在数据库较小的负载平衡器之后复制服务。经过认真思考,如果随着站点的增长平台架构需要变得更加复杂,那么其可伸缩性就不会很高。

  改善网站可扩展性的重点是解决许多阻碍扩展的实施细节。

  如果流量继续增长,Vocal的体系结构可能需要添加一些内容,但是使其变得更加复杂的主要原因是新功能。例如,如果(由于某种原因)Vocal将来需要处理实时地理空间数据,那将是一个技术怪兽,它与博客文章的需求完全不同,并且那时应该进行许多架构更改。

  大型站点体系结构中的大多数复杂性来自功能的复杂性。

  如果您不知道如何使您的体系结构可扩展,我的建议是使其结构尽可能简单明了。修复非常简单的体系结构比修复非常复杂的体系结构要容易得多,也更便宜。此外,过于复杂的体系结构更容易出错,并且这些错误更难以调试。

  顺便说一句,Vocal碰巧被分为两个应用程序,但这并不重要。一个常见的扩展误解是,以可伸缩性的名称将应用程序划分为较小的服务为时过早,但是拆分应用程序的位置选择错误,这会导致更多的可伸缩性问题。Vocal可以作为单个应用程序进行扩展,但是拆分的位置也非常好。

  2。 基础设施

  Thinkmill有一些具有使用AWS经验的开发人员,但主要是一家开发公司,因此在部署新版本时需要“处理”。我最终在AWS Fargate上部署了新版本的Vocal,这是Elastic Container Service(ECS)的一个相对较新的后端。

  过去,许多人希望ECS成为简单的“将Docker容器作为托管服务运行”产品,但发现他们仍然必须构建和管理自己的服务器集群,因此感到失望。借助ECS Fargate,AWS可以管理集群。它支持正在运行的Docker容器,并具有一些有用的基本功能,例如复制,运行状况检查,滚动更新,自动缩放和简单警报。

  一个很好的替代方法是托管平台即服务(PaaS),例如App Engine或Heroku。Thinkmill已将它们用于一些简单的项目,但是其他项目需要更大的灵活性,因此尚无法使用。一些非常大的站点也可以在PaaS上运行,但是Vocal的规模如此之大,以至于自定义云部署足够经济。

  另一个明显的选择是Kubernetes。Kubernetes具有比ECS Fargate更多的功能,但是它也更昂贵-包括资源开销,维护人员(例如常规节点升级)也更昂贵。一般来说,我不建议在没有DevOps专门人员的环境中使用Kubernetes。Fargate具有Vocal所需的功能,并允许Thinkmill和Jerrick Media专注于站点改进,而无需担心基础架构。

  另一个选项是“无服务器”功能产品,例如AWS Lambda或Google Cloud Functions。它们非常适合处理少量或不规则的流量,但是(正如我将解释的那样),ECS Fargate的自动缩放功能足以满足Vocal的后端需求。这些产品的另一个优势是,它们使开发人员可以在云环境中部署事物,而无需了解很多有关云环境的知识。其代价是无服务器产品与开发过程以及测试和调试过程紧密结合在一起。Thinkmill已经拥有足够的AWS专业知识来管理Fargate部署,您只需要知道如何制作Node。js Express HelloWorld应用程序的开发人员可以处理Vocal开发,而无需了解有关无服务器功能或Fargate的任何知识。

  ECS Fargate的明显缺点是供应商锁定。但是,避免供应商锁定是一种妥协,就像避免停机一样。如果您担心迁移成本,那么在平台独立性上花费比迁移成本更多的钱是没有意义的。Vocal中特定于Fargate的代码总数少于500行。

  最重要的是,Vocal应用程序代码本身与平台无关。它可以在普通开发人员的计算机上运行,然后打包到Docker容器中,然后在几乎可以支持Docker容器的任何位置(包括ECS Fargate)运行。

  Fargate的另一个缺点是安装起来不容易。像AWS中的大多数事物一样,它涉及VPC,子网和IAM策略等概念。幸运的是,这些事情是完全静态的(不同于需要维护的服务器群集)。

  构建可扩展的应用程序

  如果要轻松运行大型应用程序,则有很多事情要做。遵循应用程序设计的十二要素原则是基础,因此在此不再赘述。

  如果员工无法扩展其运营能力,则无需构建“可扩展”系统,就像在单轮脚踏车上安装喷气发动机一样。使Vocal可伸缩的关键是将诸如CI / CD和基础架构之类的代码设置为代码。同样,某些部署概念会使生产和开发环境大为不同,因此不值得采用。生产与开发之间的每个差异都会减慢应用程序开发的速度,并可能导致错误。

  1。 快取

  缓存是一个大话题。在上一讲中,我仅提到了HTTP缓存,但这还不够。在本文中,我将重点介绍GraphQL。

  首先,一个重要的警告:每当遇到性能问题时,您可能会认为:“可以将此值存储在缓存中以备将来重用,从而提高性能吗?“微基准测试几乎总是会给您明确的答案。但是,由于诸如高速缓存一致性之类的问题,高速缓存的滥用将使整个系统变慢。下面是使用缓存之前我需要考虑的问题列表:

  问问自己是否真的需要使用缓存来解决性能问题。 再考虑一下(非缓存的性能调整技术通常更可靠)。 问问自己是否可以改善现有的缓存来解决该问题。 如果所有其他方法均失败,则可能可以添加新的缓存

  HTTP缓存系统一直存在,我们知道在添加其他缓存之前,我们应尝试充分利用HTTP缓存。

  另一个非常常见的陷阱是使用哈希映射或应用程序内部的某些内容进行缓存。它在本地开发中效果很好,但在扩展时效果不佳。最好的方法是使用支持可插拔后端(例如Redis或Memcached)的显式缓存库。

  2。 基础

  HTTP规范中有两种类型的缓存:私有缓存和公共缓存。专用缓存是指不与多个用户共享数据的缓存,实际上是用户的浏览器缓存。其余的是公共缓存,其中包括您控制的服务器(例如CDN或Varnish或Nginx之类的服务器)和非托管服务器(代理)。在当今的HTTPS世界中,代理缓存很少见,但在某些公司网络中也可以看到。

  

  缓存查找键通常基于URL,因此如果坚持“相同的内容,相同的URL;不同的内容,不同的URL”规则,则缓存不是一个大问题。换句话说,为每个页面提供一个规范的URL,并防止“智能”技巧从一个URL返回不同的内容。显然,这会对GraphQL API端点产生影响。

  您的服务器可以使用自定义配置,但是配置HTTP缓存的主要方法是在Web响应上设置HTTP标头。最重要的头是缓存控制。以下内容表明此行下的所有缓存可能最多将页面缓存3600秒(一小时):

  快取控制:max-age = 3600,公开

  对于特定于用户的页面(例如用户设置页面),重要的是用public代替public,以告诉公共缓存不要存储响应并将其提供给其他用户。另一个常见的标头是不同的。这告诉缓存响应是根据URL以外的因素而变化的。(其中HTTP标头添加到URL旁的缓存键中)这是一个非常笨的工具,所以我建议尝试改用良好的URL结构,但它的一个重要用例是告诉浏览器响应依赖登录cookie,它们在登录/重建时更新页面。

  变化:cookie

  如果页面可能会根据登录状态而更改,则即使在已注销的公共版本上,也需要cahce-control:private(并更改:cookie),以确保响应不会混淆。

  其他有用的标头包括etag和last-modified,此处未介绍。您可能还会看到一些旧的标头,例如expires和pragma:cache。早在1997年,HTTP / 1。1不推荐使用。

  3。 客户头

  鲜为人知的是,HTTP规范允许在客户端请求中使用缓存控制,以减少缓存时间并获得更新的响应。

  幸运的是,浏览器似乎并未广泛支持大于0的max-age,但是如果您有时在更新后需要新的响应,则无缓存可能会有用。

  HTTP缓存和GraphQL

  如上所述,常规缓存键是URL。但是GraphQL API通常仅使用一个端点(我们将其称为/ api /)。如果希望GraphQL查询可缓存,则需要在URL路径中显示查询及其参数,例如/ api /?。query ={user{id}}&variables ={''x'':99}(忽略网址转义)。这里的技巧是配置GraphQL客户端以使用HTTP GET请求进行查询(例如,为apollo-link-http设置useGETForQueries)。

  无法缓存突变,因此它们仍然需要使用HTTP POST请求。对于POST请求,缓存仅将/ api /作为URL路径,但是缓存将完全拒绝缓存POST请求。请记住:GET用于非突变查询,而POST用于突变。在某些情况下,您可能希望避免在查询中使用GET:因为查询变量可能包含敏感信息。URL有出现在日志文件,浏览器历史记录和聊天通道中的习惯,因此在URL中保留敏感信息通常不是一个好主意。无论如何,身份验证之类的事情都应作为不可缓存的变异来完成,因此这种情况很少见,但也应牢记。

  不幸的是,这里存在一个问题:GraphQL查询往往比REST API URL大得多。如果仅启用基于GET的查询,您将获得一些非常大的URL,可能还不止这些?限制为2000个字节,某些流行的浏览器和服务器不接受它们。一种解决方案是发送某种查询ID,而不是发送整个查询。(类似于/ api /?queryId = 42&variables ={''x'':99}。)Apollo GraphQL服务器支持两种方法来执行此操作。

  一种方法是从代码中提取所有GraphQL查询,并建立在服务器和客户端之间共享的查找表。它的缺点之一是使构建过程更加复杂,另一个缺点是将客户端项目耦合到服务器项目,这与GraphQL的卖点相反。另一个缺点是代码的X版本与代码的Y版本可能会识别不同的查询集。这是一个问题,因为1)您的复制应用程序将在更新推出或回滚期间提供多个版本,以及2)即使升级或降级服务器,客户端也可能使用缓存的JavaScript。

  另一种方法被Apollo GraphQL称为自动持久查询(APQ)。对于APQ,查询ID是查询的哈希。客户端乐观地向服务器发送请求,并通过哈希引用查询。如果服务器无法识别查询,则客户端在POST请求中发送完整的查询。服务器将查询存储在哈希中,以便将来可以识别它。

  

  HTTP缓存和Keystone5

  如上所述,Vocal使用Keystone 5生成其GraphQL API,Keystone 5与Apollo lGraphQL服务器配合使用。在实践中,我们如何设置缓存头?

  Apollo支持GraphQL架构上的缓存提示。幸运的是,Apollo将收集查询中涉及的所有内容的所有提示,然后自动计算适当的整体缓存头值。例如,考虑以下查询:

  query userAvatarUrl { authenticatedUser { name avatar_url }}

  如果名称的最长期限是1天,而avatar_url的最长期限是1小时,则整个缓存的最长期限将是1小时,这是最小值。authenticatedUser取决于登录cookie,因此它需要一个私人提示,该提示将覆盖其他字段上的public,因此生成的标头将由缓存控制:max-age = 3600,私人。

  我将缓存提示支持添加到Keystone列表和字段。这是一个将文档中的缓存提示添加到待办事项列表演示中的字段的简单示例:

  const keystone = new Keystone({name:'Keystone To-Do List',adapter:new MongooseAdapter(),});梯形失真。createList('Todo', { schemaDoc: 'A list of things which need to be done', fields: { name: { type: Text, schemaDoc: 'This is the thing you need to do', isRequired: true, cacheHint: {scope:'PUBLIC',maxAge:3600,},},},});

  另一个问题:CORS

  基于API的网站中的跨域资源共享(CORS)规则和缓存可能会令人沮丧地发生冲突。

  在深入研究问题的细节之前,让我们看一下最简单的解决方案:将主站点和API放在同一个域中。如果您的网站和API是通过域提供的,则无需担心CORS规则(但您可能要考虑限制cookie)。如果您的API专用于此网站,则这是最干净的解决方案,您可以高高兴兴地跳过此部分。

  在Vocal V1中,网站(下一步。js)和平台(Keystone GraphQL)应用程序位于不同的域(语音)。媒体和API。声乐媒体)。为了保护用户免受恶意网站的侵害,现代浏览器不允许一个网站与另一个网站进行交互。因此,发声。媒体到api。声乐在媒体发出请求之前,浏览器将响应api。声乐媒体执行“飞行前”检查。这是使用OPTIONS方法的HTTP请求,该方法本质上是在询问跨域资源共享是否可行。飞行前检查之后,浏览器将发出最初准备发送的常规请求。

  “飞行前”检查也有其自身的问题,它们特定于每个URL。浏览器将为每个URL发出新的OPTIONS请求,并且服务器响应将应用于该URL。服务器无法发声。媒体就是所有api。声乐媒体请求的可信来源。当所有事情都是对api端点的POST请求时,这不是一个严重的问题,但是在为每个查询提供其自己的GET-able URL之后,每个查询都会遭受预检检查的延迟。

  更令人震惊的是,HTTP规范表示选项无法请求缓存,因此你会发现所有GraphQL数据都很好地缓存在用户旁边的CDN中,但是浏览器每次使用它时都得发出飞行前检查请求,一直到原始服务器。

  有几种解决方案(如果您不能仅使用共享域)。

  如果您的API非常简单,则可以使用CORS规则的例外。

  可以将某些缓存服务器配置为忽略HTTP规范,并始终缓存OPTIONS请求(例如,基于Varnish的缓存和AWS CloudFront)。它无法达到完全避免执行飞行前检查请求的性能水平,但比默认设置要好。

  另一个(确实非常可爱)选项是JSONP。当心:如果配置不当,可能会暴露出安全漏洞。

  让Vocal更好地利用缓存

  在底部进行HTTP缓存后,我需要使应用程序更好地利用它。

  HTTP缓存的局限性在于它在响应级别没有中间选项。大多数响应都是可缓存的,但是如果有一个字节无法缓存,则所有缓存都是无用的。

  作为博客平台,大多数Vocal数据易于缓存; 但在旧站点中,由于右上角有一个菜单栏,因此几乎没有可缓存的页面。对于匿名用户,菜单栏将显示一个链接,邀请用户登录或创建帐户。对于已登录的用户,此菜单将成为用户个人资料图片和个人资料菜单。由于页面根据用户的登录状态显示不同的内容,因此无法将其缓存在CDN中。

  

  声乐的典型页面。该页面的大多数内容都易于缓存,但是在旧站点中,由于右上角有一个小菜单,因此实际上所有页面都不是可缓存的。

  这些页面是由React组件的服务器端渲染(SSR)生成的。解决方案是找出所有依赖登录cookie的React组件,并强制它们仅在客户端显示。现在,服务器返回带有占位符的完全通用页面,该占位符用于登录菜单栏等。当页面加载到用户的浏览器中时,这些占位符将通过调用GraphQL API在客户端进行填充。普通页面可以安全地缓存在CDN中。

  该技术不仅可以提高缓存命中率,还可以帮助改善人们从心理上感知的页面加载时间。黑屏甚至是加载动画都会使我们不耐烦,但是一旦出现第一部分内容,它将使我们分心数百毫秒。如果人们单击社交媒体上Vocal帖子的链接,并且主要内容立即从CDN发送,那么很少有人会注意到,直到几百毫秒之后,某些组件才会完全互动。

  顺便说一句,为了更快地向用户呈现第一段内容,另一个技巧是在生成SSR响应时流式传输SSR响应,而不是在发送之前等待整个页面呈现。不幸的是,下一步。js尚不支持此方法。

  拆分响应以提高可缓存性的想法也适用于GraphQL。使用一个请求查询多个数据的能力是GraphQL的典型优势,但是如果响应的不同部分具有不同的可缓存性,则最好将它们分开。举一个简单的例子,Vocal的分页组件需要知道页面数和当前页面的内容。最初,组件在一个查询中获得了这两个组件,但是由于所有页面的总页数是恒定的,因此我将其设置为单独的查询来对其进行缓存。

  1。 缓存的好处

  缓存的明显好处是可以减少Vocal后端服务器上的负载。这固然很好,但是依靠缓存来增加容量是非常危险的,因为当您最终希望有一天删除缓存时,仍然需要备份计划。

  改进的响应能力是启用缓存的更好原因。

  其他一些好处可能并不明显。高峰流量通常高度集中。如果拥有大量社交媒体关注者的人共享指向该页面的链接,Vocal将获得大量访问量,但是大部分访问量只会访问该页面及其资产。这就是为什么高速缓存擅长吸收最大的流量峰值,从而使后端流量模式相对更平滑,更便于自动缩放。

  另一个好处是中等程度的降解。即使后端由于某种原因遇到严重麻烦,该网站仍将通过CDN缓存提供最受欢迎的部分。

  其他性能改进

  就像我经常说的那样,扩展的秘诀不是使事情复杂化,而是防止事情变得不必要的复杂,然后彻底解决所有阻止扩展的问题。

  这是一个提示:对于调试分布式系统中的问题,最困难的部分通常是获取正确的数据以查看正在发生的情况。很多时候,当我遇到麻烦时,我只是不断地调整和调整,依靠猜测而不是思考如何找到正确的数据。有时这是可行的,但不适用于棘手的问题。

  一个相关的技巧是,你可以获取系统中每个组件的实时数据(甚至只是tail -F下一个日志文件),把它们放在一个监视器中的几个窗口中,然后在另一个监视器中单击该站点以查看这些数据中的更改,因此您可以学到很多。例如:“为什么切换此复选框会在后端生成数十个数据库查询?”

  这是一个解决方案的示例。某些页面需要花费几秒钟来呈现,但是仅在部署环境中且仅在SSR中。监视仪表板没有显示CPU使用率的任何峰值,并且该应用程序未使用磁盘,这意味着该应用程序可能正在等待网络请求,也许正在等待后端。在开发环境中,我可以通过sysstat工具观察应用程序的工作状态,以记录CPU / RAM /磁盘使用情况,Postgres语句日志记录和常规应用程序日志。节点。js支持使用bpftrace之类的工具来跟踪HTTP请求的探针,但因为一些很蠢的原因,它们在开发环境中不起作用,所以我在源代码中找到了探针,并使用请求日志记录构建了自定义的节点。js构建。我使用tcpdump记录了网络数据,发现了一个问题:对于网站提出的每个API请求,都与平台建立了新的网络连接。(如果该方法仍然找不到原因,我想我将在应用程序中添加请求跟踪。)

  在本地计算机上的网络连接速度很快,但是在实际网络上花费的时间不能忽略。设置加密的连接(例如在生产环境中)花费的时间甚至更长。如果要向服务器发出大量请求(例如API),则应保持连接处于启用状态并尽可能重用它。浏览器将自动执行此操作,但默认情况下是Node。无法启用js,因为它不知道您是否在发出更多请求。这就是为什么仅在SSR中出现问题的原因。像许多长时间的调试会话一样,最终的修复实际上很简单:只需配置SSR即可保持连接处于活动状态。较慢页面的渲染时间已大大减少。

  如果您想了解有关此类内容的更多信息,强烈建议您阅读“高性能浏览器网络手册”(可免费在线阅读),并遵循Brendan Gregg发布的指南。

  概要

  实际上,我们可以做很多事情来改善Vocal的性能,但是我们还没有做到所有。在一家初创公司中,SRE工作与在一家大公司中作为永久雇员的SRE工作之间存在很大差异。我们对目标,预算和发布日期有限制。 Vocal V2已经运行了9个月,并保持了健康的增长速度。

  原始链接:

  https:// theartofmachinery。com / 2020/06/29 / scaling_a_graphql_site。html

  跟随我并转发本文,给我发私人短信“接收信息”,您可以免费获得价值4999元的InfoQ迷你书,点击文章末尾的“了解更多”,可以转到InfoQ官方网站 获取最新信息?

-