相对于 PHP 服务器,Node.js 服务器(如 Express)如何管理内存?

How does a Node.js server (like Express) manage memory as opposed to a PHP server?

据我了解,基本上,PHP 服务器端应用程序 (PHP-FPM) 在每次请求时从头开始加载整个应用程序,然后在请求结束时将其关闭.这意味着变量、容器、配置和其他所有内容都在每个单独的请求中从零开始读取和构建,并且没有交叉。我可以利用这些知识来更好地构建应用程序。例如,我知道 class statics 仅在请求期间保留其数据,并且每个新请求都有其自己的值。

然而,

Node.js 服务器像 Express.js 的工作方式非常不同。它是一个 Node.js 进程,持续 运行ning 并侦听任何新请求并将它们传递给正确的处理程序。这需要不同的开发方法,因为在请求之间有数据保存在内存中。例如,在这种情况下,class 静态听起来像是它们会在服务器正常运行的整个持续时间内保存数据,而不仅仅是单个请求的持续时间。

所以我对此有一些疑问:

  1. 在 Express.js 启动期间预加载一些数据是否有意义(例如从文件中读取私钥)以便在请求需要时它已经在内存中并且每次都会被重新使用没有从文件中重新读取的时间?在 PHP 服务器框架中,这无关紧要,因为每个请求都是从 0 开始构建的。
  2. 如何正确处理 Node.js 服务器进程中的异常?如果 PHP 服务器脚本抛出致命异常,则仅该特定请求终止,所有其他请求和任何新请求 运行 都可以。如果在 Node.js 服务器中发生致命错误,听起来它会终止整个进程并因此终止所有请求。

如果您有关于这个主题的任何资源,如果您也能分享它们就太好了。

Does it make sense to pre-load some data during Express.js startup (like reading private keys from file) so that it is already in memory when needed by a request and it would get re-used each time without being re-read from file?

是的,如果您构建代码以让这些数据在请求处理程序中可用,这很有意义。在下面的例子中,据我所知,staticResponse 只被读取了一次。

const express = require('express');
const staticResponse = fs.readFileSync('./data');
const app = express();

app.get('/', function (req, res) {
  res.json(staticResponse);
});

app.listen(3000, function () {
  console.log('Example app listening on port 3000!');
});

How do I properly handle exceptions in a Node.js server process? If a fatal error happens in a Node.js server, it sounds like it would kill the entire process and thus all requests with it.

没错,一个未处理的异常导致整个nodejs进程崩溃。有多种方法可以管理错误,没有 'the one for all' 解决方案。取决于你如何编写你的代码。

all requests with it => 请记住 nodejs 是单线程的。

app.post('/', function (req, res, next) {
    try {
        const data = JSON.parse(req.body.stringedData);

        // use data

        res.sendStatus(200);

    } catch (err) {
        return next(err);
    }
});

1-

Does it make sense to pre-load some data during Express.js startup (like reading private keys from file) so that it is already in memory when needed by a request and it would get re-used each time without being re-read from file? In a PHP server framework this wouldn't matter that much as everything gets built from 0 with each request.

是的,完全是。您会在应用程序启动时 bootstrap 连接到数据库、读取文件数据和类似任务,因此它们在每个请求中始终可用。

在这种情况下需要考虑一些事项:

  • 在应用启动时,可以安全的调用同步方法,如fs.readFileSync等,因为此时单线程没有并发请求。

  • CommonJS 模块确实缓存了它们导出的第一个值。因此,如果您选择使用专用模块来处理从文件、数据库连接等读取的秘密,您可以:

secrets.js

const fs = require('fs');
const gmailSecretApiKey = fs.readFileSync('path_to_file');
const mailgunSecretApiKey = fs.readFileSync('path_to_file'); 
...

module.exports = {
gmailSecretApiKey,
mailgunSecretApiKey,
...
}

然后 require 这作为您的应用程序启动。在此之后,任何执行以下操作的模块: const gmailKey = require('.../secrets').gmailSecretApiKey 不会 再次从文件中读取。结果缓存在模块中。

这很重要,因为允许您使用 requireimport 在您的控制器和模块中使用配置,而无需费心将额外参数传递给您的 http 控制器或将它们添加到 req 对象。

  • 根据基础架构,您可能无法让您的应用程序在启动期间不处理请求(即您只有一台机器并且不想将 service unavailble 提供给您的客户)。在这种情况下,您可以在 promises 中公开所有配置和共享资源,并bootstrap您的网络控制器尽快,等待里面的 promises。假设我们在处理 '/user':
  • 上的请求时需要 kafka up 和 运行ning

kafka.js

function kafka() {
    // return some promise of an object that can publish and read from kafka in a given port etc. etc.
  }
module.exports = kafka();

所以现在:

userController.js

const kafka = require('.../kafka');
router.get('/user', (req,res) => {
  kafka.then(k => {
    k.publish(req.user, 'userTopic'); // or whatever. This is just an example.
  });
})

这样,如果用户在 bootstrap 期间发出请求,该请求仍会得到处理(但需要一些时间)。当承诺已经解决时发出的请求将不会注意到任何东西。

  • 节点中没有多线程这样的东西。您在 commonJS 模块中声明的任何内容或您写入 process 的任何内容都将在每个请求中可用。

2-

How do I properly handle exceptions in a Node.js server process? If a PHP server script throws a fatal exception only that specific request dies, all other requests and any new ones run fine. If a fatal error happens in a Node.js server, it sounds like it would kill the entire process and thus all requests with it.

这实际上取决于您发现的异常类型。它与正在处理的请求特别相关,还是对整个应用程序至关重要?

在前一种情况下,您想捕获异常并且不让整个线程死亡。 现在,javascript 中的 'catch the exception' 很棘手 ,因为你不能 catch 异步 exceptions/errors,你可能会使用 process.on('unhandledRejection') 来处理,例如:

// main.js

try {
  bootstrapMongoDb();
  bootstrapKafka();
  bootstrapSecrets();
  ... wahtever
  bootstrapExpress();
} catch(e){
   // read what `e` brings and decide.
   // however, is worth to mention that errors raised during handling
   // http request won't ever get handled here, because they are
   // asynchronous. try/catch in javascript don't catch asynchronous errors.
 }

process.on('unhandledRejection', e => {
  // now here we are treating unhandled promise rejections, and errors that raise
  // in express controllers are likely end up here. of course, I'm talking about
  // promise rejections. I am not sure if this can catch Errors thrown in callbacks.
 // You should never `throw new Error` inside an asynchronous callback. 

});

处理节点应用程序中的错误本身就是一个完整的主题,范围太广,无法在此处考虑。然而,一些提示不应该造成伤害:

  • 永远不要在回调中抛出错误。 throw 是同步的。回调和异步应依赖于 error 参数或承诺拒绝。

  • 你最好习惯承诺。 Promises 确实改进了异步代码中的错误管理。

  • Javascript 错误可以用额外的字段修饰,因此您可以填写跟踪 ID 和其他在读取系统日志时可能有用的 ID,因为您将记录未处理的日志错误。

现在,在后一种情况下......有时会出现对您的应用程序来说完全是灾难性的故障。也许你完全需要连接到 kafka 或 mongo 服务器,如果它坏了,那么你可能想终止你的应用程序,以便客户端在尝试连接时收到 503。

然后,在某些情况下,您可能想要终止您的应用程序,然后让另一个服务在数据库再次可用时重新启动它。这在很大程度上取决于基础架构,您最好永远不要终止您的应用程序。

如果您没有为您处理 Web 服务的运行状况和重启的基础架构,那么永远不要让您的应用程序死掉可能更安全。话虽如此,至少使用像 nodemon 或 PM2 这样的工具来确保您的应用程序在关闭后重新启动是一件好事。

奖金:为什么你不应该在回调中抛出错误

抛出的错误通过调用堆栈传播。比方说,你有一个调用 B 的函数 A,B 又调用 C。然后 C 抛出一个错误。全部只有同步代码。

在这种情况下,错误会传播到 B,如果没有 catch,它会传播到 A,依此类推。

现在假设 C 本身不会抛出错误,而是调用 fs.readFile(path, callback)。在回调函数中,抛出一个错误。

这里,当调用回调并抛出错误时,A 已经完成并在很久以前就离开了堆栈,数百毫秒之前,甚至可能更多。

这意味着 A 中的任何 catch 块都不会捕获错误,因为甚至还没有 :

function bootstrapTimeout() {

try {
  setTimeout(() => {
    throw new Error('foo');
    console.log('paco');
    }, 200);

} catch (e) {
 console.log('error trapped!');
} 

}

function bootstrapInterval() {

  setInterval(() => {
  console.log('interval')
  }, 50);
}

console.log('start');
bootstrapTimeout();
bootstrapInterval();

如果您 运行 该片段,您会看到错误是如何到达op 级别并终止进程,即使 throw new Error('foo'); 行位于 try/catch 块内。

错误,结果界面

node.js 没有使用错误来处理异步代码中的异常,而是具有标准行为,即为您传递给异步方法的每个回调公开一个 (error, result) 接口。例如,如果 fs.readFile 碰巧因为文件名不存在而出错,它不会抛出错误,它会调用带有相应错误的回调作为 error 参数。

赞:

fs.readFile('notexists.png', (error, callback) => {

  if(error){
    // foo
  }
  else {
    http.post('http://something.com', result, (error, callback) => {
      if(error){
        // oops, something went wrong with an http request
      } else {
        // keep working
        // etc.
        // maybe more callbacks, always with the dreadful 'if (error)'...
      }
    })
  }
});

你总是在回调中控制异步操作​​的错误,你不应该抛出。

现在这很痛苦。 Promise 允许更好的错误控制 因为你可以在一个 catch 块中控制异步错误:

fsReadFilePromise('something.png')
.then(res => someHttpRequestPromise(res))
.then(httpResponse => someOtherAsyncMethod(httpResponse))
.then(_ => maybeSomeLoggingOrWhatever() )
.catch(e => {
  // here you can control any error thrown in the previous chain.
});

还有 async/await 允许您混合异步和同步代码并处理 catch 块中的承诺拒绝:

await function main() {
  try {
    a(); // some sync code
    await b(); // some promise
  } catch(e) {
    console.log(e); // either an error throw in a() or a promise rejection reason in b();
  }
}

但是请记住,await 并不是魔法,您确实需要很好地理解 promises 和异步才能正确使用它。

最后,您总是通过 try/catch 得到一个用于同步错误的错误控制流,以及另一个用于通过回调参数或承诺拒绝的异步错误的错误控制流。

回调 可以 在使用同步 api 时使用 try/catch,但绝不能 throw。任何函数都可以使用 catch 来处理同步错误,但不能依赖 catch 块来处理异步错误。有点乱。