大众点评CAT微服务监控架构对于消息的具体处理,是由消息分析器完成的,消息分析器会轮训读取PeriodTask中队列的消息来处理,一共有12类消息分析器,处理后的结果就是生成各类报表。
消息分析器的构建
在周期Period构造函数中,我们会通过m_analyzerManager.getAnalyzer(name, startTime)获取分析器(MessageAnalyzer)列表。getAnalyzer函数源码如下,首先会清理2小时之前的分析器,然后从m_analyzers中获取分析器(MessageAnalyzer),我们先来看看m_analyzers 的结构
Map<Long, Map<String, List<MessageAnalyzer>>>
最外层Map的key的类型为long,代表由startTime对应的周期。value还是一个Map,Map的key类型是String,是分析器的名字,代表一类分析器,value是MessageAnalyzer列表,同一类分析器,至少有一个MessageAnalyzer实例,对于复杂耗时的分析任务,我们通常会开启更多的实例处理。
如果在Map中没有找到我们需要的分析器,我们就创建,创建的过程函数会通过synchronized给map上锁,以保证创建过程map同时只能被一个线程访问,保证了线程安全。
分析器创建之后会被初始化,然后放入m_analyzers中,
public class DefaultMessageAnalyzerManager extends ContainerHolder implements MessageAnalyzerManager, Initializable,LogEnabled { private Map<Long, Map<String, List<MessageAnalyzer>>> m_analyzers = new HashMap<Long, Map<String, List<MessageAnalyzer>>>(); @Override public List<MessageAnalyzer> getAnalyzer(String name, long startTime) { // remove last two hour analyzer Map<String, List<MessageAnalyzer>> temp = m_analyzers.remove(startTime - m_duration * 2); ... Map<String, List<MessageAnalyzer>> map = m_analyzers.get(startTime); if (map == null) { synchronized (m_analyzers) { map = m_analyzers.get(startTime); if (map == null) { map = new HashMap<String, List<MessageAnalyzer>>(); m_analyzers.put(startTime, map); } } } List<MessageAnalyzer> analyzers = map.get(name); if (analyzers == null) { synchronized (map) { analyzers = map.get(name); if (analyzers == null) { analyzers = new ArrayList<MessageAnalyzer>(); MessageAnalyzer analyzer = lookup(MessageAnalyzer.class, name); analyzer.setIndex(0); analyzer.initialize(startTime, m_duration, m_extraTime); analyzers.add(analyzer); int count = analyzer.getAnalyzerCount(); for (int i = 1; i < count; i++) { MessageAnalyzer tempAnalyzer = lookup(MessageAnalyzer.class, name); tempAnalyzer.setIndex(i); tempAnalyzer.initialize(startTime, m_duration, m_extraTime); analyzers.add(tempAnalyzer); } map.put(name, analyzers); } } } return analyzers; } }
我们再来看看分析器的大体结构
每个分析器都包含有多个报表,报表交由报表管理器(ReportManage)管理,报表在报表管理器中存储结构如下:
Map<Long, Map<String, T>> m_reports
最外层是个Map, key 为long类型,代表的是当前时间周期的报表,value还是一个Map,key类型为String,代表的是不同的domain,一个domain可以理解为一个 Project,value是不同report对象,在分析器处理报表的时候,我们会通过周期管理器(DefaultReportManage)的getHourlyReport方法根据周期时间和domain获取对应的Report。
分析器分析上报的消息之后,生成相应的报表存于Report对象中,报表实体类XxxReport的结构都是由上一章讲的代码自动生成器生成的,配置位于 cat-sonsumer/src/main/resources/META-INFO/dal/model/*.xml 中。
TopAnalyzer
TopAnalyzer分析生成每个周期的报表,不区分domain,所有domain的数据都会汇总到所在周期的domain='cat'的这个报表下去:
getHourlyReport(getStartTime(), Constants.CAT, true);
TopAnalyzer会处理指定Type类型的Event消息,具体有哪些类型会被处理会在 plexus/components-cat-consumer.xml 文件中配置。如下
<implementation>com.dianping.cat.consumer.top.TopAnalyzer</implementation> <instantiation-strategy>per-lookup</instantiation-strategy> <configuration> <errorType>Error,RuntimeException,Exception</errorType> </configuration>
再来看看TopAnalyzer对Event的处理过程,他会统计当前小时周期内上面类型消息的3个计数。
1、当前小时周期内每分钟,每个domain,也就是每个project的错误计数
2、每个名字对应的错误计数
3、每个IP对应的错误计数
public class TopAnalyzer extends AbstractMessageAnalyzer<TopReport> implements LogEnabled { private void processEvent(TopReport report, MessageTree tree, Event event) { String type = event.getType(); if (m_errorTypes.contains(type)) { String domain = tree.getDomain(); String ip = tree.getIpAddress(); String exception = event.getName(); long current = event.getTimestamp() / 1000 / 60; int min = (int) (current % (60)); Segment segment = report.findOrCreateDomain(domain).findOrCreateSegment(min).incError(); segment.findOrCreateError(exception).incCount(); segment.findOrCreateMachine(ip).incCount(); } } }
EventAnalyzer - 事件发生次数分析
时间分析分析报表会记录Event类型消息的统计汇总信息,每个周期时间,每个domain对应一个EventReport,每个Event报表包含多个Machine对象,按IP区分,注意一下这里的Machine类与后面其它报表的Machine类是有区别的,相同IP下不同类型(Type)的Event信息存在于不同的EventType对象中,EventType记录了该类型消息的总数,失败总数,失败百分比,成功的MessageID,失败的MessageID,tps,以及该类型下各种命名消息。
同一类型但是不同名字(Name)的Event信息存在于不同的EventName对象中,他也会记录该命名消息的总数,失败总数,失败百分比,成功的MessageID,失败的MessageID,tps。
每个EventName对象会存储当前周期时间内,不同类型不同名字的Event消息每分钟的消息总数和失败总数,放在m_ranges字段中。
大家可能疑惑为什么存的成功与失败的MessageID只有一个,而不是一个列表,因为Event报表仅仅只是统计一类事件发生的次数,同类消息做的事情本质上是一样的,所以仅取一条MessageID对应的消息树作为这一类消息的代表。
public class EventAnalyzer extends AbstractMessageAnalyzer<EventReport> implements LogEnabled { private void processEvent(EventReport report, MessageTree tree, Event event, String ip) { int count = 1; EventType type = report.findOrCreateMachine(ip).findOrCreateType(event.getType()); EventName name = type.findOrCreateName(event.getName()); String messageId = tree.getMessageId(); report.addIp(tree.getIpAddress()); type.incTotalCount(count); name.incTotalCount(count); if (event.isSuccess()) { type.setSuccessMessageUrl(messageId); name.setSuccessMessageUrl(messageId); } else { type.incFailCount(count); name.incFailCount(count); type.setFailMessageUrl(messageId); name.setFailMessageUrl(messageId); } type.setFailPercent(type.getFailCount() * 100.0 / type.getTotalCount()); name.setFailPercent(name.getFailCount() * 100.0 / name.getTotalCount()); processEventGrpah(name, event, count); } private void processEventGrpah(EventName name, Event t, int count) { long current = t.getTimestamp() / 1000 / 60; int min = (int) (current % (60)); Range range = name.findOrCreateRange(min); range.incCount(count); if (!t.isSuccess()) { range.incFails(count); } } }
MetricAnalyzer - 业务分析
Metric主要监控业务系统,业务指标,在讲Metric业务报表之前,我们首先讲一下产品线的概念,先看下面类图:
metricProductLine-业务监控是需要配置产品线的,产品线可以认为是一系列project的集合,我们之前说过,每个domain可以认为是一个project,所以产品线也可以认为由多个domain组成,metric产品线的配置文件为 metricProductLine.xml,默认配置如下,最外面是一个company,company下面可以有多条产品线,每条产品线下面又有多个domain。
<?xml version="1.0" encoding="utf-8"?> <company> <product-line id="Platform" order="10.0" title="架构" > <domain id="Cat"/> <domain id="PumaServer"/> <domain id="SessionService"/> </product-line> </company>
每个domain对应哪条产品线是由产品线配置管理类(ProductLineConfigManager)维护的,产品线配置管理类通过一个Map存储domain id 与 product-line id的映射关系,这些映射关系在产品线配置管理类初始化的时候被创建,通过buildMetricProductLines函数。
除此之外,在初始化函数中,ProductLineConfigManager类还会根据配置初始化ProductLineConfig中指定的其它6中监控类型的产品线,分别是userProductLine-外部监控,applicationProductLine-应用监控,networkProductLine-网络监控,systemProductLine-系统监控,databaseProductLine-数据库监控,cdnProductLine-CDN监控,我们可以在上一章的配置列表中找到这些监控类别的配置。
public class ProductLineConfigManager implements Initializable, LogEnabled { private volatile Map<String, String> m_metricProductLines = new HashMap<String, String>(); private Map<String, String> buildMetricProductLines() { Map<String, String> domainToProductLines = new HashMap<String, String>(); for (ProductLine product : ProductLineConfig.METRIC.getCompany().getProductLines().values()) { for (Domain domain : product.getDomains().values()) { domainToProductLines.put(domain.getId(), product.getId()); } } return domainToProductLines; } @Override public void initialize() throws InitializationException { for (ProductLineConfig productLine : ProductLineConfig.values()) { initializeConfig(productLine); } m_metricProductLines = buildMetricProductLines(); } }
接下来我们看看MetricAnalyzer做了什么事情,他会根据domain获取消息所属的产品线,然后生成对应的业务报表(MetricReport) ,也就是说,一条产品线对应一个业务报表,一个业务报表,一共包含3个维度的统计,总条数、总额度、平均数,我们来看一个客户端的案例。
MessageProducer cat = Cat.getProducer(); Transaction t = cat.newTransaction("URL", "WebPage"); cat.logMetric("payCount", "C", "1"); cat.logMetric("totalfee", "S", "30.5"); cat.logMetric("avgfee", "T", "25.6"); cat.logMetric("order", "S,C", "3,25.6"); Metric event = Cat.getProducer().newMetric("kingsoft", "praise"); event.setStatus("C"); event.addData("3"); event.complete(); ...
status = "C" 表示总条数, 默认加1, "S"表示总额度,"T"表示平均数,"S,C"表示总条数+总金额,logMetric函数的第一个参数 payCount/totalfee/avgfee/order 都是表示的业务名,大家可以看到这里并没有上报type类型,实际上type表示的就是统计到哪条产品线下面,但是我们cat客户端默认会上送domain字段,MetricAnalyzer可以通过根据domain找到对应的产品线,生成相应报表。
然而,我们也可以通过添加type类型对指定产品线做统计,比如案例中的 newMetric("kingsoft", "praise") 调用,我们就指定向 kingsoft 产品线统计,这时候MetricAnalyzer会调用 insertIfNotExsit 函数匹配所有监控类型的 product-line和domain,找到与客户端上报的type、domain匹配的产品线。找到则返回,没有找到的话,会为此新建一条产品线,放入到Metric监控类型中,并更新到数据库。然后为新的产品线创建一个report报表。
public class MetricAnalyzer extends AbstractMessageAnalyzer<MetricReport> implements LogEnabled { private int processMetric(MetricReport report, MessageTree tree, Metric metric) { String group = metric.getType(); String metricName = metric.getName(); String domain = tree.getDomain(); String data = (String) metric.getData(); String status = metric.getStatus(); ConfigItem config = parseValue(status, data); if (StringUtils.isNotEmpty(group)) { boolean result = m_productLineConfigManager.insertIfNotExsit(group, domain); if (!result) { m_logger.error(String.format("error when insert product line info, productline %s, domain %s", group, domain)); } report = findOrCreateReport(group); } if (config != null && report != null) { ... } return 0; } private ConfigItem parseValue(String status, String data) { ConfigItem config = new ConfigItem(); if ("C".equals(status)) { ... } else if ("T".equals(status)) { ... } else if ("S".equals(status)) { ... } else if ("S,C".equals(status)) { ... } else { return null; } return config; } }
接下来我们看下具体报表统计逻辑。
我们知道,每个Metric报表都是一条产品线数据的集合,具体的统计信息记录在 MetricItem 中,domain+METRIC+metricName 用以唯一标识一个MetricItem,MetricItem 的 m_type 字段用于区别统计的维度,所以,我们可以推出, 同一个 domain+metricName 只能用于统计一个维度的数据,Metric报表统计的最小粒度是分钟,MetricItem下每个Segment存储的是每分钟的统计信息。
public class MetricAnalyzer extends AbstractMessageAnalyzer<MetricReport> implements LogEnabled { private int processMetric(MetricReport report, MessageTree tree, Metric metric) { ... ConfigItem config = parseValue(status, data); ... if (config != null && report != null) { long current = metric.getTimestamp() / 1000 / 60; int min = (int) (current % (60)); String key = m_configManager.buildMetricKey(domain, METRIC, metricName); MetricItem metricItem = report.findOrCreateMetricItem(key); metricItem.addDomain(domain).setType(status); updateMetric(metricItem, min, config.getCount(), config.getValue()); config.setTitle(metricName); ProductLineConfig productLineConfig = m_productLineConfigManager.queryProductLine(report.getProduct()); if (ProductLineConfig.METRIC.equals(productLineConfig)) { boolean result = m_configManager.insertMetricIfNotExist(domain, METRIC, metricName, config); ... } } return 0; } private void updateMetric(MetricItem metricItem, int minute, int count, double sum) { Segment seg = metricItem.findOrCreateSegment(minute); seg.setCount(seg.getCount() + count); seg.setSum(seg.getSum() + sum); seg.setAvg(seg.getSum() / seg.getCount()); } }
报表展示配置
统计完成之后, 我们会利用ConfigItem对象为Metric报表首次初始化展示配置,并更新到数据库config表。
public class MetricConfigManager implements Initializable, LogEnabled { private volatile MetricConfig m_metricConfig; public boolean insertMetricIfNotExist(String domain, String type, String metricKey, ConfigItem item) { String key = buildMetricKey(domain, type, metricKey); MetricItemConfig config = m_metricConfig.findMetricItemConfig(key); if (config != null) { return true; } else { config = new MetricItemConfig(); config.setId(key); config.setDomain(domain); config.setType(type); config.setMetricKey(metricKey); config.setTitle(item.getTitle()); config.setShowAvg(item.isShowAvg()); config.setShowCount(item.isShowCount()); config.setShowSum(item.isShowSum()); m_logger.info("insert metric config info " + config.toString()); return insertMetricItemConfig(config); } } }
ProblemAnalyzer -异常分析
Problem记录整个项目在运行过程中出现的问题,包括一些错误、访问较长的行为。
Problem分析器会通过报表管理器(ReportManager)根据时间和domain,获取对应报表,然后根据IP,找到相应的machine对象,将machine和消息交给问题处理器(ProblemHandler)处理,注意这里的Machine和前面其它报表的Machine不是同一个类。
public class ProblemAnalyzer extends AbstractMessageAnalyzer<ProblemReport> implements LogEnabled, Initializable { public static final String ID = "problem"; @Inject(ID) private ReportManager<ProblemReport> m_reportManager; @Inject private List<ProblemHandler> m_handlers; @Override public void process(MessageTree tree) { String domain = tree.getDomain(); ProblemReport report = m_reportManager.getHourlyReport(getStartTime(), domain, true); report.addIp(tree.getIpAddress()); Machine machine = report.findOrCreateMachine(tree.getIpAddress()); for (ProblemHandler handler : m_handlers) { handler.handle(machine, tree); } } }
CAT默认有DefaultProblemHandler和LongExecutionProblemHandler两个问题处理器,我们可以也定义自己的问题处理器,用于收集我们感兴趣的问题,只需要我们的问题处理器继承自ProblemHandler,并且重写了handle方法,然后在components-cat-consumer.xml中相关模块配置了即可,如下:
<component> <role>com.dianping.cat.analysis.MessageAnalyzer</role> <role-hint>problem</role-hint> <implementation>com.dianping.cat.consumer.problem.ProblemAnalyzer</implementation> <instantiation-strategy>per-lookup</instantiation-strategy> <requirements> ... <requirement> <role>com.dianping.cat.consumer.problem.ProblemHandler</role> <field-name>m_handlers</field-name> <role-hints> <role-hint>default-problem</role-hint> <role-hint>long-execution</role-hint> </role-hints> </requirement> </requirements> </component>
我们先来看看DefaultProblemHandler,他主要用于收集3类Problem,
1、状态不为SUCCESS的事务消息
2、components-cat-consumer.xml中指定errorType类型的Event消息,如下Error、RuntimeException、Exception三种类型消息。
3、heartbeat异常。
<component> <role>com.dianping.cat.consumer.problem.ProblemHandler</role> <role-hint>default-problem</role-hint> <implementation>com.dianping.cat.consumer.problem.DefaultProblemHandler</implementation> <configuration> <errorType>Error,RuntimeException,Exception</errorType> </configuration> ... </component>
接下来我们看下DefaultProblemHandler处理之后生成的Problem报表的组成结构。
不同IP的Problem消息存储于不同的Machine里面,而且我们还会为不同消息类型、消息名称创建相应的Entity存储消息信息,在Entity中,问题以两种方式存储:
一种是按duration存储,这里的duration指事务执行时间所在的阈值,不过DefaultProblemHandler并不关心duration的不同,所以这里duration全部等于0,我们会在超时调用处理器(LongExecutionProblemHandler)中去做讲解。
另外一种是按线程组来存储,线程组内的消息统计最小粒度为分钟,每个分钟数据统计在Segment中,不管是哪种方式,我们存储的都只是消息树的message_id,而且存储的消息总数有限制,默认每分钟最多只能存储60条消息。
public abstract class ProblemHandler { public static final int MAX_LOG_SIZE = 60; public void updateEntity(MessageTree tree, Entity entity, int value) { Duration duration = entity.findOrCreateDuration(value); List<String> messages = duration.getMessages(); duration.incCount(); if (messages.size() < MAX_LOG_SIZE) { messages.add(tree.getMessageId()); } // make problem thread id = thread group name, make report small JavaThread thread = entity.findOrCreateThread(tree.getThreadGroupName()); if (thread.getGroupName() == null) { thread.setGroupName(tree.getThreadGroupName()); } if (thread.getName() == null) { thread.setName(tree.getThreadName()); } Segment segment = thread.findOrCreateSegment(getSegmentByMessage(tree)); List<String> segmentMessages = segment.getMessages(); segment.incCount(); if (segmentMessages.size() < MAX_LOG_SIZE) { segmentMessages.add(tree.getMessageId()); } } }
LongExecutionProblemHandler
超时调用处理器(LongExecutionProblemHandler)用于监控系统中用时比较长的调用,可以是缓存调用、数据库查询,也可以是一次RPC调用、微服务请求、还可以是一次HTTP请求。超时调用处理器分析的对象仅仅是 Transaction事务类型消息。
public class LongExecutionProblemHandler extends ProblemHandler implements Initializable { private void processTransaction(Machine machine, Transaction transaction, MessageTree tree) { String type = transaction.getType(); if (type.startsWith("Cache.")) { processLongCache(machine, transaction, tree); } else if (type.equals("SQL")) { processLongSql(machine, transaction, tree); } else if (m_configManager.isRpcClient(type)) { processLongCall(machine, transaction, tree); } else if (m_configManager.isRpcServer(type)) { processLongService(machine, transaction, tree); } else if ("URL".equals(type)) { processLongUrl(machine, transaction, tree); } List<Message> messageList = transaction.getChildren(); for (Message message : messageList) { if (message instanceof Transaction) { processTransaction(machine, (Transaction) message, tree); } } } }
超时调用处理器(LongExecutionProblemHandler)会计算事务执行时间,首先查看是否超过了默认阈值设置,每种调用的阈值设置都不同,分4-6个级别,如下,
m_defaultLongServiceDuration = { 50, 100, 500, 1000, 3000, 5000 }
m_defaultLongSqlDuration = { 100, 500, 1000, 3000, 5000 }
m_defaultLongUrlDuration = { 1000, 2000, 3000, 5000 }
m_defalutLongCallDuration = { 100, 500, 1000, 3000, 5000 }
m_defaultLongCacheDuration = { 10, 50, 100, 500 }
超时的事务消息将会被存储到指定machine的entity中,逻辑与DefaultProblemHandler相同,只不过,这里的duration就有了实际意义,问题处理器会根据事务所超出的阈值范围来存储到对应的Duration对象里面去。
自定义自己的问题处理器
TransactionAnalyzer - 事务分析
事务分析器会统计事务(Transaction)类型消息的运行时间,次数,错误次数,当然不是所有Transactionx消息都会被统计,Cache.web、ABTest以及serverFilter配置指定需要过滤的事务消息,会在分析器处理时被丢弃。
统计结果存于TransactionReport,依然是以周期时间和domain来划分不同的报表。
checkForTruncatedMessage ??
相同的domain下的不同IP对应的统计信息依然是存于不同的Machine对象中,截止目前我们已经看到很多报表都包含有Machine类,但是一定注意他们的Machine类都是不同的,可以在cat-consumer/target/generated-sources/dal-model/com/dianping/cat/consumer/ 目录下去查看这些类。
每台机器下面,不同类型的事务统计信息会存于不同的TransactionType对象里,在管理页面上,我们展开指定Type,会看到该Type下所有Name的统计信息,相同Type下的不同名称的统计信息就是分别存在于不同的TransactionName下面,点开每条记录前面的 [:: show ::], 我们将会看到该周期小时内每分钟的统计信息,每分钟的统计存储在 Type 的 Range2对象、Name的Range对象内,实际上Range2和Range对象的代码结构完全一致,除了类名不同,你可以认为他们就是同一个东西。
Type和Name都会统计总执行次数、失败次数、示例链接、最小时间、最大调用时间、平均值、标准差等等信息,同时分析器会选取最近一条消息作为他的示例链接,将messageId存于m_successMessageUrl或者m_failMessageUrl中。
我们会根据一定规则划分几个执行时间区间,将该区间的事务消息总数统计在 AllDuration 和 Duration 对象中。