软件的生命周期分为研发、迭代、运维,其中运维最重要,运维里面,监控和报警最重要。
哪个接近稳定安全的运行,哪个就最重要。
异常处理和安全预防
统一异常处理
服务出现异常,统一处理。
代码错误的统一处理
dev和测试环境,我们期望尽量详尽,具体地暴露出代码的异常,可以帮助我们尽快排查,解决问题。
而线上环境正好相反,我们期望隐藏这些异常,给用户比较友好的错误提示解密。
但是线上环境错综复杂,我们无法枚举出会有什么样的异常,所以最好的方法是,统一的异常处理,每次请求,都加一个try…catch包裹。
以中间件的方式来实现
/*** @description 错误处理中间件*/const { ErrorRes } = require('../res-model/index')const { serverErrorFailInfo, notFoundFailInfo } = require('../res-model/failInfo/index')const { isPrd } = require('../utils/env')const { mailAlarm } = require('../alarm/index')/*** 统一错误处理* @param {object} ctx ctx* @param {Function} next next*/async function onerror(ctx, next) {try {await next()} catch (ex) {console.error('onerror middleware', ex)// 报警mailAlarm(`onerror 中间件 - ${ex.message}`, // 统一错误报警,标题中必须错误信息,因为报警会依据标题做缓存ex)const errInfo = serverErrorFailInfoif (!isPrd) {// 非线上环境,暴露错误信息errInfo.data = {message: ex.message,stack: ex.stack,}}ctx.body = new ErrorRes(errInfo)}}/*** 404* @param {object} ctx ctx*/async function onNotFound(ctx) {ctx.body = new ErrorRes(notFoundFailInfo)}module.exports = {onerror,onNotFound,}
预防内存泄漏
在pm2配置,增加max_memory_restart:'300M',如果出现内存泄漏就自动重启。
观察pm2重启频繁,就要去排查内存泄漏的问题。
如果重启不频繁,就可以不用着急解决。
安全预防
常见的Web攻击有
- SQL注入:使用Sequelize可以预防,不用刻意去解决。只要不裸写sql语句,常见的数据库工具,都可以解决sql注入的问题。
XSS:通过vue和react可以防止,h5也可以通过vue ssr防止。
- vue中输出原生html要用v-html
- React要用danerouslySetInnerHtml
- 如果想要单独处理,用
console.dir('<input name="full_name" value="' + escapeHtml(fullName) + '">')// -> '<input name="full_name" value="John "Johnny" Smith">'
CSRF
- 网络攻击
防止网络攻击
阿里云网络防火墙WAF。工作流程:
短信验证码接口
关于获取短信验证码的接口,本来想通过WAF来做单IP的频率设置,即单个IP每个时间段都限制访问次数,但是后来发现这个服务要单独收费,还挺贵。
通过讨论和分析,腾讯云的短信服务也有单个手机号的发送频率限制,所以就暂时没买这个服务。
遇到花销大的事情,要考虑实际情况再做决定。监控和报警
系统如果出了问题,我一定是第一个知道的人。不要等着被用户发现
为系统制定一套监控和报警机制,让系统时刻在掌握之中。
监控需要结合业务和系统。
心跳检测
对接口自动定时体检,时间间隔自己定,不用太频繁,如10min
定时任务
node工具:
/*** @description 心跳检测*/const { CronJob } = require('cron')const checkAllServers = require('./check-server')/*** 开始定时任务* @param {string} cronTime cron 规则* @param {Function} onTick 回调函数*/function schedule(cronTime, onTick) {if (!cronTime) returnif (typeof onTick !== 'function') return// 创建定时任务const c = new CronJob(cronTime,onTick,null, // onComplete 何时停止任务,nulltrue, // 初始化之后立刻执行,否则要执行 c.start() 才能开始'Asia/Shanghai' // 时区,重要!!)// 进程结束时,停止定时任务process.on('exit', () => c.stop())}// 开始定时任务function main() {const cronTime = '*/10 * * * *' // 每 10 分钟检查一次schedule(cronTime, checkAllServers)console.log('设置心跳检测定时任务', cronTime)}main()
这里是同时检测了多个服务。
/*** 检查各个服务*/async function checkAllServers() {console.log('心跳检测 - 开始')// biz-editor-serverawait checkServerDbConn('https://api.imooc-lego.com/api/db-check')// h5-serverawait checkServerDbConn('https://h5.imooc-lego.com/api/db-check')// admin-serverawait checkServerDbConn('https://admin.imooc-lego.com/api/db-check')// 统计服务 - OpenAPIawait checkServerDbConn('https://statistic-res.imooc-lego.com/api/db-check')// 统计服务 - 收集日志await checkImg('https://statistic.imooc-lego.com/event.png')console.log('心跳检测 - 结束')}/*** 测试 API 的数据库连接* @param {string} url url*/async function checkServerDbConn(url = '') {if (!url) returntry {const res = await axios(url)const { data = {}, errno } = res.data/*** 数据格式:{"errno": 0,"data": {"name": "xxx","version": "1.0.1","ENV": "production","redisConn": true,"mysqlConn": true,"mongodbConn": true}}*/const { name, version, ENV } = dataif (errno === 0 && name && version && ENV) {console.log('心跳检测成功', url)return}// 报警mailAlarm(`心跳检测失败 ${url}`, res)} catch (ex) {// 报警mailAlarm(`心跳检测失败 ${url}`, ex)}}
用pm2常驻一个服务,用一个核数去运行就好,因为10分钟运行一次不需要占用太多内存。
然后在部署脚本deploy.sh中,线上服务启动后,开启这个心跳检测服务。
"heart-beat-check": "pm2 start bin/heart-beat-check.pm2.json",
心跳检测服务单独拆分
现在心跳检测依附于edior-server。如果项目慢慢变大,可以把心跳检测服务单独拆分出来,作为单独的服务,也可以使用第三方的监控服务,或者大公司自研的监控服务。
报警
报警范围
- 心跳检测
- 统一异常处理
middlewares/error.jssrc/app.js中的app.on('error')
第三方服务
短信,不采用
- 花钱
- 如果频繁触发报警,可能触发短信的频率限制机制
- 邮件 ,采用
- 免费
- 无限制
- 手机接收邮件很方便
- 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 = {
from: '"imooc-lego" <imooclego@126.com>',to: mails.join(','),subject,
} if (content.indexOf(‘<’) === 0) {
// html 内容conf.html = content
} else {
// text 内容conf.text = content
}
// 发送邮件 const res = await transporter.sendMail(conf)
console.log(‘mail sent: %s’, res.messageId) }
module.exports = sendMail
报警需要做缓存,缓存时间2分钟。```shell/*** @description 错误报警* @author 双越*/const sendMail = require('../vendor/sendMail')const { adminMails } = require('../config/index')const { isPrd, isDev, isTest } = require('../utils/env')const { cacheSet, cacheGet } = require('../cache/index')/*** 检查缓存* @param {string} title title*/async function getCache(title) {const key = `mail_alarm_${title}`const res = await cacheGet(key)if (res == null) {// 缓存中没有,则加入缓存cacheSet(key,1, // 缓存 val 无所谓,有值就行2 * 60 // 缓存 2 分钟,即 2 分钟内不频繁发送)}return res}/*** 邮件报警,普通报警* @param {string} title title* @param {Error|string|Object} error 错误信息*/async function mailAlarm(title, error) {if (isDev || isTest) {// dev test 环境下不发报警,没必要console.log('dev test 环境,不发报警')return}if (!title || !error) return// 检查缓存。title 相同的报警,不要频繁发送const cacheRes = await getCache(title)if (cacheRes != null) return // 尚有缓存,说明刚刚发送过,不在频繁发送// 拼接标题let alarmTitle = `【慕课乐高】报警 - ${title}`if (!isPrd) {alarmTitle += '(非线上环境)' // 和线上作出区分}// 拼接内容let alarmContent = ''if (typeof error === 'string') {// 报错信息是字符串alarmContent = `<p>${error}</p>`} else if (error instanceof Error) {// 报错信息是 Error 对象const errMsg = error.messageconst errStack = error.stackalarmContent = `<h1>${errMsg}</h1><p>${errStack}</p>`} else {// 其他情况,不报警alarmContent = `<p>${JSON.stringify(error)}</p>`return}alarmContent += '<p><b>请尽快处理问题,勿回复此邮件</b></p>'try {// 发送邮件await sendMail(adminMails, alarmTitle, alarmContent)} catch (ex) {console.error('邮件报警错误', ex)}}module.exports = {mailAlarm,}
alinnode服务器监控
实时检测CPU内存,硬盘的健康状况。
免费的服务器性能平台alinode,实时监控。在服务器安装agenthub ,它会记录服务器的使用情况,然后发送到阿里的后台。你可以设置监监控项。
