本文将以解决一个ping告警失败的问题为切入点,介绍如何利用CAT-Home端的日志来简化和加深CAT-HOME端的源码理解。
1. 概述
最近在测试Cat的告警功能时,惊讶地发现之前已经测试通过的ping告警功能居然无法成功重现了,这就很尴尬了!对于目前的我而言,CAT整体就是一个大黑盒。不出现明显的错误,愣是有种无从下手的感觉。
2. 思路
既然发现了问题,即使感觉无从下手,也必须强迫自己去面对。毕竟不能指望问题会自己把自己解决掉。
回想一下Cat-Home的设计思路,以及平时在进行Cat控制台操作时的所见所闻,大致可以作出如下判断:既然CAT-HOME作为Server运行时,同时也是作为一个Client不断向服务端上报自身的运行情况,那么应该可以在控制台上缩小问题的规模,进而最终定位和解决问题。
3. 探究
正如本文开头部分提到的,这次我们就以ping告警失败作为切入点来进行探究。
从官方文档告警文档中得到的提示ping告警是一种HeartBeat检测
,我们最终找到了cat-home
项目中的ThirdPartyAlert
类。
3.1 ThirdPartyAlert
类
在经过一番确认之后,我们基本可以认定就是这个类实现了ping告警的功能。接下来我们就对这个类的实现细节进行一些探究。
从以上的继承链来看,ThirdPartyAlert
的实现还是相当简单的(这也符合CAT一直所秉承的开发原则——”简单的架构就是最好的架构”)。我们需要关注的重点应该只有其实现的java.lang.Runnable
接口。
// ThirdPartyAlert实现的java.lang.Runnable接口
@Override
public void run() {
// 启动时,保证线程休眠到下一个分钟的开始处, 才开始执行; 休眠的刻度以毫秒为单位
boolean active = TimeHelper.sleepToNextMinute();
while (active) {
// 每分钟创建一个type为AlertThirdParty的 Transaction; 参见下方的图1
Transaction t = Cat.newTransaction("AlertThirdParty", TimeHelper.getMinuteStr());
// 开始执行前的时刻
long current = System.currentTimeMillis();
try {
List<ThirdPartyAlertEntity> alertEntities = new ArrayList<ThirdPartyAlertEntity>();
// m_entities中的Item最终来源属于另外一个类ThirdPartyAlertBuilder中的逻辑; 具体是 ThirdPartyAlertBuilder.HttpReconnector
// 对于ping告警,一般是2次ping不通或者超时则告警; 具体原因正是这里, 首次ping失败会直接进行HttpReconnector的逻辑;如果依然ping不通,就会正式进入告警环节。
while (m_entities.size() > 0) {
ThirdPartyAlertEntity entity = m_entities.poll(5, TimeUnit.MILLISECONDS);
alertEntities.add(entity);
}
// 按照上面的讲解, 代码逻辑执行到这里时, alertEntities字段中存放的正是本次需要告警的项目
// 按Domain对ThirdParty Alert进行分组
Map<String, List<ThirdPartyAlertEntity>> domain2AlertMap = buildDomain2AlertMap(alertEntities);
for (Entry<String, List<ThirdPartyAlertEntity>> entry : domain2AlertMap.entrySet()) {
String domain = entry.getKey();
List<ThirdPartyAlertEntity> thirdPartyAlerts = entry.getValue();
// 按Domain构建AlertEntity
AlertEntity entity = new AlertEntity();
entity.setDate(new Date()).setContent(thirdPartyAlerts.toString()).setLevel(AlertLevel.WARNING);
entity.setMetric(getName()).setType(getName()).setGroup(domain);
// 将本次需要告警的项目封装为统一的AlertEntity实例, 交由AlertManager进行统一的通知(sms/email/weixin)
m_sendManager.addAlert(entity);
}
t.setStatus(Transaction.SUCCESS);
} catch (Exception e) {
t.setStatus(e);
Cat.logError(e);
} finally {
t.complete();
}
// 计算执行任务的耗时
long duration = System.currentTimeMillis() - current;
try {
// 如果耗时小于一分钟, 则休眠满一分钟; 否则直接进行下次调度
// DURATION 的值为 一分钟的毫秒数 - 60 * 1000L
if (duration < DURATION) {
Thread.sleep(DURATION - duration);
}
} catch (InterruptedException e) {
active = false;
}
}
}
图1(注意图中的45代表当前已经是一个小时里的第45分钟,所以该类Transaction已经累计了45条记录)
3.2 ThirdPartyAlertBuilder
类
前面我们提到,本次需要进行告警的项目正是有此类产生的。而通过观察其继承链我们可以看到,关键性的方法依然是其实现的接口java.lang.Runnable
:
@Override
public void run() {
boolean active = true;
while (active) {
// 本次循环开始的时刻
long current = System.currentTimeMillis();
// 一个专门的Transaction; 参见下方的图2
Transaction t = Cat.newTransaction("ReloadTask", "AlertThirdPartyBuilder");
try {
// 此方法的逻辑就是,对于配置的请求地址,首次请求无正常响应, 则马上另起线程进行重连
// 对于重连依然没有响应的, 则将其添加到ThirdPartyAlert实例中; 这样就和上面的逻辑契合了。
buildAlertEntities(current);
t.setStatus(Transaction.SUCCESS);
} catch (Exception e) {
t.setStatus(e);
m_logger.error(e.getMessage(), e);
} finally {
t.complete();
}
long duration = System.currentTimeMillis() - current;
try {
if (duration < DURATION) {
Thread.sleep(DURATION - duration);
}
} catch (InterruptedException e) {
active = false;
}
}
}
图2
3.3 同一package下的其他类
3.3.1 HttpConnector
类
CAT正是使用此类来进行get/post请求,进而验证连通性。
3.3.2 ThirdPartyConfigManager
类
负责从数据库,或者默认的配置文件中读取thirdParty相关的配置信息。
// 数据库存放配置信息对应的name字段值
private static final String CONFIG_NAME = "thirdPartyConfig";
数据库截图
3.4 AlertManager
类
本类对于CAT-Home有着关键性的作用,告警的发送正是在此类中完成调度,
此类在实例化时,借助IOC容器即在后台进行着两个发送告警通知的任务。
// 实现的Initializable接口方法
@Override
public void initialize() throws InitializationException {
// SendExecutor在其实现中回调AlertManager.send, 发送告警信息
Threads.forGroup("cat").s在111tart(new SendExecutor());
// RecoveryAnnouncer在其实现中回调AlertManager.sendRecoveryMessage, 发送恢复告警信息
Threads.forGroup("cat").start(new RecoveryAnnouncer());
}
我们还可以在本类中找到所接收到的恢复告警邮件等的内容模板。
3.5 SenderManager
类
最终来看,AlertManager
也是一个调度者,而将具体的告警发送的实现方式调度给了SenderManager
。
public boolean sendAlert(AlertChannel channel, AlertMessageEntity message) {
String channelName = channel.getName();
try {
boolean result = true;
String str = "nosend";
// 如果本服务端负责发送告警
if (m_configManager.isSendMachine()) {
// 观察Sender的继承链, 就能看到mail, sms, weixin三种实现方式
Sender sender = m_senders.get(channelName);
result = sender.send(message);
// 字段 str 应该反映的是 发送情况,即是否发送成功
str = String.valueOf(result);
}
// 以后如果发现告警失败,我们就可以关注这个Event, 查看发送是否成功
// 如果这个存在, 则表明警告已产生, 只是发送告警失败; 反之若不存在, 则代表告警都没有正常生成。
Cat.logEvent("Channel:" + channelName, message.getType() + ":" + str, Event.SUCCESS, null);
return result;
} catch (Exception e) {
Cat.logError(e);
return false;
}
}
言语过于抽象,遂贴上几幅图以方便理解。
由以上两幅截图,我们可以确定警告已经如期产生,只是在最终的发送阶段除了差池。而在定位了以上问题之后,笔者很快就找到了问题的根源。轻松地排除了错误。
4. 补充
CAT-Home的控制台不失为一个快速了解CAT服务端的捷径。我们可以通过使用IDE的查找功能快速定位感兴趣的细节。例如上面的“Channel:mail” Event, 我们就可以使用如下方式快速定位。注意这里的查找内容中,把前面的
"
带上可以大大缩小查找范围。
本文所讨论的相关类所在的
com.dianping.cat.report.alert
package 。 旗下的子package就分别属于一种类型告警。