崩溃和重新启动进程不是处理错误甚至错误的有效策略。在 Erlang 中会很好,其中一个进程很便宜并且只做一件孤立的事情,比如为单个客户端提供服务。这不适用于节点,其中一个流程的成本要高出几个数量级,并且一次为数千个客户提供服务
假设您的服务每秒处理 200 个请求。如果其中 1% 的代码在你的代码中遇到了抛出路径,那么你每秒将获得 20 次进程关闭,大约每 50 毫秒一次。如果您有 4 个内核,每个内核有 1 个进程,那么您将在 200 毫秒内丢失它们。因此,如果一个进程启动和准备服务请求的时间超过 200 毫秒(不加载任何模块的节点进程的最低成本约为 50 毫秒),我们现在就成功地进行了完全拒绝服务。更不用说用户遇到错误往往会做诸如反复刷新页面之类的事情,从而使问题更加复杂。
域不能解决问题,因为它们不能确保资源不泄露。
阅读问题#5114和#5149中的更多信息。
现在您可以尝试对此“聪明”一点,并根据一定数量的错误制定某种进程回收策略,但是无论您采用哪种策略,都会严重改变 node.js 的可伸缩性配置文件。我们说的是每个进程每秒几十个请求,而不是几千个。
然而,promise 捕获所有异常,然后以与同步异常向上传播堆栈的方式非常相似的方式传播它们。此外,它们通常提供一种finally
等效于这两个功能的方法,我们可以通过构建“上下文管理器”(类似于在python、C#或Java中)try...finally
来封装清理逻辑,它总是清理资源。with
using
try-with-resources
让我们假设我们的资源被表示为具有acquire
和dispose
方法的对象,它们都返回承诺。调用函数时没有建立连接,我们只返回一个资源对象。该对象将由稍后处理using
:
function connect(url) {
return {acquire: cb => pg.connect(url), dispose: conn => conn.dispose()}
}
我们希望 API 像这样工作:
using(connect(process.env.DATABASE_URL), async (conn) => {
await conn.query(...);
do other things
return some result;
});
我们可以很容易地实现这个 API:
function using(resource, fn) {
return Promise.resolve()
.then(() => resource.acquire())
.then(item =>
Promise.resolve(item).then(fn).finally(() =>
// bail if disposing fails, for any reason (sync or async)
Promise.resolve()
.then(() => resource.dispose(item))
.catch(terminate)
)
);
}
fn
在 using 的参数中返回的承诺链完成后,资源将始终被处理掉。即使在该函数(例如 from JSON.parse
)或其内部.then
闭包(如 second JSON.parse
)中抛出错误,或者如果链中的承诺被拒绝(相当于回调调用错误)。这就是为什么承诺捕获错误并传播它们如此重要的原因。
然而,如果处理资源真的失败了,那确实是一个很好的终止理由。在这种情况下,我们极有可能泄漏了资源,并且开始结束该过程是一个好主意。但是现在我们崩溃的机会被隔离到我们代码的一小部分 - 实际处理可泄漏资源的部分!
注意:终止基本上是带外抛出,因此承诺无法捕获它,例如process.nextTick(() => { throw e });
. 哪种实现有意义可能取决于您的设置——基于 nextTick 的实现类似于回调保释的方式。
使用基于回调的库怎么样?它们可能不安全。让我们看一个示例,看看这些错误可能来自哪里以及哪些可能导致问题:
function unwrapped(arg1, arg2, done) {
var resource = allocateResource();
mayThrowError1();
resource.doesntThrow(arg1, (err, res) => {
mayThrowError2(arg2);
done(err, res);
});
}
mayThrowError2()
在内部回调中,如果它抛出仍然会使进程崩溃,即使unwrapped
在另一个 Promise 中调用.then
。这些类型的错误不会被典型的promisify
包装器捕获,并且会像往常一样继续导致进程崩溃。
但是,mayThrowError1()
如果在内部调用,将被 promise 捕获.then
,并且内部分配的资源可能会泄漏。
我们可以编写一个偏执的版本,promisify
以确保任何抛出的错误都无法恢复并使进程崩溃:
function paranoidPromisify(fn) {
return function(...args) {
return new Promise((resolve, reject) =>
try {
fn(...args, (err, res) => err != null ? reject(err) : resolve(res));
} catch (e) {
process.nextTick(() => { throw e; });
}
}
}
}
现在,在另一个 Promise 的回调中使用 Promisified 函数.then
会导致进程崩溃,如果解包抛出,则退回到 throw-crash 范式。
一般希望随着您使用越来越多的基于 Promise 的库,他们会使用上下文管理器模式来管理他们的资源,因此您不需要让进程崩溃。
这些解决方案都不是万无一失的——甚至不会因抛出的错误而崩溃。尽管不抛出,但很容易意外编写泄漏资源的代码。例如,这个节点样式的函数会泄漏资源,即使它没有抛出:
function unwrapped(arg1, arg2, done) {
var resource = allocateResource();
resource.doSomething(arg1, function(err, res) {
if (err) return done(err);
resource.doSomethingElse(res, function(err, res) {
resource.dispose();
done(err, res);
});
});
}
为什么?因为当doSomething
's 的回调收到错误时,代码会忘记释放资源。
上下文管理器不会发生此类问题。您不能忘记调用 dispose:您不必这样做,因为using
它为您服务!
参考:为什么我要切换到 promises、上下文管理器和事务