首页 > 解决方案 > 与 PHP 服务器相比,Node.js 服务器(如 Express)如何管理内存?

问题描述

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

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

所以我对此有一些疑问:

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

如果你有关于这个主题的任何资源,如果你也可以分享它们会很棒。

标签: phpnode.jsexpressyii

解决方案


1-

在 Express.js 启动期间预加载一些数据是否有意义(比如从文件中读取私钥),以便在请求需要时它已经在内存中,并且每次都可以重新使用它而不被重新读取文件?在 PHP 服务器框架中,这并不重要,因为每个请求都是从 0 开始构建的。

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

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

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

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

秘密.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给您的客户)。在这种情况下,您可以在 Promise 中公开所有配置和共享资源,并尽可能快地引导您的 Web 控制器,等待里面的 Promise。假设在处理 '/user' 上的请求时,我们需要启动并运行 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();

所以现在在:

用户控制器.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.
  });
})

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

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

2-

如何正确处理 Node.js 服务器进程中的异常?如果 PHP 服务器脚本仅在特定请求终止时抛出致命异常,则所有其他请求和任何新请求都可以正常运行。如果在 Node.js 服务器中发生致命错误,听起来它会杀死整个进程,从而杀死所有请求。

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

在前一种情况下,您希望捕获异常并且不让整个线程死掉。现在,在 javascript 中“捕获异常”很棘手,因为您不能catch异步异常/错误,并且您可能会使用它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参数或一个 Promise 拒绝。

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

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

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

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

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

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

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

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

现在让我们说,相反,C 本身不会抛出错误,而是调用fs.readFile(path, callback). 在回调函数中,抛出一个错误。

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

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

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();

如果您运行该代码段,您将看到错误如何到达顶层并终止进程,即使该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 允许您混合 async 和 sync 代码并在 catch 块中处理 promise 拒绝:

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不是魔术,您确实需要很好地理解 Promise 和异步才能正确使用它。

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

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


推荐阅读