部署Node.js应用程序以完成服务器端渲染的性能调整,其中整个网页被编写为包含来自我们API的数据的React组件层次结构。RubyonRails在将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渲染失败(由于错误或超时),回退是将组件及其props嵌入页面而不渲染HTML,允许它们(希望)被客户端成功渲染。这导致我们将SSR视为可选依赖项,并且我们能够容忍一定数量的超时和失败。我们将调用超时设置为大约我们在调整值时观察到的值。正如预期的那样,我们以略低于5%的超时基线运行。在每日高峰流量负载期间进行部署时,我们发现多达40%的SSR请求超时。BadRequestError:Requestabortedondeploys等错误掩盖了所有其他应用程序/编码错误。我们将延迟归咎于启动延迟,而延迟实际上是由等待彼此使用CPU的并发请求引起的。从我们的性能指标来看,由于其他正在运行的请求而等待执行所花费的时间与执行请求所花费的时间无法区分。这也意味着由于并发而增加的延迟看起来与由于新代码路径或功能而增加的延迟相同-有效地增加了任何单个请求的成本。BadRequestError:Requestaborted错误也越来越明显,不能用一般的慢启动性能来解释。错误来自文字解析器,特别是当客户端在服务器能够完全读取请求文字之前中止请求时。客户端放弃并关闭连接,带走我们继续处理请求所需的宝贵数据。这更有可能发生,因为我们开始处理一个请求,然后我们的事件循环被另一个请求的渲染阻塞,然后返回到我们离开的地方完成,却发现客户端已经离开了。我们决定使用两个现成的组件来解决这个问题,我们有很多现有的操作经验:一个反向代理(nginx)和一个负载均衡器(haproxy)。反向代理和负载平衡为了利用我们SSR服务器上存在的多个CPU内核,我们通过内置的Node.js集群模块运行多个SSR进程。由于这些是独立的进程,我们能够并行处理并发请求。这里的问题是每个节点进程在整个请求期间都被有效占用,包括从客户端读取请求文本。虽然我们可以在一个进程中并行读取多个请求,但这会导致渲染时计算操作的交错。节点进程的使用与客户端和网络的速度有关。解决方案是使用缓存反向代理来处理与客户端的通信。为此,我们使用nginx。Nginx将来自客户端的请求读入缓冲区,并在完全读取后将完整的请求传递给节点服务器。这种传输通过环回或unix域套接字在机器本地发生,这比机器之间的通信更快、更可靠。通过nginx处理读取请求,我们能够实现节点进程的更高利用率。总结服务器端渲染代表了一种不同于规范的工作负载,主要是Node.js擅长的I/O工作负载。了解异常行为的原因使我们能够使用我们拥有现有操作经验的现成组件来解决它。异步渲染仍然存在资源争用。异步渲染解决了进程或浏览器的响应性问题,但没有解决并行性或延迟问题。这篇翻译后的博客文章重点介绍了纯计算工作负载的简单模型。对于IO和计算的混合工作负载,请求并发会增加延迟,但具有允许更高吞吐量的好处。更多Jerry原创文章在这里:《王子熙》:
