Mozilla 身份团队的《Node.JS 假日季系列》的第二集,探索了针对计算密集型工作负载的最佳服务器应用程序架构。
这是 Lloyd Hilaiel 在 2012 年 Node Philly 上发表的同名简短演讲的文字版本,请点击此处观看视频。
由于 Node.JS 进程几乎完全运行在单个处理核心上,因此构建可扩展的服务器需要特别注意。
借助编写原生扩展的能力以及一套强大的进程管理 API,有多种方法可以设计并行执行代码的 Node.JS 应用程序:在本篇文章中,我们将评估这些可能的设计。
本文还介绍了 compute-cluster 模块:一个小型 Node.JS 库,它可以轻松管理一系列进程以分发计算。
问题
我们为 Mozilla Persona 选择了 Node.JS,并在其中构建了一个可以处理大量具有混合特征的请求的服务器。
我们的“交互式”请求执行的计算成本低,需要快速完成以保持 UI 的响应能力,而“批处理”操作需要大约半秒的处理器时间,并且可以在不损害用户体验的情况下延迟一段时间。
为了找到一个优秀的应用程序设计,我们查看了应用程序必须处理的请求类型,仔细思考了可用性和扩展成本,并提出了四个关键需求
- 饱和度:解决方案能够使用所有可用的处理器。
- 响应能力:我们的应用程序 UI 应始终保持响应能力。
- 优雅:当处理的流量超过我们的处理能力时,我们应该为尽可能多的用户提供服务,并立即向其余用户显示清晰的错误。
- 简单性:解决方案应该易于增量集成到现有的服务器中。
有了这些需求,我们就可以有意义地对比这些方法了
方法 1:直接在主线程上执行。
当计算在主线程上执行时,结果很糟糕
您无法饱和多个计算核心,并且反复半秒的交互式请求饥饿会导致您无法响应或优雅。
此方法唯一的好处是简单性
function myRequestHandler(request, response) {
// Let's bring everything to a grinding halt for half a second.
var results = doComputationWorkSync(request.somesuch);
}
在预期一次服务多个请求的 Node.JS 程序中进行同步计算是一个糟糕的想法。
方法 2:异步执行。
使用在后台运行的异步函数可以改善情况,对吧?
好吧,这取决于后台的确切含义
如果计算函数以在主线程上实际执行 JavaScript 或原生代码中的计算的方式实现,则性能与同步方法相比没有改善。
请看
function doComputationWork(input, callback) {
// Because the internal implementation of this asynchronous
// function is itself synchronously run on the main thread,
// you still starve the entire process.
var output = doComputationWorkSync(input);
process.nextTick(function() {
callback(null, output);
});
}
function myRequestHandler(request, response) {
// Even though this *looks* better, we're still bringing everything
// to a grinding halt.
doComputationWork(request.somesuch, function(err, results) {
// ... do something with results ...
});
}
关键点是,在 NodeJS 中使用异步 API 并不一定能产生能够使用多个处理器的应用程序。
方法 3:使用带线程库的异步执行!
如果使用以原生代码编写并巧妙实现的库,则可以在 NodeJS 中的不同线程中执行工作。
有很多例子,其中一个是 Nick Campbell 编写的优秀的 bcrypt 库,请点击此处访问他的 GitHub 主页。
如果您在四核机器上测试它,您会看到效果非常棒!吞吐量提高了四倍,充分利用了所有计算资源!如果您在 24 核处理器上执行相同的测试,您就不会那么高兴了:您会看到四个核心被充分利用,而其余核心处于空闲状态。
这里的问题是,该库正在使用 NodeJS 的内部线程池来解决它并非设计用于解决的问题,并且此线程池具有 硬编码的上限为 4。
除了这些硬编码限制之外,此方法还存在更深层次的问题
- 用计算工作淹没 NodeJS 的内部线程池会导致网络或文件操作饥饿,从而损害响应能力。
- 没有好的方法来控制积压——如果您的队列中已经有 5 分钟的计算工作,您真的想再添加更多工作吗?
以这种方式“内部线程化”的库无法饱和多个核心,会对响应能力产生不利影响,并限制应用程序在负载下优雅降级的能力。
方法 4:使用 node 的 cluster 模块!
NodeJS 0.6.x 及更高版本提供了一个 cluster 模块,它允许创建“共享侦听套接字”的进程,以在一些子进程之间平衡负载。
如果您将 cluster 与上述方法之一结合使用会怎样?
由此产生的设计将继承同步或内部线程化解决方案的缺点:它们不响应且缺乏优雅。
简单地旋转新的应用程序实例并不总是正确的答案。
方法 5:介绍 compute-cluster
我们在 Persona 中解决此问题的当前方案是管理一个用于计算的单用途进程集群。
我们在 compute-cluster 库中将此解决方案进行了概括。
compute-cluster
为您生成并管理进程,为您提供了一种以编程方式在本地子进程集群上运行工作的方法。
用法如下
const computecluster = require('compute-cluster');
// allocate a compute cluster
var cc = new computecluster({ module: './worker.js' });
// run work in parallel
cc.enqueue({ input: "foo" }, function (error, result) {
console.log("foo done", result);
});
cc.enqueue({ input: "bar" }, function (error, result) {
console.log("bar done", result);
});
文件 worker.js
应响应 message
事件以处理传入的工作
process.on('message', function(m) {
var output;
// do lots of work here, and we don't care that we're blocking the
// main thread because this process is intended to do one thing at a time.
var output = doComputationWorkSync(m.input);
process.send(output);
});
可以在不修改调用者的情况下,将 compute-cluster
集成到现有的异步 API 后面,并通过最少的代码更改开始在多个处理器上真正并行执行工作。
那么这种方法如何实现这四个标准呢?
饱和度:多个工作进程使用所有可用的处理核心。
响应能力:因为管理进程只做进程生成和消息传递,所以它保持空闲状态,并且可以将大部分时间用于处理交互式请求。
即使机器负载很高,操作系统调度程序也可以帮助优先处理管理进程。
简单性:集成到现有项目中很容易:通过将 compute-cluster
的细节隐藏在一个简单的异步 API 后面,调用代码可以愉快地忽略这些细节。
现在,在流量激增期间如何优雅降级呢?
同样,目标是在流量激增期间以最大效率运行,并为尽可能多的请求提供服务。
Compute cluster 通过管理的不仅仅是进程生成和消息传递来实现优雅的设计。
它跟踪正在运行的工作量以及工作平均完成所需的时间。
有了这种状态,就可以可靠地预测工作在排队之前需要多长时间才能完成。
将此知识与客户端提供的参数 max_request_time
相结合,可以预先拒绝可能需要比允许时间更长的时间才能完成的请求。
此功能使您可以轻松地将用户体验需求映射到您的代码中:“用户不应该等待超过 10 秒才能登录”,转换为大约 7 秒的 max_request_time
(加上网络时间填充)。
在 Persona 服务的负载测试中,到目前为止结果很有希望。
在极端负载下,我们可以允许已认证的用户继续使用服务,并在最开始就阻止一部分未认证的用户,并显示清晰的错误消息。
后续步骤
使用进程进行应用程序级并行化适用于单层部署架构——在这种架构中,您只有一种类型的节点,并且只需添加更多节点即可支持扩展。
但是,随着应用程序变得越来越复杂,部署架构可能会发展为拥有不同的应用程序层以支持性能或安全目标。
除了多个部署层之外,高可用性和扩展通常还需要在多个托管设施中部署应用程序。最后,可以通过利用按需云计算资源来实现计算绑定应用程序的经济高效的扩展。
在多个托管设施中拥有多个层和按需生成的云服务器会大大改变扩展问题的参数,而目标保持不变。
compute-cluster 的未来可能涉及能够在多个不同的层上分配工作,以便在负载期间最大程度地利用可用的计算资源。
这可能跨托管设施工作以支持地理位置不对称的流量激增。
这可能涉及能够利用按需生成的新的硬件…
或者我们可能会以不同的方式解决问题!如果您对一种优雅的方式有任何想法,可以让 compute-cluster 在网络上分配工作,同时保留其迄今为止具有的特性,我非常乐意听取您的意见!
感谢您的阅读,您可以加入讨论,并从 我们的邮件列表 中了解更多关于 Persona 中当前扩展挑战和方法的信息。
16 条评论