前言
首先介绍下背景:我们公司的产品,会直接部署在甲方,因为甲方比较多,且他们包含个性化需求较多,所以,每个甲方可以理解为我们产品的一条 git 分支. 由于甲方的机器环境,网络环境各不相同,时常出现一些运行时的问题,于是,我设计了这套简易的智能监控系统,用来实时监测各个甲方接口情况.
适用范围
该套方案衍生的适用范围如下:
docker 下多容器运行项目,且暂不具备接口健康检测,该套方案可实施检测多个运行点状态情况.
公司同一套代码有多个运行环境,需要监控各个环境状态.
项目分支化后,部署在各个甲方(含个性化需求)------ 我们是这个.
效果图
鼠标移到某个接口,可看某节点产生的时间和当时接口的出参.[这里只显示变动节点]
其中 200 为出参 json 的一个状态码,我们项目普遍采用.如出参无法解析出状态码(比如服务器 500 错误),则不显示.
单击节点可以再次模拟该次节点,在浏览器中显出出参.
双击某个节点可以复制该节点出参.
主体分析
因为我们产品已经完成了前后端分离(前几篇文章有介绍),所以,我们重点监控接口.
我的方案是:
每隔一定时间,比如 3 分钟,主动 get(或者 post)一下 http 接口(含用户信息),获得接口出参.
比对此次出参和前一次该接口出参是否相同,如果不通则标记下.
前端展示,每个接口对应每个项目所有变动的节点,并排列一张节点差异图.
这里,我们监控并不关心接口出参的内容,不对节点出参进行校验是否合法,因为我们面向所有接口.
对某个接口分析
这里因为每个公司的各个接口,入参,出参加密方式不通,而且身份认证也是不相同,我们会在独立写一个方法来实现加解密,所以,我只介绍未加密,已解密的参数;关于出参下文以 {"code":"200","data":null,"message"::"success" 为例,身份认证我以简单 token 进行讲解本文.
某个获取新闻列表接口:
http://*.com/v1/news?token=2c789e34dc81d79feba6a005ad63902b
解密后的出参:
{"code":"200","data":[{"id":"1","title":"这是一条假新闻","url":"http://",{"id":"2","title":"这是一条假新闻","url":"http://"],"message"::"success"
由接口可知
GET 方法
入参 token=2c789e34dc81d79feba6a005ad63902b
出参为 {"code":"200","data":[],"message"::"success"
其中 *.com 对于各个环境可能各不相同,比如容器下为 10.0.0.1,10.0.0.2 多个地址
其中 token 在各个甲方不相同.
添加新闻获取接口时:
相对 url:
v1/news
请求类型:
get
body 主体:
token={token}
corn 表达式:
0 0 / 3 * **?(每隔3分钟执行一次)
添加 A 环境时:
host(必填)
http://a.com
参数(list)
token 2c789e34dc81d79feba6a005ad63902b
添加 B 环境时:
host(必填)
http://b.com
参数(list)
token 4297f44b13955235245b2497399d7a93
绑定接口和环境
一个接口可绑定多个环境
一个环境可绑定多个接口
数据库中只记录单向绑定,逻辑上是双向的.
比如在新闻获取接口绑定 A,B 环境,则每隔 3 分钟请求一轮接口.
A:
A 环境 host + 相对 url=
http://a.com/v1/news
请求类型:
get
body 主体:
B 环境 host + 相对 url=
token = 2c789e34dc81d79feba6a005ad63902b
B:
http://b.com/v1/news
请求类型:
get
body 主体:
token = 4297f44b13955235245b2497399d7a93
我们框架本身并不会关心有几个参数,只会遍历接口中带占位符的,把占位符中字段替换为环境里的参数 list 中变量. 比如一个 body 为 token=2c789e34dc81d79feba6a005ad63902b&type=1&mobile=2
则添加接口时,body 填入:
token={token}&type={type}&mobile={mobile} 其中占位符可以随便起名字
配置环境时参数(list)如下:
参数和占位符里的对应,和 url 其他变量不相关.
k v
token 2c789e34dc81d79feba6a005ad63902b
type 1
mobile 2
前端展示
横轴代表时间,每个变动的节点都是一次出参变更,节点包含,当时的入参,出参,单击左键可以再次模拟请求得出解密后的出参;双击可以复制出参.
对于不同的状态码,我们有不同的颜色,对应自己产品中错误码选择合适颜色即可,错误等级越高,颜色越显眼.
某个节点右键可以查看历史解决方案,以及新增新的解决方案,比如解决了这个错误代码为 999,是重启服务解决的,则,输入重启服务,再点添加. 下次其他人就可以看到这个问题怎么解决的,优先参照做.
不同的展现
包含两种不同的纬度,可以直接点击进入,查看 A 环境所有接口的监控情况,或者查看接口 1 所有环境的监控情况.
或者:
环境编辑界面:
任务编辑界面:
核心技术
既然是周期性监控,自然少不了使用 cron 表达式,我们又是 java 项目,所以采用了 quartz 框架.
quartz 核心代码
停止和暂停均可使用
public class QuartzSchedule {
private static SchedulerFactory sf = new StdSchedulerFactory();
private static Scheduler sched;
final static String groupName = "task";
public static void init() throws IOException,
SchedulerException {
//查询所有需要执行的任务和项目列表
// 使用类加载器,加载mybatis的配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 构造sqlSession工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
MprojectTaskDao mprojectTaskDao = sqlSession.getMapper(MprojectTaskDao.class);
sched = sf.getScheduler();
//只查询所有在m_project_include表里面的任务
List < MprojectTask > mprojectTaskList = mprojectTaskDao.findList();
for (MprojectTask mprojectTask: mprojectTaskList) {
startJob(mprojectTask);
}
}
public static void stopTask(MprojectTask mprojectTask) {
TriggerKey triggerKey = TriggerKey.triggerKey(mprojectTask.getId(), groupName);
try {
sched.pauseTrigger(triggerKey); // 停止触发器
sched.unscheduleJob(triggerKey); // 移除触发器
sched.deleteJob(JobKey.jobKey(mprojectTask.getId(), groupName)); // 删除任务
} catch(Exception e) {
e.printStackTrace();
}
}
/**/
job 核心代码
* /
public static void startTask(MprojectTask mprojectTask) {
/ / 无论是否关闭,
先关闭任务再开启.stopTask(mprojectTask);
startJob(mprojectTask);
}
private static void startJob(MprojectTask mprojectTask) {
try {
JobDetail jobDetail = JobBuilder.newJob(TaskQuzrtzJob.class).withIdentity(mprojectTask.getId(), groupName).build();
// 触发器
TriggerBuilder < Trigger > triggerBuilder = TriggerBuilder.newTrigger();
// 触发器名,触发器组
triggerBuilder.withIdentity(mprojectTask.getId(), groupName);
triggerBuilder.startNow();
// 触发器时间设定
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(mprojectTask.getCron()));
// 创建Trigger对象
CronTrigger trigger = (CronTrigger) triggerBuilder.build();
// 调度容器设置JobDetail和Trigger
sched.scheduleJob(jobDetail, trigger);
// 启动
if (!sched.isShutdown()) {
sched.start();
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
数据量
public class TaskQuzrtzJob implements Job {
public TaskQuzrtzJob() throws IOException {}
//JobExecutionContext context传递参数值
public void execute(JobExecutionContext context) {
//通过context得到该任务的所有环境
//遍历所有环境
//拼凑出请求地址
//使用封装的框架加密发送请求
//得出出参解密后的结果
//与上次进行比对,不相同则标记,入节点表
//入记录表
}
}
因为我们这个监控每个接口(任务)有多个环境,假设 100 个接口 100 个环境,每 3 分钟监控一次,则每天记录量为
100 * 100 * 20 * 24=4800 000 条记录,这个记录比较惊人,所以我们分了两张表,一张表只记录记录,另外一张表记录变更,展示节点的时候只查询变更表,可解决性能上的问题.
前端
前端采用 CSS 来画圆和颜色
线条使用 line 画线
round {
border-radius: 50%;
text-align: center;
width: 25px;
height: 25px;
line-height: 25px;
}
.on {
border: 1px solid #7CBA23;
}
整体采用 angular js 渲染列表
line {
border - bottom: 1px solid gainsboro;
height: 2px;
width: 20px;
}
数据库
我们使用 mysql 数据库,mybatis 框架连接.
异常提醒
我们会对每个接口和状态码进行关联,异常时,发送邮件,测试人员核实问题后反馈给开发,开发解决后,录入解决方案,以便下次查询.
总结
这个平台已经试行 1 个多月了,总体满足我们的需求,以往,我们跑测试脚本,往往不能实时监测服务器健康状态,现在,任何一个环境有问题,我们都实时,并且可以统计出某个环境从什么时间到什么时间接口处于异常.
这套平台,功能不多,但极大的简化了我们的对环境稳定性的评估,也收集了大量的数据信息便于后期统计分析. 其次,该方案稍作改造,可运用于多种监测场景中.
展望
未来,我会融入一些智能化的分析,比如下文是某次邮件提醒的内容:
(智能监控) 发现《A 客户私有环境》中 【获取新闻列表】 接口异常,异常代码为 102; 出参为 {"data":null,"message":"成功","state":102}
历史解决方案有:
到管理端重启容器.
xx 表中有异常脏数据. 根据历史统计,方案 1 可能性为 80%,方案 2 为 10%.
该环境接口在 5 分钟前是正常的,出参为 {"data":null,"message":"暂无数据","state":200}
发现有另外 2 个其它环境异常,分别为 XXX,YYY 环境.但异常状态和该校不相似,排除接口 war 包 bug 的原因.
由于《XXX 环境》其他接口在 5 分钟内某次监控均为异常,所以(智能监控) 已自主调用容器自愈模块,目前已恢复服务,本次收集数据耗时 5 分钟,分析问题耗时 100 毫秒,自愈耗时 60 秒,该环境中断服务时间为 6 分钟,本月中断服务总时长为 15 分钟,高可用性为 98.33%.
over
来源: https://juejin.im/post/5a56cabd51882573432d11ef