软件的生命周期分为研发、迭代、运维,其中运维最重要,运维里面,监控和报警最重要。
哪个接近稳定安全的运行,哪个就最重要。

异常处理和安全预防

统一异常处理

服务出现异常,统一处理。

代码错误的统一处理

dev和测试环境,我们期望尽量详尽,具体地暴露出代码的异常,可以帮助我们尽快排查,解决问题。
而线上环境正好相反,我们期望隐藏这些异常,给用户比较友好的错误提示解密。
但是线上环境错综复杂,我们无法枚举出会有什么样的异常,所以最好的方法是,统一的异常处理,每次请求,都加一个try…catch包裹。
以中间件的方式来实现

  1. /**
  2. * @description 错误处理中间件
  3. */
  4. const { ErrorRes } = require('../res-model/index')
  5. const { serverErrorFailInfo, notFoundFailInfo } = require('../res-model/failInfo/index')
  6. const { isPrd } = require('../utils/env')
  7. const { mailAlarm } = require('../alarm/index')
  8. /**
  9. * 统一错误处理
  10. * @param {object} ctx ctx
  11. * @param {Function} next next
  12. */
  13. async function onerror(ctx, next) {
  14. try {
  15. await next()
  16. } catch (ex) {
  17. console.error('onerror middleware', ex)
  18. // 报警
  19. mailAlarm(
  20. `onerror 中间件 - ${ex.message}`, // 统一错误报警,标题中必须错误信息,因为报警会依据标题做缓存
  21. ex
  22. )
  23. const errInfo = serverErrorFailInfo
  24. if (!isPrd) {
  25. // 非线上环境,暴露错误信息
  26. errInfo.data = {
  27. message: ex.message,
  28. stack: ex.stack,
  29. }
  30. }
  31. ctx.body = new ErrorRes(errInfo)
  32. }
  33. }
  34. /**
  35. * 404
  36. * @param {object} ctx ctx
  37. */
  38. async function onNotFound(ctx) {
  39. ctx.body = new ErrorRes(notFoundFailInfo)
  40. }
  41. module.exports = {
  42. onerror,
  43. onNotFound,
  44. }

预防内存泄漏

在pm2配置,增加max_memory_restart:'300M',如果出现内存泄漏就自动重启。
观察pm2重启频繁,就要去排查内存泄漏的问题。
如果重启不频繁,就可以不用着急解决。

安全预防

常见的Web攻击有

  • SQL注入:使用Sequelize可以预防,不用刻意去解决。只要不裸写sql语句,常见的数据库工具,都可以解决sql注入的问题。
  • XSS:通过vue和react可以防止,h5也可以通过vue ssr防止。

    • vue中输出原生html要用v-html
    • React要用danerouslySetInnerHtml
    • 如果想要单独处理,用
      1. console.dir('<input name="full_name" value="' + escapeHtml(fullName) + '">')
      2. // -> '<input name="full_name" value="John &quot;Johnny&quot; Smith">'
  • CSRF

  • 网络攻击

    防止网络攻击

    阿里云网络防火墙WAF。工作流程:
    监控和报警 - 图1
    image.png

    短信验证码接口

    关于获取短信验证码的接口,本来想通过WAF来做单IP的频率设置,即单个IP每个时间段都限制访问次数,但是后来发现这个服务要单独收费,还挺贵。
    通过讨论和分析,腾讯云的短信服务也有单个手机号的发送频率限制,所以就暂时没买这个服务。
    遇到花销大的事情,要考虑实际情况再做决定。

    监控和报警

    系统如果出了问题,我一定是第一个知道的人。不要等着被用户发现

为系统制定一套监控和报警机制,让系统时刻在掌握之中。
监控需要结合业务和系统。
监控和报警 - 图3

心跳检测

对接口自动定时体检,时间间隔自己定,不用太频繁,如10min

定时任务

node工具:

  1. /**
  2. * @description 心跳检测
  3. */
  4. const { CronJob } = require('cron')
  5. const checkAllServers = require('./check-server')
  6. /**
  7. * 开始定时任务
  8. * @param {string} cronTime cron 规则
  9. * @param {Function} onTick 回调函数
  10. */
  11. function schedule(cronTime, onTick) {
  12. if (!cronTime) return
  13. if (typeof onTick !== 'function') return
  14. // 创建定时任务
  15. const c = new CronJob(
  16. cronTime,
  17. onTick,
  18. null, // onComplete 何时停止任务,null
  19. true, // 初始化之后立刻执行,否则要执行 c.start() 才能开始
  20. 'Asia/Shanghai' // 时区,重要!!
  21. )
  22. // 进程结束时,停止定时任务
  23. process.on('exit', () => c.stop())
  24. }
  25. // 开始定时任务
  26. function main() {
  27. const cronTime = '*/10 * * * *' // 每 10 分钟检查一次
  28. schedule(cronTime, checkAllServers)
  29. console.log('设置心跳检测定时任务', cronTime)
  30. }
  31. main()

这里是同时检测了多个服务。

  1. /**
  2. * 检查各个服务
  3. */
  4. async function checkAllServers() {
  5. console.log('心跳检测 - 开始')
  6. // biz-editor-server
  7. await checkServerDbConn('https://api.imooc-lego.com/api/db-check')
  8. // h5-server
  9. await checkServerDbConn('https://h5.imooc-lego.com/api/db-check')
  10. // admin-server
  11. await checkServerDbConn('https://admin.imooc-lego.com/api/db-check')
  12. // 统计服务 - OpenAPI
  13. await checkServerDbConn('https://statistic-res.imooc-lego.com/api/db-check')
  14. // 统计服务 - 收集日志
  15. await checkImg('https://statistic.imooc-lego.com/event.png')
  16. console.log('心跳检测 - 结束')
  17. }
  18. /**
  19. * 测试 API 的数据库连接
  20. * @param {string} url url
  21. */
  22. async function checkServerDbConn(url = '') {
  23. if (!url) return
  24. try {
  25. const res = await axios(url)
  26. const { data = {}, errno } = res.data
  27. /**
  28. * 数据格式:
  29. {
  30. "errno": 0,
  31. "data": {
  32. "name": "xxx",
  33. "version": "1.0.1",
  34. "ENV": "production",
  35. "redisConn": true,
  36. "mysqlConn": true,
  37. "mongodbConn": true
  38. }
  39. }
  40. */
  41. const { name, version, ENV } = data
  42. if (errno === 0 && name && version && ENV) {
  43. console.log('心跳检测成功', url)
  44. return
  45. }
  46. // 报警
  47. mailAlarm(`心跳检测失败 ${url}`, res)
  48. } catch (ex) {
  49. // 报警
  50. mailAlarm(`心跳检测失败 ${url}`, ex)
  51. }
  52. }

用pm2常驻一个服务,用一个核数去运行就好,因为10分钟运行一次不需要占用太多内存。
然后在部署脚本deploy.sh中,线上服务启动后,开启这个心跳检测服务。

  1. "heart-beat-check": "pm2 start bin/heart-beat-check.pm2.json",

心跳检测服务单独拆分

现在心跳检测依附于edior-server。如果项目慢慢变大,可以把心跳检测服务单独拆分出来,作为单独的服务,也可以使用第三方的监控服务,或者大公司自研的监控服务。

报警

出问题第一时间发送邮件

报警范围

  • 心跳检测
  • 统一异常处理
    • middlewares/error.js
    • src/app.js中的app.on('error')
  • 第三方服务

    • 发送短信
    • 上传oss
    • 百度云-内容检查

      报警方式

  • 短信,不采用

    • 花钱
    • 如果频繁触发报警,可能触发短信的频率限制机制
  • 邮件 ,采用
    • 免费
    • 无限制
    • 手机接收邮件很方便
    • npm 工具:nodemailer,可以很方便的发送邮件

      nodemailer配置

      申请一个邮箱,开启SMTP服务,记住授权密码,此邮箱作为发送方。 ```shell /**
      • @description 发送邮件
      • @author 双越 */

const nodemailer = require(‘nodemailer’)

// 创建发送邮件的客户端 const transporter = nodemailer.createTransport({ host: ‘smtp.126.com’, port: 465, secure: true, // true for 465, false for other ports auth: { user: ‘imooclego@126.com’, pass: ‘xxx’, }, })

/**

  • @param {Array} mails 邮箱列表
  • @param {string} subject 邮件主题
  • @param {string} content 邮件内容,支持 html 格式 */ async function sendMail(mails = [], subject = ‘’, content = ‘’) { if (!mails.length) return if (!subject || !content) return

    // 邮件配置 const conf = {

    1. from: '"imooc-lego" <imooclego@126.com>',
    2. to: mails.join(','),
    3. subject,

    } if (content.indexOf(‘<’) === 0) {

    1. // html 内容
    2. conf.html = content

    } else {

    1. // text 内容
    2. conf.text = content

    }

    // 发送邮件 const res = await transporter.sendMail(conf)

    console.log(‘mail sent: %s’, res.messageId) }

module.exports = sendMail

  1. 报警需要做缓存,缓存时间2分钟。
  2. ```shell
  3. /**
  4. * @description 错误报警
  5. * @author 双越
  6. */
  7. const sendMail = require('../vendor/sendMail')
  8. const { adminMails } = require('../config/index')
  9. const { isPrd, isDev, isTest } = require('../utils/env')
  10. const { cacheSet, cacheGet } = require('../cache/index')
  11. /**
  12. * 检查缓存
  13. * @param {string} title title
  14. */
  15. async function getCache(title) {
  16. const key = `mail_alarm_${title}`
  17. const res = await cacheGet(key)
  18. if (res == null) {
  19. // 缓存中没有,则加入缓存
  20. cacheSet(
  21. key,
  22. 1, // 缓存 val 无所谓,有值就行
  23. 2 * 60 // 缓存 2 分钟,即 2 分钟内不频繁发送
  24. )
  25. }
  26. return res
  27. }
  28. /**
  29. * 邮件报警,普通报警
  30. * @param {string} title title
  31. * @param {Error|string|Object} error 错误信息
  32. */
  33. async function mailAlarm(title, error) {
  34. if (isDev || isTest) {
  35. // dev test 环境下不发报警,没必要
  36. console.log('dev test 环境,不发报警')
  37. return
  38. }
  39. if (!title || !error) return
  40. // 检查缓存。title 相同的报警,不要频繁发送
  41. const cacheRes = await getCache(title)
  42. if (cacheRes != null) return // 尚有缓存,说明刚刚发送过,不在频繁发送
  43. // 拼接标题
  44. let alarmTitle = `【慕课乐高】报警 - ${title}`
  45. if (!isPrd) {
  46. alarmTitle += '(非线上环境)' // 和线上作出区分
  47. }
  48. // 拼接内容
  49. let alarmContent = ''
  50. if (typeof error === 'string') {
  51. // 报错信息是字符串
  52. alarmContent = `<p>${error}</p>`
  53. } else if (error instanceof Error) {
  54. // 报错信息是 Error 对象
  55. const errMsg = error.message
  56. const errStack = error.stack
  57. alarmContent = `<h1>${errMsg}</h1><p>${errStack}</p>`
  58. } else {
  59. // 其他情况,不报警
  60. alarmContent = `<p>${JSON.stringify(error)}</p>`
  61. return
  62. }
  63. alarmContent += '<p><b>请尽快处理问题,勿回复此邮件</b></p>'
  64. try {
  65. // 发送邮件
  66. await sendMail(adminMails, alarmTitle, alarmContent)
  67. } catch (ex) {
  68. console.error('邮件报警错误', ex)
  69. }
  70. }
  71. module.exports = {
  72. mailAlarm,
  73. }

alinnode服务器监控

实时检测CPU内存,硬盘的健康状况。
免费的服务器性能平台alinode,实时监控。在服务器安装agenthub ,它会记录服务器的使用情况,然后发送到阿里的后台。你可以设置监监控项。
截屏2022-10-02 15.23.47.png