Chinaunix首页 | 论坛 | 博客
  • 博客访问: 147733
  • 博文数量: 174
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1800
  • 用 户 组: 普通用户
  • 注册时间: 2018-04-16 06:56
文章分类

全部博文(174)

文章存档

2020年(7)

2019年(29)

2018年(138)

我的朋友

分类: IT业界

2018-07-22 22:35:20

编程学习之如何在Node.js中优化服务器端渲染?[图]
在 Airbnb,我们花了数年时间将所有前端代码迁移到 React 架构,Ruby on Rails 在 Web 应用中所占的比例每天都在减少。实际上,我们很快会转向另一个新的服务,即通过 Node.js 提供完整的服务器端渲染页面。这个服务将为 Airbnb 的所有产品渲染大部分 HTML。这个渲染引擎不同于其他后端服务,因为它不是用 Ruby 或 Java 开发的,但它也不同于常见的 I/O 密集型 Node.js 服务。
一说起 Node.js,你可能就开始畅想着高度异步化的应用程序,可以同时处理成千上万个连接。你的服务从各处拉取数据,以迅雷不及掩耳之势处理好它们,然后返回给客户端。你可能正在处理一大堆 WebSocket 连接,你对自己的轻量级并发模型充满自信,认为它非常适合完成这些任务。
但服务器端渲染(SSR)却打破了你对这种美好愿景的假设,因为它是计算密集型的。Node.js 中的用户代码运行在单个线程上,因此可以并发执行计算操作(与 I/O 操作相反),但不能并行执行它们。Node.js 可以并行处理大量的异步 I/O,但在计算方面却受到了限制。随着计算部分所占比例的增加,开始出现 CPU 争用,并发请求将对延迟产生越来越大的影响。
以 Promise.all([fn1,fn2]) 为例,如果 fn1 或 fn2 是属于 I/O 密集型的 promise,就可以实现这样的并行执行:
如果 fn1 和 fn2 是计算密集型的,它们将像这样执行:
一个操作必须等待另一个操作完成后才能运行,因为只有一个执行线程。
在进行服务器端渲染时,当服务器进程需要处理多个并发请求,就会出现这种情况。正在处理中的请求将导致其他请求延迟:
在实际当中,请求通常由许多不同的异步阶段组成,尽管仍然以计算为主。这可能导致更糟糕的交叉。如果我们的请求包含一个像 renderPromise().then(out => formatResponsePromise(out)).then(body => res.send(body)) 这样的链,那么请求的交叉可能是这样的:
在这种情况下,两个请求都需要两倍的时间才能处理完成。随着并发的增加,这个问题将变得更加严重。
SSR 的一个目标是能够在客户端和服务器上使用相同或类似的代码。这两种环境之间存在一个巨大的差异,客户端上下文本质上是单租户的,而服务器上下文却是多租户的。在客户端可以正常运行的东西,比如单例或全局状态,到了服务器端就会导致 bug、数据泄漏和各种混乱。
这两个问题都与并发有关。在负载水平较低时,或在开发环境当中,一切都正常。
这与 Node 应用程序的情况完全不同。我们之所以使用 JavaScript 运行时,是因为它提供的库支持和对浏览器的支持,而不是因为它的并发模型。上述的示例表明,异步并发模型所带来的成本已经超出了它所能带来的好处。
从 Hypernova 中学到的教训:
我们的新渲染服务 Hyperloop 将成为 Airbnb 用户的主要交互服务。因此,它的可靠性和性能对用户体验来说至关重要。随着逐渐在生产环境中使用新服务,我们将参考从早期 SSR 服务 Hypernova 中吸取到的教训。
Hypernova 的工作方式与新服务不同。它是一个纯粹的渲染器,Rails 单体应用 Monorail 会调用它,它返回渲染组件的 HTML 片段。在大多数情况下,“片段”是整个页面的一部分,Rails 只提供外部布局。页面上的各个部分可以使用 ERB 拼接在一起。但是,不管是哪一种情况,Hypernova 都不获取数据,数据由 Rails 提供。
也就是说,在计算方面,Hyperloop 和 Hypernova 具有类似的操作特性,而 Hypernova 提供了良好的测试基础,可以帮助我们理解生产环境中的页面内容是如何进行替换的。
用户请求进入我们的 Rails 主应用程序 Monorail,它为需要进行渲染的 React 组件组装 props,并向 Hypernova 发送带有这些 props 和组件名称的请求。Hypernova 使用收到的 props 来渲染组件,生成 HTML 并返回给 Monorail,Monorail 将 HTML 片段嵌入到页面模板中,并将所有内容发送给客户端。
如果 Hypernova 渲染失败(由于错误或超时),就将组件及 props 嵌入页面,或许它们可以成功地在客户端渲染。因此,我们认为 Hypernova 是一个可选的依赖项,我们能够容忍一些超时和失败。我根据 SLA p95 来设置超时时间,不出所料,我们的超时基线略低于 5%。
在高峰流量负载期间进行部署时,我们可以看到从 Monorail 到 Hypernova 最多有 40%的请求超时。我们可以从 Hypernova 中看到 BadRequestError:aborted 的错误率峰值。
部署超时峰值示例:
我们把这些超时和错误归因于缓慢的启动时间,如 GC 启动初始化、缺少 JIT、填充缓存等等。新发布的 React 或 Node 有望提供足够的性能改进,以缓解启动缓慢的问题。
我怀疑这可能是由于不良的负载均衡或部署期间的容量问题造成的。当我们在同一个进程上同时运行多个计算请求时,我们看到了延迟的增加。我添加了一个中间件来记录进程同时处理的请求数。
我们将启动延迟归咎于并发请求等待 CPU。从我们的性能指标来看,我们无法区分用于等待执行的时间与用于实际处理请求的时间。这也意味着并发性带来的延迟与新代码或新特性带来的延迟是相同的——这些实际上都会增加单个请求的处理成本。
很明显,我们不能将 BadRequestError:Request aborted 错误归咎于启动延迟。这个错误来自消息解析器,特别在服务器完全读取请求消息体之前,客户端中止了请求。客户端关闭了连接,我们无法拿到处理请求所需的宝贵数据。发生这种情况的可能性更大,比如:我们开始处理请求,然后事件循环被另一个请求渲染阻塞,当回到之前被中断的地方继续处理时,发现客户端已经消失了。Hypernova 的请求消息体也很大,平均有几百千字节,这样只会让事情变得更糟。
我们决定使用两个现有的组件来解决这个问题:反向代理(Nginx)和负载均衡器(HAProxy)。
反向代理和负载均衡:
为了充分利用 Hypernova 实例上的多核 CPU,我们在单个实例上运行多个 Hypernova 进程。因为这些是独立的进程,所以能够并行处理并发请求。
问题是每个 Node 进程将在整个请求时间内被占用,包括从客户端读取请求消息体。虽然我们可以在单个进程中并行读取多个请求,但在渲染时,这会导致计算操作交叉。因此,Node 进程的使用情况取决于客户端和网络的速度。
解决办法是使用缓冲反向代理来处理与客户端的通信。为此,我们使用了 Nginx。Nginx 将客户端的请求读入缓冲区,并在完全读取后将完整请求传给 Node 服务器。摘抄好词好句及感悟赏析,这个传输过程是在本地机器上进行的,使用了回送或 unix 域套接字,这比机器之间的通信更快、更可靠。
通过使用 Nginx 来处理读取请求,我们能够实现更高的 Node 进程利用率。
我们还使用 Nginx 来处理一部分请求,不需要将它们发送给 Node.js 进程。我们的服务发现和路由层通过 /ping 低成本请求来检查主机之间的连接性。在 Nginx 中处理这些可以降低 Node.js 进程的吞吐量。
接下来是负载均衡。我们需要明智地决定哪些 Node.js 进程应该接收哪些请求。cluster 模块通过 round-robin 算法来分配请求,当请求延迟的变化很小时,这种方式是很好的,例如:
但是当有不同类型的请求需要花费不同的处理时间时,它就不那么好用了。后面的请求必须等待前面的请求全部完成,即使有另一个进程可以处理它们。
更好的分发模型应该像这样:
因为这可以最大限度地减少等待时间,并可以更快地返回响应。
这可以通过将请求放进队列中并只将请求分配给空闲的进程来实现。为此,我们使用了 HAProxy。
当我们在 Hypernova 中实现了这些,就完全消除了部署时的超时峰值以及 BadRequestError 错误。并发请求也是造成延迟的主要因素,随着新方案的实施,延迟也降低了。在使用相同的超时配置的情况下,超时率基线从 5%变为 2%。部署期间的 40%失败也降低到了 2%,这是一个重大的胜利。现在,用户看到空白页的几率已经很低了。未来,部署稳定性对于我们的新渲染器来说至关重要,因为新渲染器没有 Hypernova 的回滚机制。作者:无明
阅读(367) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~