处理Http服务器崩溃

5

我有一个非常基础的http服务器:

require("http").createServer(function (req, res) {
    res.end("Hello world!");                      
}).listen(8080);                                 

我如何监听服务器崩溃以便在响应中发送 500 状态码?
process 层面上,监听 process.on("uncaughtException", handler) 可以实现,但是我没有请求和响应对象。可能的解决方案是在 createServer 回调函数内部使用 try-catch 语句,但我正在寻找是否有更好的解决方案。我尝试监听 server 对象上的 error 事件,但是没有任何反应。
var s = require("http").createServer(function (req, res) {
    undefined.foo; // test crash
    res.end("Hello world!");                      
});
s.on("error", function () { console.log(arguments); });
s.listen(8080);                                 

1
在风险部分或调用外部函数中的风险部分的行上使用try/catch。 - dandavis
1
关于此主题的更多好文章:https://dev59.com/TGkv5IYBdhLWcg3wvDNF和https://www.joyent.com/developers/node/design/errors。你还可以安装一个express默认的错误处理程序。 - jfriend00
@jfriend00 不,我不会用Express做那个。我看到其他关于Express的问题,难道没有其他解决方案吗? - Ionică Bizău
1
你需要在正确的位置捕获异常(在每个请求中,只要有可用的信息来正确处理它)。这就是它的工作原理。问题解决了。添加适当的错误处理。这里没有免费的午餐。哦,顺便说一下,你还需要确保没有从异步回调中抛出异常,因为即使在请求级别的异常处理程序中也无法捕获它们 - 你必须在回调中捕获它们。 - jfriend00
1
您可以考虑使用 domain 模块。 - Paramore
显示剩余7条评论
1个回答

9

捕获和处理错误

您可以使用Node内置的domain模块来实现。

域提供了一种将多个不同的IO操作作为单个组处理的方式。如果任何事件发射器或回调函数向域发出错误事件,或者抛出错误,则域对象将被通知,而不是在process.on('uncaughtException')处理程序中丢失错误上下文,或者导致程序立即退出并带有错误代码。

需要注意的一件非常重要的事情是:

域错误处理程序不能替代当错误发生时关闭进程。

由于JavaScript中throw的工作方式的本质,几乎没有安全的“从上次离开的地方”继续执行的方法,而不会泄漏引用或创建某种其他未定义的脆弱状态。

由于您只询问如何使用500错误进行响应,因此我不会像Node文档那样详细介绍如何处理重新启动服务器等内容;我强烈建议查看节点文档中的示例。他们的示例展示了如何捕获错误,将错误响应发送回客户端(如果可能),然后重新启动服务器。我只会展示域创建和发送500错误响应。 (有关重新启动进程的内容请参见下一节)

域的工作方式类似于在createServer回调中放置try/catch。在您的回调中:

  1. 创建一个新的域对象
  2. 侦听域的error事件
  3. reqres添加到域中(因为它们是在域存在之前创建的)
  4. run域并调用您的请求处理程序(这类似于try/catchtry部分)

像这样:

var domain = require('domain');

function handleRequest(req, res) {
    // Just something to trigger an async error
    setTimeout(function() {
        throw Error("Some random async error");
        res.end("Hello world!");  
    }, 100);
}

var server = require("http").createServer(function (req, res) {
    var d = domain.create();

    d.on('error', function(err) {
        // We're in an unstable state, so shutdown the server.
        // This will only stop new connections, not close existing ones.
        server.close();

        // Send our 500 error
        res.statusCode = 500;
        res.setHeader("content-type", "text/plain");
        res.end("Server error: " + err.message);
    });

    // Since the domain was created after req and res, they
    // need to be explictly added.
    d.add(req);
    d.add(res);

    // This is similar to a typical try/catch, but the "catch"
    // is now d's error event.
    d.run(function() {
        handleRequest(req, res);
    });
}).listen(8080); 

在错误后重启进程

通过使用cluster模块,您可以在错误后很好地重新启动进程。我基本上是从节点文档中复制了一个示例,在主进程中启动多个工作进程。工作进程是处理传入连接的进程。如果其中一个进程有不可恢复的错误(即我们在前一节中捕获的错误),那么它将与主进程断开连接,发送500响应并退出。当主进程看到工作进程断开连接时,它将知道发生了错误并启动一个新的工作进程。由于同时运行多个工作进程,如果其中一个进程失败,不会出现丢失传入连接的问题。

示例代码,从这里复制:

var cluster = require('cluster');
var PORT = +process.env.PORT || 1337;

if (cluster.isMaster) {
  // In real life, you'd probably use more than just 2 workers,
  // and perhaps not put the master and worker in the same file.
  //
  // You can also of course get a bit fancier about logging, and
  // implement whatever custom logic you need to prevent DoS
  // attacks and other bad behavior.
  //
  // See the options in the cluster documentation.
  //
  // The important thing is that the master does very little,
  // increasing our resilience to unexpected errors.

  cluster.fork();
  cluster.fork();

  cluster.on('disconnect', function(worker) {
    console.error('disconnect!');
    cluster.fork();
  });

} else {
  // the worker
  //
  // This is where we put our bugs!

  var domain = require('domain');

  // See the cluster documentation for more details about using
  // worker processes to serve requests.  How it works, caveats, etc.

  var server = require('http').createServer(function(req, res) {
    var d = domain.create();
    d.on('error', function(er) {
      console.error('error', er.stack);

      // Note: we're in dangerous territory!
      // By definition, something unexpected occurred,
      // which we probably didn't want.
      // Anything can happen now!  Be very careful!

      try {
        // make sure we close down within 30 seconds
        var killtimer = setTimeout(function() {
          process.exit(1);
        }, 30000);
        // But don't keep the process open just for that!
        killtimer.unref();

        // stop taking new requests.
        server.close();

        // Let the master know we're dead.  This will trigger a
        // 'disconnect' in the cluster master, and then it will fork
        // a new worker.
        cluster.worker.disconnect();

        // try to send an error to the request that triggered the problem
        res.statusCode = 500;
        res.setHeader('content-type', 'text/plain');
        res.end('Oops, there was a problem!\n');
      } catch (er2) {
        // oh well, not much we can do at this point.
        console.error('Error sending 500!', er2.stack);
      }
    });

    // Because req and res were created before this domain existed,
    // we need to explicitly add them.
    // See the explanation of implicit vs explicit binding below.
    d.add(req);
    d.add(res);

    // Now run the handler function in the domain.
    d.run(function() {
      handleRequest(req, res);
    });
  });
  server.listen(PORT);
}

// This part isn't important.  Just an example routing thing.
// You'd put your fancy application logic here.
function handleRequest(req, res) {
  switch(req.url) {
    case '/error':
      // We do some async stuff, and then...
      setTimeout(function() {
        // Whoops!
        flerb.bark();
      });
      break;
    default:
      res.end('ok');
  }
}

注意:我仍然强调您应该查看domain模块文档,并查看其中的示例和说明。它解释了大部分,如果不是全部,关于此内容的原因以及您可能遇到的其他情况。


这个回答中有一些有趣的内容,但是如果在使用超时时进程被杀死。他们的示例展示了如何捕获错误(即使它发生在异步函数中) - 只有当我删除超时(异步操作)时,才会出现500响应,否则进程将被抛出的错误杀死。有没有办法捕获这样的错误?想象一下有一个大型应用程序,在进行请求时可能会出现错误(foo.something,其中fooundefined)- 当然,它们是错误,但我们如何优雅地处理这些异常呢?谢谢! - Ionică Bizău
我不确定我理解你的问题。我提供的示例代码应该返回500响应,即使错误发生在setTimeout中(您可以将throw替换为undefined.foo()或其他强制性错误,它仍然可以工作)。请注意,它仍将按照编写方式终止进程,但是500响应应该首先发送出去。 - Mike S
你可以删除 server.close() 这一行,进程应该会保持开启状态,但是由于 JavaScript 中 throw 的工作方式,进程可能不太稳定。如果你查看 domain 模块文档 中的示例,它展示了如何使用 cluster 模块在 500 响应发送后自动重启进程的方法。 - Mike S
这就是为什么要使用 cluster 模块的原因;-) 有了集群,您可以拥有多个进程来处理请求。如果其中一个进程崩溃,那么其他进程会接手工作,同时启动一个新的进程来补充。 - Mike S
当然。已添加新部分以回答问题。 - Mike S
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接