配置定时任务
定时任务也就是由时间触发的执行过程,属于很常见的业务逻辑。Unix 在早期版本就提供了定时任务调度模块 Cron,并在各类 Linux 系统上沿用至今。Cron 的配置文件 crontab 具有全面却清晰的格式,能够解决大多数场景下的定时任务配置问题,企业级服务器可以使用类 crontab 的格式灵活配置的各种定时任务逻辑,以下为 crontab 的格式:
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
本章将基于上一章已完成的工程 licg9999/nodejs-server-examples - 10-log 通过 node-schedule 以类似 crontab 的方式配置定时任务,检测可能含有网络攻击的店铺信息并通过 nodemailer 将可疑店铺信息邮件发送给管理员。在工程根目录执行 node-schedule 与 nodemailer 的安装命令:
$ yarn add node-schedule nodemailer # 本地安装 node-schedule、nodemailer
# ...
info Direct dependencies
├─ node-schedule@1.3.2
└─ nodemailer@6.4.11
# ...
网络攻击巡检
现在实现对网络攻击信息的定时检测与报警的逻辑。先补充服务层逻辑:
// src/services/shop.js
const { Shop } = require('../models');
class ShopService {
async init() {}
- async find({ id, pageIndex = 0, pageSize = 10, logging }) {
+ async find({ id, pageIndex = 0, pageSize = 10, where, logging }) {
if (id) {
return [await Shop.findByPk(id, { logging })];
}
return await Shop.findAll({
offset: pageIndex * pageSize,
limit: pageSize,
+ where,
logging,
});
}
// ...
}
// ...
// src/services/mail.js
const { promisify } = require('util');
const nodemailer = require('nodemailer');
const { mailerOptions } = require('../config');
class MailService {
mailer;
async init() {
this.mailer = nodemailer.createTransport(mailerOptions);
await promisify(this.mailer.verify)();
}
async sendMail(params) {
return await this.mailer.sendMail({
from: mailerOptions.auth.user,
...params,
});
}
}
let service;
module.exports = async () => {
if (!service) {
service = new MailService();
await service.init();
}
return service;
};
// src/config/index.js
const merge = require('lodash.merge');
const logger = require('../utils/logger');
const { logging } = logger;
const config = {
// 默认配置
default: {
// ...
+ mailerOptions: {
+ host: 'smtp.126.com',
+ port: 465,
+ secure: true,
+ logger: logger.child({ type: 'mail' }),
+ auth: {
+ user: process.env.MAILER_USER,
+ pass: process.env.MAILER_PASS,
+ },
+ },
},
// ...
};
// ...
# .env.local
GITHUB_CLIENT_ID='b8ada004c6d682426cfb'
GITHUB_CLIENT_SECRET='0b13f2ab5651f33f879a535fc2b316c6c731a041'
+
+MAILER_USER='ht_nse@126.com'
+MAILER_PASS='CAEJHSTBWNOKHRVL'
注意由于应用节点可能不止 1 个,执行巡检时将使用分布式锁限制执行节点数量以避免重复报警,这里借助数据库来实现分布式锁:
$ # 生成定时任务锁的 model 文件与 schema 迁移文件
$ yarn sequelize model:generate --name scheduleLock --attributes name:string,counter:integer
$ # 将 src/models/schedulelock.js 命名为 src/models/scheduleLock.js
$ mv src/models/schedulelock.js src/models/scheduleLock.js
$ tree src/models # 展示 src/models 目录内容结构
src/models
├── config
│ └── index.js
├── index.js
├── migrate
│ ├── 20200725045100-create-shop.js
│ ├── 20200727025727-create-session.js
│ └── 20200801120113-create-schedule-lock.js
├── scheduleLock.js
├── seed
│ └── 20200725050230-first-shop.js
└── shop.js
调整 src/models/scheduleLock.js
与 src/models/migrate/20200801120113-create-schedule-lock.js
:
// src/models/scheduleLock.js
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class scheduleLock extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
scheduleLock.init(
{
name: DataTypes.STRING,
counter: DataTypes.INTEGER,
},
{
sequelize,
modelName: 'ScheduleLock',
tableName: 'schedule_lock',
}
);
return scheduleLock;
};
// src/models/migrate/20200801120113-create-schedule-lock.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('schedule_lock', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
name: {
type: Sequelize.STRING,
},
counter: {
type: Sequelize.INTEGER,
},
created_at: {
allowNull: false,
type: Sequelize.DATE,
},
updated_at: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('schedule_lock');
},
};
然后写入巡检逻辑:
$ mkdir src/schedules # 新建 src/schedules 存放定时任务
$ tree src -L 1 # 展示 src 目录内容结构
src
├── config
├── controllers
├── middlewares
├── models
├── moulds
├── schedules
├── server.js
├── services
└── utils
// src/schedules/inspectAttack.js
const { basename } = require('path');
const schedule = require('node-schedule');
const { sequelize, ScheduleLock, Sequelize } = require('../models');
const mailService = require('../services/mail');
const shopService = require('../services/shop');
const escapeHtmlInObject = require('../utils/escape-html-in-object');
const logger = require('../utils/logger');
const { Op } = Sequelize;
// 当前任务的锁名称
const LOCK_NAME = basename(__dirname);
// 锁的最长占用时间
const LOCK_TIMEOUT = 15 * 60 * 1000;
// 分布式任务并发数
const CONCURRENCY = 1;
// 报警邮件发送对象
const MAIL_RECEIVER = 'licg9999@126.com';
class InspectAttack {
mailService;
shopService;
async init() {
this.mailService = await mailService();
this.shopService = await shopService();
// 每到 15 分时巡检一次
schedule.scheduleJob('*/15 * * * *', this.findAttackedShopInfoAndSendMail);
}
findAttackedShopInfoAndSendMail = async () => {
// 上锁
const lockUpT = await sequelize.transaction();
try {
const [lock] = await ScheduleLock.findOrCreate({
where: { name: LOCK_NAME },
defaults: { name: LOCK_NAME, counter: 0 },
transaction: lockUpT,
});
if (lock.counter >= CONCURRENCY) {
if (Date.now() - lock.updatedAt.valueOf() > LOCK_TIMEOUT) {
lock.counter--;
await lock.save({ transaction: lockUpT });
}
await lockUpT.commit();
return;
}
lock.counter++;
await lock.save({ transaction: lockUpT });
await lockUpT.commit();
} catch (err) {
logger.error(err);
await lockUpT.rollback();
return;
}
try {
// 寻找异常数据
const shops = await this.shopService.find({
pageSize: 100,
where: {
name: { [Op.or]: [{ [Op.like]: '<%' }, { [Op.like]: '%>' }] },
},
});
// 发送报警邮件
if (shops.length) {
const subject = '安全警告,发现可疑店铺信息!';
const html = `
<div>以下是服务器巡检发现的疑似含有网络攻击的店铺信息:</div>
<pre>
${shops
.map((shop) => JSON.stringify(escapeHtmlInObject(shop), null, 2))
.join('\n')}
</pre>`;
await this.mailService.sendMail({ to: MAIL_RECEIVER, subject, html });
}
} catch {}
// 解锁
const lockDownT = await sequelize.transaction();
try {
const lock = await ScheduleLock.findOne({
where: { name: LOCK_NAME },
transaction: lockDownT,
});
if (lock.counter > 0) {
lock.counter--;
await lock.save({ transaction: lockDownT });
}
await lockDownT.commit();
} catch {
await lockDownT.rollback();
}
};
}
module.exports = async () => {
const s = new InspectAttack();
await s.init();
};
// src/schedules/index.js
const inspectAttackSchedule = require('./inspectAttack');
module.exports = async function initSchedules() {
await inspectAttackSchedule();
};
// src/server.js
const express = require('express');
const { resolve } = require('path');
const { promisify } = require('util');
const initMiddlewares = require('./middlewares');
const initControllers = require('./controllers');
+const initSchedules = require('./schedules');
const logger = require('./utils/logger');
const server = express();
const port = parseInt(process.env.PORT || '9000');
const publicDir = resolve('public');
const mouldsDir = resolve('src/moulds');
async function bootstrap() {
server.use(await initMiddlewares());
server.use(express.static(publicDir));
server.use('/moulds', express.static(mouldsDir));
server.use(await initControllers());
server.use(errorHandler);
+ await initSchedules();
await promisify(server.listen.bind(server, port))();
logger.info(`> Started on port ${port}`);
}
// ...
查看报警
在新增两个含有网络攻击的店铺信息之后,即可在分钟数为 15 的倍数时收到一则警告邮件:
本章源码
licg9999/nodejs-server-examples - 11-schedule
更多阅读
从零搭建 Node.js 企业级 Web 服务器(零):静态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异常处理
从零搭建 Node.js 企业级 Web 服务器(五):数据库访问
从零搭建 Node.js 企业级 Web 服务器(六):会话
从零搭建 Node.js 企业级 Web 服务器(七):认证登录
从零搭建 Node.js 企业级 Web 服务器(八):网络安全
从零搭建 Node.js 企业级 Web 服务器(九):配置项
从零搭建 Node.js 企业级 Web 服务器(十):日志
从零搭建 Node.js 企业级 Web 服务器(十一):定时任务
从零搭建 Node.js 企业级 Web 服务器(十二):远程调用
从零搭建 Node.js 企业级 Web 服务器(十三):断点调试与性能分析
从零搭建 Node.js 企业级 Web 服务器(十四):自动化测试
从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。