软件的生命周期分为研发、迭代、运维,其中运维最重要,运维里面,监控和报警最重要。
哪个接近稳定安全的运行,哪个就最重要。
异常处理和安全预防
统一异常处理
服务出现异常,统一处理。
代码错误的统一处理
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 = serverErrorFailInfo
if (!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) return
if (typeof onTick !== 'function') return
// 创建定时任务
const c = new CronJob(
cronTime,
onTick,
null, // onComplete 何时停止任务,null
true, // 初始化之后立刻执行,否则要执行 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-server
await checkServerDbConn('https://api.imooc-lego.com/api/db-check')
// h5-server
await checkServerDbConn('https://h5.imooc-lego.com/api/db-check')
// admin-server
await checkServerDbConn('https://admin.imooc-lego.com/api/db-check')
// 统计服务 - OpenAPI
await 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) return
try {
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 } = data
if (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.js
src/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.message
const errStack = error.stack
alarmContent = `<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 ,它会记录服务器的使用情况,然后发送到阿里的后台。你可以设置监监控项。