崩溃和重新启动进程不是处理错误甚至错误的有效策略。在 Erlang 中会很好,其中一个进程很便宜并且只做一件孤立的事情,比如为单个客户端提供服务。这不适用于节点,其中一个流程的成本要高出几个数量级,并且一次为数千个客户提供服务
假设您的服务每秒处理 200 个请求。如果其中 1% 的代码在你的代码中遇到了抛出路径,那么你每秒将获得 20 次进程关闭,大约每 50 毫秒一次。如果您有 4 个内核,每个内核有 1 个进程,那么您将在 200 毫秒内丢失它们。因此,如果一个进程启动和准备服务请求的时间超过 200 毫秒(对于不加载任何模块的节点进程,最低成本约为 50 毫秒),我们现在有一个成功的 total 拒绝服务.更不用说遇到错误的用户往往会做一些事情,例如反复刷新页面,从而使问题更加复杂。
域名无法解决问题,因为它们cannot ensure that resources are not leaked。
阅读问题#5114 和#5149 了解更多信息。
现在您可以尝试对此“聪明”一点,并根据一定数量的错误制定某种进程回收策略,但无论您采用何种策略,它都会严重改变 node.js 的可伸缩性配置文件。我们说的是每个进程每秒几十个请求,而不是几千个。
然而,promise 捕获所有异常,然后以与同步异常向上传播堆栈的方式非常相似的方式传播它们。此外,他们通常提供一个方法finally,它相当于try...finally。由于这两个功能,我们可以通过构建“上下文管理器”来封装清理逻辑(类似于@中的with 987654324@, using in C# 或 try-with-resources in Java) 总是清理资源。
假设我们的资源被表示为具有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)
)
);
}
在 using 的 fn 参数中返回的承诺链完成后,资源将始终被处置。即使在该函数中抛出错误(例如来自JSON.parse)或其内部.then闭包(如第二个JSON.parse),或者如果链中的promise被拒绝(相当于回调调用错误) .这就是为什么承诺捕获错误并传播它们如此重要的原因。
如果处理资源真的失败了,那确实是一个很好的终止理由。在这种情况下,我们极有可能泄漏了资源,并且开始结束该过程是一个好主意。但是现在我们崩溃的机会被隔离到我们代码的一小部分 - 实际处理可泄漏资源的部分!
注意:终止基本上是抛出带外,因此承诺无法捕获它,例如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() 在一个内部回调中,如果它抛出,仍然会导致进程崩溃,即使在另一个 Promise 的 .then 中调用了 unwrapped。这些类型的错误不会被典型的promisify 包装器捕获,并且会像往常一样继续导致进程崩溃。
但是,如果在.then 内调用mayThrowError1(),则承诺会捕获mayThrowError1(),并且内部分配的资源可能会泄漏。
我们可以编写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 的 .then 回调中使用 promisified 函数现在会导致进程崩溃,如果解包 throws,则退回到 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的回调收到错误时,代码会忘记释放资源。
上下文管理器不会发生这种问题。您不能忘记调用 dispose:您不必这样做,因为 using 会为您完成!
参考:why I am switching to promises、context managers and transactions