共计 2658 个字符,预计需要花费 7 分钟才能阅读完成。
原文:Operationalizing Node.js for Server Side Rendering
在 Airbnb,咱们花了数年工夫将所有前端代码稳固地迁徙到统一的架构中,在该架构中,整个网页都被编写为 React 组件的层次结构,其中蕴含来自咱们 API 的数据。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,但会遇到计算限度。随着申请的计算局部绝对于 I/O 的减少,并发申请将对提早产生越来越大的影响,因为 CPU 争用。
思考 Promise.all([fn1, fn2])。如果 fn1 或 fn2 是由 I/O 解析的承诺,您能够像这样实现并行性:
如果 fn1 和 fn2 是计算的,它们将改为这样执行:
一个操作必须期待另一个实现能力运行,因为只有一个执行线程。
对于服务器端渲染,当服务器过程解决多个并发申请时会呈现这种状况。并发申请将被正在解决的其余申请提早:
在实践中,申请通常由许多不同的异步阶段组成,即便依然次要是计算。这可能导致更蹩脚的交错。如果咱们的申请由一个像 renderPromise().then(out => formatResponsePromise(out)).then(body => res.send(body)) 这样的链组成,咱们能够有像这样的申请交织:
在这种状况下,两个申请最终都会破费两倍的工夫。随着并发性的减少,这个问题变得更糟。
此外,SSR 的独特指标之一是可能在客户端和服务器上应用雷同或类似的代码。这些环境之间的一个很大区别是客户端上下文实质上是单租户,而服务器上下文是多租户的。在客户端轻松工作的技术(如单例或其余全局状态)将导致服务器上并发申请负载下的谬误、数据透露和个别凌乱。
这两个问题只会成为并发问题。在较低的负载程度下或在您的开发环境的舒服繁多租户中,所有通常都能失常工作。
这导致了与 Node 应用程序的标准示例齐全不同的状况。咱们应用 JavaScript 运行时是因为它的库反对和浏览器个性,而不是它的并发模型。在这个应用程序中,异步并发模型强加了它的所有老本,没有或只有很少的益处。
一些教训分享
用户发送申请到咱们的次要 Rails 应用程序 Monorail,它将心愿在任何给定页面上出现的 React 组件的 props 拼凑在一起,并应用这些 props 和组件名称向 Hypernova 发出请求。Hypernova 应用 props 渲染组件以生成 HTML 以返回到 Monorail,而后将其嵌入到页面模板中并将整个内容发送回客户端。
在 SSR 渲染失败(因为谬误或超时)的状况下,回退是将组件及其道具嵌入页面而不渲染 HTML,容许它们(心愿)被客户端胜利渲染。这导致咱们将 SSR 视为一种可选的依赖项,并且咱们可能容忍肯定数量的超时和失败。咱们将调用超时设置为大概在咱们调整值时察看到的值。不出所料,咱们以略低于 5% 的超时基线运行。
在日常流量负载高峰期进行部署时,咱们会看到高达 40% 的 SSR 申请产生超时。相似 BadRequestError: Request aborted on deploys 的这些谬误,覆盖了所有其余应用程序 / 编码谬误。
咱们曾将提早归咎于启动提早,而提早实际上是由并发申请互相期待以应用 CPU 造成的。从咱们的性能指标来看,因为其余正在运行的申请而期待执行所破费的工夫与执行申请所破费的工夫无奈辨别。这也意味着并发导致的提早减少看起来与新代码门路或性能导致的提早减少雷同——实际上减少了任何单个申请的老本。
BadRequestError: Request aborted 谬误也变得越来越显著,不能用个别的慢启动性能来解释。该谬误来自注释解析器,特地是在客户端在服务器可能齐全读取申请注释之前停止申请的状况下产生。客户端放弃并敞开连贯,带走咱们持续解决申请所需的贵重数据。产生这种状况的可能性要大得多,因为咱们开始解决一个申请,而后咱们的事件循环被另一个申请的渲染阻塞,而后从咱们被中断的中央返回实现,却发现客户端曾经来到了。
咱们决定通过应用咱们领有大量现有操作教训的两个现成组件来解决这个问题:反向代理 (nginx) 和负载均衡器 (haproxy)。
Reverse Proxying and Load Balancing
为了利用咱们的 SSR 服务器上存在的多个 CPU 内核,咱们通过内置的 Node.js 集群模块运行多个 SSR 过程。因为这些是独立的过程,咱们可能并行处理并发申请。
这里的问题是每个节点过程在申请的整个持续时间内都被无效占用,包含从客户端读取申请注释。
尽管咱们能够在单个过程中并行读取多个申请,但这会导致在进行渲染时计算操作的交织。
节点过程的应用与客户端和网络的速度耦合。
解决方案是应用缓冲反向代理来解决与客户端的通信。为此,咱们应用 nginx。Nginx 将来自客户端的申请读入缓冲区,并在齐全读取后将残缺申请传递给节点服务器。
这种传输通过环回或 unix 域套接字在机器上本地产生,这比机器之间的通信更快、更牢靠。
通过 nginx 解决读取申请,咱们可能实现节点过程的更高利用率。
总结
服务器端渲染代表与 Node.js 善于的标准的、次要是 I/O 工作负载不同的工作负载。理解异样行为的起因使咱们可能应用咱们领有现有操作教训的现成组件来解决它。
异步渲染依然存在资源争用。异步渲染解决过程或浏览器的响应问题,但不解决并行性或提早问题。这篇翻译的博文重点介绍的是纯计算工作负载的简略模型。对于 IO 和计算的混合工作负载,申请并发会减少提早,但具备容许更高吞吐量的益处。
更多 Jerry 的原创文章,尽在:” 汪子熙 ”: