从零搭建 Node.js 企业级 Web 服务器(十一):定时任务

配置定时任务

定时任务也就是由时间触发的执行过程,属于很常见的业务逻辑。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

本章将基于上一章已完成的工程 host1-tech/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.jssrc/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 的倍数时收到一则警告邮件:

6b237b86e0f827754b34a9218855c44e3ba7dc1b.jpg

5b65bc396739366f0dd8d89b023b2097fb6045f7.jpg

本章源码

host1-tech/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 服务器(十一):定时任务 的全部内容, 来源链接: utcz.com/a/36842.html

回到顶部