后台作业
了解如何在不影响连接用户的状况下执行后台作业。
Meteor 在服务器端使用 Node.js,而 Node.js 默认情况下是单线程的,或者至少除了它委托给操作系统的事情之外几乎是单线程的。
在像 Node.js 这样的单线程环境中,如果您在处理已连接用户操作的相同容器中执行长时间运行的任务,它们将使用事件循环进行竞争。这种竞争可能会导致许多问题,例如速度缓慢,甚至感觉您的应用已崩溃(即使它没有崩溃)。在这些情况下,您的应用只是非常繁忙。
如果您想了解更多关于事件循环和“单线程”的信息,我们推荐以下内容
- JavaScript VM 内部机制、事件循环、异步和作用域链 - Arindam Paul 在 YouTube 上
- 事件循环 - Jake Archibald 在 YouTube 上
- 事件循环到底是什么? - Philip Roberts 在 YouTube 上
- 您需要了解的有关 Node.js 事件循环的所有信息 - Bert Belder 在 YouTube 上
- Node.js 事件循环:并非真正的单线程 - Bryan Hughes 在 YouTube 上
- 从内部了解 Node 的事件循环 - Sam Roberts 在 YouTube 上
现在让我们关注一下如何在您的 Meteor 应用中解决这种竞争。
每个 Meteor 应用都有一个设置文件。设置文件在运行时使用,但在构建时不使用。这意味着您有机会使用设置 JSON 文件来更改系统的一些行为,即使您只有一个代码库。
这里的想法是使用相同的代码为长时间运行的任务(如后台作业)提供不同的环境,并为您的已连接用户(Web)提供另一个环境,这样您就不需要担心共享依赖项、更新 Meteor 包的多个应用、更新 npm 依赖项的多个应用等等。
如果您想拥有两个具有某些共享代码的独立应用,这没有问题,但在大多数情况下,我们认为使用相同的代码是最有效的方法。
> 然而,在一些公司,他们已经拥有许多不同的应用,因此创建专门用于长时间运行任务的应用是有意义的。
以下是我们的操作方法:在我们的设置文件中,我们将使用一个布尔值,例如 runJobs。在已连接用户应用(Web)的设置中,我们将使用 (settings-web.json)
在运行长时间任务的应用中,我们将使用 (settings-jobs.json)
因此,我们仅在作业应用中启用了作业。在从您的服务器 mainModule 导入的文件中,您将拥有 (server/main.js)
您将拥有根据您的设置决定是否应运行长时间运行的任务的逻辑
在此示例中,我们使用 synced-cron 包通过 MongoDB 在应用之间进行通信。但您可以使用其他包或创建自己的解决方案。
您可以与这种双应用设置一起使用的另一个想法是使用 DDP 调用将数据从一个应用发送到另一个应用。一些客户端使用此设置,因此他们可以将几乎所有工作延迟到作业应用,例如处理图像或生成 Excel 文件。任何花费超过几毫秒的任务都不会导致响应用户直接操作的容器速度变慢。
现在您的代码和设置都已准备就绪,我们需要考虑部署。
由于您拥有相同的代码库,因此无需构建两次。这可以通过部署标志 --cache-build 来实现。
以下是我们的操作方法
第二个部署命令将跳过构建部分。它只会上传您的 bundle 并更快地部署您的第二个应用。
请注意,它们是 Meteor Cloud(Galaxy)上的两个不同的应用,这非常好,因为您可以隔离它们,使用不同的 触发器,在不同的 APM 仪表板中分析它们的性能,并且在运行时一切都是隔离的,除了您的数据库。
例如,作业应用可以只是一个后端应用,没有任何域访问它。您只需将其保留,而无需在您的 DNS 配置中指向它的 CNAME。
现在您有两个不同的应用,因此可以轻松地为特定类型的负载定制您的应用。
例如,如果您的作业应用正在使用 Worker 线程,您可以使用允许您拥有多个内核的容器大小。如果您在那里没有使用 Worker 线程,那么在您的 Web 应用中这样做可能没有多大意义。
另一个重要的方面是独立地分析 APM、Galaxy 指标和日志,包括 Galaxy 通知。如果应用中发生某些事情,您将收到特定的通知。例如,也许您知道您的作业应用在执行繁重的作业时将处于不健康状态,这可以接受;您甚至可以关闭此通知(如果您愿意)。
当然,所有 Galaxy 配置对于每个应用都将是独立的,例如触发器、应用保护、宽限期、不健康容器替换等。
使用相同负载运行两个不同的应用不会增加您的成本。实际上,它可以降低您的成本,因为您可以使用不同的容器大小,而不是对所有工作负载使用最大的容器。您还可以使用不同的触发器并更积极地缩减规模,因为您将更好地控制哪些任务在哪个应用中运行。
- 您可以使用具有不同代码库的不同应用。这也可以。如上所述,唯一的缺点是您需要管理两个 Meteor 应用、两个 package.json 文件以及所有内容的两个副本。因此,这可能是更多工作。此外,您需要有一种共享包的方法,这很好,但需要多加注意。
- 您可以使用 GALAXY_CONTAINER_ID 来尝试控制每个容器中运行的内容,但这很难依靠 Galaxy 内部机制来管理您的容器。此外,您需要确保始终有分配给特定作业的容器可用。另一个问题是避免用户访问长时间运行的任务容器,因为 Galaxy 代理正在为您管理负载均衡,而这正是您首先要避免的:混合用户请求和长时间运行的任务。您的应用监控也将更加困难,因为您将在同一应用中混合来自不同工作负载的指标。
最后,我们还有一个 示例 应用实现了这种方法。
我们在内部也大量使用这种方法,并且在此处将其作为最佳实践分享,因为我们确实认为这是解决此问题的非常有效的方法。
Meteor 在服务器端使用 Node.js,而 Node.js 默认情况下是单线程的,或者至少除了它委托给操作系统的事情之外几乎是单线程的。
在像 Node.js 这样的单线程环境中,如果您在处理已连接用户操作的相同容器中执行长时间运行的任务,它们将使用事件循环进行竞争。这种竞争可能会导致许多问题,例如速度缓慢,甚至感觉您的应用已崩溃(即使它没有崩溃)。在这些情况下,您的应用只是非常繁忙。
如果您想了解更多关于事件循环和“单线程”的信息,我们推荐以下内容
- JavaScript VM 内部机制、事件循环、异步和作用域链 - Arindam Paul 在 YouTube 上
- 事件循环 - Jake Archibald 在 YouTube 上
- 事件循环到底是什么? - Philip Roberts 在 YouTube 上
- 您需要了解的有关 Node.js 事件循环的所有信息 - Bert Belder 在 YouTube 上
- Node.js 事件循环:并非真正的单线程 - Bryan Hughes 在 YouTube 上
- 从内部了解 Node 的事件循环 - Sam Roberts 在 YouTube 上
现在让我们关注一下如何在您的 Meteor 应用中解决这种竞争。
拆分为多个应用
每个 Meteor 应用都有一个设置文件。设置文件在运行时使用,但在构建时不使用。这意味着您有机会使用设置 JSON 文件来更改系统的一些行为,即使您只有一个代码库。
这里的想法是使用相同的代码为长时间运行的任务(如后台作业)提供不同的环境,并为您的已连接用户(Web)提供另一个环境,这样您就不需要担心共享依赖项、更新 Meteor 包的多个应用、更新 npm 依赖项的多个应用等等。
如果您想拥有两个具有某些共享代码的独立应用,这没有问题,但在大多数情况下,我们认为使用相同的代码是最有效的方法。
> 然而,在一些公司,他们已经拥有许多不同的应用,因此创建专门用于长时间运行任务的应用是有意义的。
以下是我们的操作方法:在我们的设置文件中,我们将使用一个布尔值,例如 runJobs。在已连接用户应用(Web)的设置中,我们将使用 (settings-web.json)
{
"runJobs": false
}
在运行长时间任务的应用中,我们将使用 (settings-jobs.json)
{
"runJobs": true
}
因此,我们仅在作业应用中启用了作业。在从您的服务器 mainModule 导入的文件中,您将拥有 (server/main.js)
// server/main.js
import '../infra/jobs.js';
您将拥有根据您的设置决定是否应运行长时间运行的任务的逻辑
// infra/jobs.js
import { Meteor } from 'meteor/meteor';
import { SyncedCron } from 'meteor/littledata:synced-cron';
import { NotificationsCollection } from '../data/NotificationsCollection';
Meteor.startup(() => {
console.time('jobs');
// we want to run jobs in development as well, no matter the settings
if (!Meteor.isDevelopment && !Meteor.settings.runJobs) {
logger.log('** APP: SyncedCron is not started on app instance **');
console.timeEnd('jobs');
return;
}
SyncedCron.add({
name: 'send notifications',
schedule: parser => parser.text('every minute'),
job: () => NotificationsCollection.sendNotifications(),
});
SyncedCron.start();
console.timeEnd('jobs');
});
在此示例中,我们使用 synced-cron 包通过 MongoDB 在应用之间进行通信。但您可以使用其他包或创建自己的解决方案。
您可以与这种双应用设置一起使用的另一个想法是使用 DDP 调用将数据从一个应用发送到另一个应用。一些客户端使用此设置,因此他们可以将几乎所有工作延迟到作业应用,例如处理图像或生成 Excel 文件。任何花费超过几毫秒的任务都不会导致响应用户直接操作的容器速度变慢。
部署应用
现在您的代码和设置都已准备就绪,我们需要考虑部署。
由于您拥有相同的代码库,因此无需构建两次。这可以通过部署标志 --cache-build 来实现。
以下是我们的操作方法
# deploy.sh
# build and deploy the jobs app
meteor deploy jobs.yourdomain.com --settings settings-jobs.json --cache-build
# deploy the web app
meteor deploy app.yourdomain.com --settings settings-web.json --cache-build
第二个部署命令将跳过构建部分。它只会上传您的 bundle 并更快地部署您的第二个应用。
请注意,它们是 Meteor Cloud(Galaxy)上的两个不同的应用,这非常好,因为您可以隔离它们,使用不同的 触发器,在不同的 APM 仪表板中分析它们的性能,并且在运行时一切都是隔离的,除了您的数据库。
例如,作业应用可以只是一个后端应用,没有任何域访问它。您只需将其保留,而无需在您的 DNS 配置中指向它的 CNAME。
监控
现在您有两个不同的应用,因此可以轻松地为特定类型的负载定制您的应用。
例如,如果您的作业应用正在使用 Worker 线程,您可以使用允许您拥有多个内核的容器大小。如果您在那里没有使用 Worker 线程,那么在您的 Web 应用中这样做可能没有多大意义。
另一个重要的方面是独立地分析 APM、Galaxy 指标和日志,包括 Galaxy 通知。如果应用中发生某些事情,您将收到特定的通知。例如,也许您知道您的作业应用在执行繁重的作业时将处于不健康状态,这可以接受;您甚至可以关闭此通知(如果您愿意)。
当然,所有 Galaxy 配置对于每个应用都将是独立的,例如触发器、应用保护、宽限期、不健康容器替换等。
成本
使用相同负载运行两个不同的应用不会增加您的成本。实际上,它可以降低您的成本,因为您可以使用不同的容器大小,而不是对所有工作负载使用最大的容器。您还可以使用不同的触发器并更积极地缩减规模,因为您将更好地控制哪些任务在哪个应用中运行。
替代方法
- 您可以使用具有不同代码库的不同应用。这也可以。如上所述,唯一的缺点是您需要管理两个 Meteor 应用、两个 package.json 文件以及所有内容的两个副本。因此,这可能是更多工作。此外,您需要有一种共享包的方法,这很好,但需要多加注意。
- 您可以使用 GALAXY_CONTAINER_ID 来尝试控制每个容器中运行的内容,但这很难依靠 Galaxy 内部机制来管理您的容器。此外,您需要确保始终有分配给特定作业的容器可用。另一个问题是避免用户访问长时间运行的任务容器,因为 Galaxy 代理正在为您管理负载均衡,而这正是您首先要避免的:混合用户请求和长时间运行的任务。您的应用监控也将更加困难,因为您将在同一应用中混合来自不同工作负载的指标。
示例
最后,我们还有一个 示例 应用实现了这种方法。
我们在内部也大量使用这种方法,并且在此处将其作为最佳实践分享,因为我们确实认为这是解决此问题的非常有效的方法。
更新于:2024年7月17日
谢谢!