我们今天发布的翻译材料专门讲述了Airbnb如何优化Web应用程序的服务器部分,并着眼于服务器渲染技术的不断使用。 在过去的几年中,该公司逐渐将其整个前端转变为
统一的架构,根据该架构,网页是React组件的层次结构,其中填充了来自其API的数据。 特别是在此过程中,系统地放弃了Ruby on Rails。 实际上,Airbnb计划切换到仅基于Node.js的新服务,这将使服务器上呈现的充分准备好的页面能够交付给用户的浏览器。 该服务将为所有Airbnb产品生成大多数HTML代码。 由于该渲染引擎不是用Ruby或Java编写的,因此与该公司使用的大多数后端服务不同。 但是,它与传统的高负载Node.js服务不同,后者围绕Airbnb中使用的思维模型和辅助工具进行构建。

Node.js平台
考虑到Node.js平台,您可以想象在考虑该平台的异步数据处理功能的情况下构建的某个应用程序如何快速有效地服务于成百上千的并行连接。 该服务从任何地方提取所需的数据并对其进行少量处理,从而满足大量客户的需求。 这样的应用程序的所有者没有理由抱怨,他对他使用的同时进行数据处理的轻量级模型很有信心(在本材料中,我们使用“同时”一词来表达“并发”,即“并行”-“并行”一词)。 她完美地为她解决了任务。
服务器端渲染(SSR)更改了基本概念,导致对该问题有类似的看法。 因此,服务器渲染需要大量的计算资源。 Node.js环境中的代码在单个线程中执行,因此,为解决计算问题(与I / O任务不同),该代码可以同时执行,但不能并行执行。 Node.js平台能够处理大量并行I / O操作,但是,在计算方面,情况发生了变化。
因为当应用服务器端渲染时,请求处理任务的计算部分比与输入/输出有关的部分增加,因此,由于请求竞争处理器资源,因此传入的请求将影响服务器的响应速度。 应该注意的是,当使用异步渲染时,仍然存在对资源的竞争。 异步呈现解决了流程或浏览器的响应问题,但并没有通过延迟或并发改善情况。 在本文中,我们将重点介绍一个仅包含计算负载的简单模型。 如果我们谈论混合负载,包括输入/输出和计算操作,那么同时传入的请求将增加延迟,但要考虑到更高的系统吞吐量的优势。
考虑形式为
Promise.all([fn1, fn2])
。 如果
fn1
或
fn2
是由I / O子系统解决的承诺,则在执行此命令期间可以实现操作的并行执行。 看起来像这样:
通过输入/输出子系统并行执行操作如果
fn1
和
fn2
是计算任务,则它们将按以下方式执行:
计算任务其中一个操作将不得不等待第二个操作的完成,因为Node.js中只有一个线程。
在服务器渲染的情况下,当服务器进程必须处理多个同时的请求时,会发生此问题。 此类请求的处理将被延迟,直到处理较早收到的请求为止。 这是它的外观。
处理并发请求实际上,请求处理通常包含许多异步阶段,即使它们涉及系统上的严重计算量。 随着处理此类请求的任务的交替,这可能导致更加困难的情况。
假设我们的查询由类似于以下任务的任务链组成:
renderPromise().then(out => formatResponsePromise(out)).then(body => res.send(body))
。 当一对这样的请求以很小的间隔到达系统时,我们可以观察到下图。
处理间隔很小的请求,这是处理器资源争夺的问题在这种情况下,处理每个请求所花的时间是处理单个请求所花时间的两倍。 随着同时处理的请求数量的增加,情况变得更糟。
此外,SSR实现的典型目标之一是能够在客户端和服务器上使用相同或非常相似的代码。 这些环境之间的严重区别在于,客户端环境本质上是一个客户端在其中运行的环境,而服务器环境从本质上来说是多客户端环境。 在客户端上运行良好的方法(例如单调或其他用于存储应用程序全局状态的方法)会导致错误,数据泄漏,并且在处理许多到达服务器的请求时通常会造成混乱。
这些功能在您需要同时处理多个请求的情况下成为问题。 在开发环境的舒适环境中,一切通常都可以在较低的负载下正常地进行,而开发环境则由一个程序员亲自使用。
这导致与经典Node.js应用程序示例截然不同的情况。 应该注意的是,我们将JavaScript运行时用于其中可用的丰富库集,这是由于浏览器支持它的事实,而不是出于其用于同时进行数据处理的模型的考虑。 在此应用程序中,同步数据处理的异步模型展示了它的所有缺点,而这些优点很少或根本没有被优点所弥补。
超新星项目教程
我们新的渲染服务Hyperloop将成为Airbnb用户将与之交互的主要服务。 因此,它的可靠性和性能在确保使用资源的便利性方面起着至关重要的作用。 在将Hyperloop引入生产环境时,我们会考虑使用早期服务器渲染系统
Hypernova所获得的经验。
超新星不像我们的新服务那样工作。 这是一个纯渲染系统。 从我们的单轨Rail服务(称为Monorail)中调用它,并且仅返回特定呈现组件的HTML代码段。 在许多情况下,此“代码段”代表页面的大部分,而Rails仅提供页面布局。 使用传统技术,可以使用ERB将页面的各个部分链接在一起。 但是,无论如何,Hypernova不会加载构成页面所需的任何数据。 这是Rails的任务。
因此,Hyperloop和Hypernova具有相似的计算性能。 同时,Hypernova作为生产服务并处理大量流量,为测试提供了良好的场所,从而使人们了解了Hypernova替代品在战斗条件下的表现。
超新星工作流程这是Hypernova的工作方式。 用户请求到达我们的主要Rails应用程序Monorail,该应用程序收集需要在页面上显示的React组件的属性,并通过传递这些属性和组件名称来向Hypernova发送请求。 Hypernova渲染具有属性的组件,以便生成需要返回到Monorail应用程序的HTML代码,然后将其嵌入到页面模板中,并将其全部发送回客户端。
将完成的页面发送给客户端在Hypernova中发生紧急情况(可能是错误或响应超时)时,有一个后备选项,使用该选项时,组件和它们的属性将被嵌入到页面中,而服务器上不会生成HTML,之后所有这些都将发送到客户端并在那里呈现希望成功。 这导致我们得出这样一个事实:我们并不认为Hypernova服务是系统的关键部分。 结果,我们可以允许发生一定数量的故障和触发超时的情况。 通过调整请求超时,我们基于观察值将它们设置为大约P95级别。 结果,系统以低于5%的基本超时响应率工作就不足为奇了。
在流量达到峰值的情况下,我们可以看到多达40%的对Hypernova的请求被Monorail中的超时关闭了。 在超新星方面,我们看到了
BadRequestError: Request aborted
峰值
BadRequestError: Request aborted
高度较低的
BadRequestError: Request aborted
。 此外,这些错误通常在正常情况下存在,而在正常操作中,由于解决方案的体系结构,其余错误不会特别明显。
峰值超时值(红线)由于我们的系统可以在没有Hypernova的情况下运行,因此我们没有过多关注这些功能,因此它们被视为烦人的琐事,而不是严重的问题。 我们通过平台的功能解释了这些问题,这是因为由于初始垃圾收集操作相当困难,代码编译和数据缓存的特殊性以及其他原因,导致应用程序的启动缓慢。 我们曾希望新的React或Node版本能够在性能上有所改善,从而减轻服务启动缓慢的缺点。
我怀疑发生的事情很可能是负载平衡不良或解决方案部署中出现问题的结果,这是由于流程上过多的计算负载而导致延迟增加。 我在系统中添加了一个辅助层,用于记录有关单个进程同时处理的请求数量的信息,以及记录该进程接收到多个处理请求的情况。
研究成果我们认为服务启动缓慢是造成延迟的原因,但实际上,问题是由并行请求争夺CPU时间引起的。 根据测量结果,发现请求在预期完成其他请求的处理上花费的时间与处理请求所花费的时间相对应。 另外,这意味着由于同时处理请求而导致的延迟增加看起来与由于代码的计算复杂度增加而导致的延迟增加相同,这导致处理每个请求时系统上的负载增加。
此外,这更明显地表明
BadRequestError: Request aborted
无法通过缓慢的系统启动来自信地解释。 该错误从请求正文的解析代码开始,并在服务器能够完全读取请求正文之前客户端取消请求时发生。 客户端停止工作,关闭了连接,剥夺了我们继续处理请求所需的数据。 发生这种情况的可能性更大,因为我们开始处理请求,然后事件循环被证明是另一个请求的阻塞渲染,然后我们返回到中断的任务以完成该任务,但结果是客户端向我们发送此请求的人已经断开连接,正在中止该请求。 此外,请求中发送给Hypernova的数据平均非常大,平均约为数百KB,这当然不会改善情况。
由于断开不等待响应的客户端而导致的错误我们决定通过使用一些我们具有丰富经验的标准工具来解决此问题。 我们正在谈论反向代理服务器(
nginx )和负载平衡器(
HAProxy )。
反向代理和负载平衡
为了利用多核处理器体系结构,我们使用内置的Node.js
集群模块运行了多个Hypernova进程。 由于这些过程是独立的,因此我们可以同时处理传入的请求。
并行处理同时到达的请求这里的问题在于,每个Node进程在处理一个请求的所有时间都完全忙,包括读取从客户端发送的请求的主体(在这种情况下,Monorail扮演着自己的角色)。 尽管我们可以同时在一个进程中读取许多查询,但是在呈现时,它会导致计算运算的交替。
节点进程资源的使用取决于客户端和网络速度。
作为此问题的解决方案,我们可以考虑使用缓冲反向代理服务器,该服务器将允许我们维护与客户端的通信会话。 这个想法的灵感来自于Unicorn Web服务器,我们将其用于Rails应用程序。 独角兽宣布
的原则可以完美地解释为什么会这样。 为此,我们使用了nginx。 Nginx将请求从客户端读取到缓冲区,并在完全读取请求后才将其传递到节点服务器。 该数据传输会话是通过环回接口或使用Unix域套接字在本地计算机上执行的,这比在单独的计算机之间传输数据要快得多,也更可靠。
Nginx缓冲请求,然后将其发送到节点服务器由于nginx现在正在处理读取请求,因此我们能够实现对Node进程的更统一加载。
使用Nginx均匀加载进程此外,我们使用nginx来处理一些不需要访问Node进程的请求。 我们服务的检测和路由层使用
/ping
请求,这些请求不会在系统上产生很大的负载来验证主机之间的通信。 在nginx中处理所有这些操作,可以为Node.js消除大量的额外工作负载(尽管很小)。
下一个改进涉及负载平衡。 我们需要就节点进程之间的请求分配做出明智的决定。
cluster
模块根据轮询算法分配请求,在大多数情况下,尝试绕过不响应请求的进程。 使用这种方法,每个进程都按优先级顺序接收请求。
cluster
模块分配连接,而不分配请求,因此所有这些都无法正常工作。 当使用持久连接时,情况变得更糟。 来自客户端的任何永久连接都绑定到单个特定的工作流程,这使任务的有效分配变得复杂。
当请求延迟的可变性较低时,循环算法很好。 例如,在以下情况下。
轮询算法和连接,通过该算法和连接可以稳定地接收请求当您必须处理不同类型的请求时,此算法已经不是很好,因为处理这些请求可能需要完全不同的时间成本。 即使有另一个能够处理这样的请求的进程,发送到某个进程的最新请求也被迫等待所有先前发送的请求的处理完成。
过程负载不均如果您更合理地分配上面显示的查询,您将得到类似于下图所示的查询。
通过线程合理分配请求使用这种方法,等待时间最小化,并且可以更快地发送对请求的响应。
这可以通过将请求放在队列中,然后仅在不忙于处理另一个请求时才将它们分配给一个进程来实现。 为此,我们使用HAProxy。
HAProxy和进程负载平衡当我们使用HAProxy平衡Hypernova上的负载时,我们完全消除了超时峰值以及
BadRequestErrors
错误。
同时请求也是正常操作期间延迟的主要原因;这种方法减少了此类延迟。 其后果之一是,现在只有2%的请求被超时关闭了,而没有5%的超时设置被关闭了。 我们设法从错误率40%的情况转移到2%的情况下触发超时的情况,这表明我们正在朝着正确的方向前进。 因此,今天我们的用户很少看到该网站的加载屏幕。 应该注意的是,系统稳定性对于我们来说特别重要,因为它有望过渡到一个新系统,该系统不具有Hypernova拥有的相同备份机制。
有关系统及其设置的详细信息
为了使所有这些工作正常,您需要配置nginx,HAProxy和Node应用程序。 这是使用nginx和HAProxy
的类似应用程序
的示例 ,通过分析它们,您可以了解所涉及系统的设备。 该示例基于我们在生产中使用的系统,但是它经过了简化和修改,因此可以代表无特权的用户在前台执行。 在生产中,应使用某种管理程序(我们使用runit,或更常见的是kubernetes)来配置所有内容。
nginx配置非常标准,它使用侦听端口9000的服务器,该服务器配置为将请求代理到HAProxy服务器,侦听端口9001(在我们的配置中,我们使用Unix域套接字)。
此外,此服务器拦截对
/ping
端点的请求,以直接为旨在检查网络连接的请求提供服务。 nginx ,
worker_processes
1, nginx — HAProxy Node-. , , , Hypernova, ( ). .
Node.js
cluster
. HAProxy,
cluster
, .
pool-hall . — , , ,
cluster
, .
pool-hall
, .
HAProxy , 9001 , 9002 9005. —
maxconn 1
, . . HAProxy ( 8999).
HAProxyHAProxy . ,
maxconn
.
static-rr
(static round-robin), , , . , round-robin, , , , , . , , . .
, , . ( ). , , , , . , , .
HAProxy
HAProxy. , , , . , , ( ) . , ,
cluster
. , .
ab
(Apache Benchmark) 10000 . - . :
ab -l -c <CONCURRENCY> -n 10000 http://<HOSTNAME>:9000/render
15 4- -,
ab
, . (
concurrency=5
), (
concurrency=13
), , (
concurrency=20
). , .
, -, . , . , , , , . , , , .
, — .
maxconn 1
, , .
HTTP TCP , , , . ,
maxconn
, . , , (, , ).
, , , , , , .
— , .
option redispatch
retries 3
, , , , , , . .
, - , . , . , , . 100 , 10 , , . , . ,
accept
.
, (
backlog ) , . SYN-ACK (
, , , ACK ). , , , , .
, , , , . , , 1.
maxconn
. 0 , , , , , . , . - , , .
abortonclose
, . ,
abortonclose
. nginx.
, , . ( ) , , , , , . HAProxy , , ( ). , , , HTML. , , . , , ( , , ). , , . , , , . HAProxy, MAINT HAProxy.
, , ,
server.close
Node.js , HAProxy , , , . , , , , , .
, ,
balance first
, (
worker1
) 15% , , ,
balance static-rr
. , «» . . (12 ), , , - . , , , «» «». .
, , Node
server.maxconnections
, ( , ), , , , . ,
maxconnection
, , , . JavaScript, ( ). , , , . , , , HAProxy Node , . , , .
, , , ,
.
总结
Node.js . , , , -. Node.js . , , , , , , , nginx HAProxy.
, Airbnb , Node.js .
亲爱的读者们! 您是否在项目中使用服务器端渲染?