配置定时工作

定时工作也就是由工夫触发的执行过程,属于很常见的业务逻辑。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.jsconst { 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.jsconst { 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.jsconst 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.localGITHUB_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.jsconst { 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.jsmodule.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.jsconst { 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.jsconst inspectAttackSchedule = require('./inspectAttack');module.exports = async function initSchedules() {  await inspectAttackSchedule();};
// src/server.jsconst 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 的倍数时收到一则正告邮件:

本章源码

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 服务器(十一):定时工作