php - 与 PHP 服务器相比,Node.js 服务器(如 Express)如何管理内存?
问题描述
据我了解,基本上,PHP 服务器端应用程序 (PHP-FPM) 在每次请求时从头开始加载整个应用程序,然后在请求结束时将其关闭。这意味着变量、容器、配置和其他所有内容在每个单独的请求中都从零开始读取和构建,并且没有交叉。我可以利用这些知识更好地构建应用程序。例如,我知道类静态只在请求期间保存它们的数据,并且每个新请求都有自己的值。
然而,像 Express.js 这样的 Node.js 服务器的工作方式却大不相同。它是一个持续运行的单个 Node.js 进程,它侦听任何新请求并将它们传递给正确的处理程序。这需要一种不同的开发方法,因为在请求之间有数据保存在内存中。例如,在这种情况下,类静态听起来像是在服务器正常运行的整个持续时间内保存数据,而不仅仅是单个请求的持续时间。
所以我对此有一些疑问:
- 在 Express.js 启动期间预加载一些数据是否有意义(比如从文件中读取私钥),以便在请求需要时它已经在内存中,并且每次都可以重新使用它而不被重新读取文件?在 PHP 服务器框架中,这并不重要,因为每个请求都是从 0 开始构建的。
- 如何正确处理 Node.js 服务器进程中的异常?如果 PHP 服务器脚本仅在特定请求终止时抛出致命异常,则所有其他请求和任何新请求都可以正常运行。如果在 Node.js 服务器中发生致命错误,听起来它会杀死整个进程,从而杀死所有请求。
如果你有关于这个主题的任何资源,如果你也可以分享它们会很棒。
解决方案
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
不会再次从文件中读取。结果缓存在模块中。
这很重要,因为允许您在控制器和模块中使用require
和import
使用配置,而无需将额外的参数传递给您的 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 已经完成并在很久以前离开堆栈,几百毫秒之前,甚至可能更久。
这意味着catch
A 中的任何块都不会捕获错误,因为甚至还没有:
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
块来处理异步错误。有点乱
推荐阅读
- c++ - std::map 在被告知存储所述值之前包含来自最后一个实例的值
- antlr4 - ANTLR4:如何将树转换回源代码
- angular - Angular 8 多域多站点
- c# - 从 WPF 应用程序启用 PS ExecutionPolicy
- flutter - 即使我使用flutter Row,我想要的字符串也不会走到一边
- python - 读入一个6G的文本文件,为什么要占用60G内存,如何减少?
- javascript - 如何有条件地加载依赖于语言的 Javascript 文件
- r - 如何重塑 data.frame
- linux - Docker-Compose:“在此目录或任何父目录中找不到合适的配置文件”
- javascript - JS 表单字段更新后提交