Skip to content

一、概述

CAT是由大众点评开源的一款调用链监控系统,基于JAVA开发的。有很多互联网企业在使用,热度非常高。它有一个非常强大和丰富的可视化报表界面,这一点其实对于一款调用链监控系统而来非常的重要。在CAT提供的报表界面中有非常多的功能,几乎能看到你想要的任何维度的报表数据。

特点:聚合报表丰富,中文支持好,国内案例多

1.1 CAT报表介绍

CAT支持如下报表:

报表名称报表内容
Transaction报表一段代码的运行时间、次数、比如URL/cache/sql执行次数相应时间
Event报表一段代码运行次数,比如出现一次异常
Problem报表根据Transaction/Event数据分析出系统可能出现的一次,慢程序
Heartbeat报表JVM状态信息
Business报表业务指标等,用户可以自己定制

二、集成使用

2.1 集成dubbo

添加依赖即可

xml
<dependency>
    <groupId>net.dubboclub</groupId>
    <artifactId>cat-monitor</artifactId>
    <version>0.0.6</version>
</dependency>

2.2 集成mybatis

添加依赖

xml
 <dependency>
    <groupId>com.dianping.cat</groupId>
    <artifactId>cat-client</artifactId>
    <version>3.0.0</version>
</dependency>

集成cat-mybatis插件 该文件来源于cat的官方代码。

java

import com.alibaba.druid.pool.DruidDataSource;
import com.dianping.cat.Cat;
import com.dianping.cat.message.Message;
import com.dianping.cat.message.Transaction;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;

import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 *  1.Cat-Mybatis plugin:  Rewrite on the version of Steven;
 *  2.Support DruidDataSource,PooledDataSource(mybatis Self-contained data source);
 * @author zhanzehui(west_20@163.com)
 */

@Intercepts({
        @Signature(method = "query", type = Executor.class, args = {
                MappedStatement.class, Object.class, RowBounds.class,
                ResultHandler.class }),
        @Signature(method = "update", type = Executor.class, args = { MappedStatement.class, Object.class })
})
public class CatMybatisPlugin implements Interceptor {

    private static final Pattern PARAMETER_PATTERN = Pattern.compile("\\?");
    private static final String MYSQL_DEFAULT_URL = "jdbc:mysql://UUUUUKnown:3306/%s?useUnicode=true";
    private Executor target;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = this.getStatement(invocation);
        String          methodName      = this.getMethodName(mappedStatement);
        Transaction t = Cat.newTransaction("SQL", methodName);

        String sql = this.getSql(invocation,mappedStatement);
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        Cat.logEvent("SQL.Method", sqlCommandType.name().toLowerCase(), Message.SUCCESS, sql);

        String url = this.getSQLDatabaseUrlByStatement(mappedStatement);
        Cat.logEvent("SQL.Database", url);

        return doFinish(invocation,t);
    }

    private MappedStatement getStatement(Invocation invocation) {
        return (MappedStatement)invocation.getArgs()[0];
    }

    private String getMethodName(MappedStatement mappedStatement) {
        String[] strArr = mappedStatement.getId().split("\\.");
        String methodName = strArr[strArr.length - 2] + "." + strArr[strArr.length - 1];

        return methodName;
    }

    private String getSql(Invocation invocation, MappedStatement mappedStatement) {
        Object parameter = null;
        if(invocation.getArgs().length > 1){
            parameter = invocation.getArgs()[1];
        }

        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Configuration configuration = mappedStatement.getConfiguration();
        String sql = sqlResolve(configuration, boundSql);

        return sql;
    }

    private Object doFinish(Invocation invocation,Transaction t) throws InvocationTargetException, IllegalAccessException {
        Object returnObj = null;
        try {
            returnObj = invocation.proceed();
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
            Cat.logError(e);
            throw e;
        } finally {
            t.complete();
        }

        return returnObj;
    }


    private String getSQLDatabaseUrlByStatement(MappedStatement mappedStatement) {
        String url = null;
        DataSource dataSource = null;
        try {
            Configuration configuration = mappedStatement.getConfiguration();
            Environment environment = configuration.getEnvironment();
            dataSource = environment.getDataSource();

            url = switchDataSource(dataSource);

            return url;
        } catch (NoSuchFieldException|IllegalAccessException|NullPointerException e) {
            Cat.logError(e);
        }

        Cat.logError(new Exception("UnSupport type of DataSource : "+dataSource.getClass().toString()));
        return MYSQL_DEFAULT_URL;
    }

    private String switchDataSource(DataSource dataSource) throws NoSuchFieldException, IllegalAccessException {
        String url = null;

        if(dataSource instanceof DruidDataSource) {
            url = ((DruidDataSource) dataSource).getUrl();
        }else if(dataSource instanceof PooledDataSource) {
            Field dataSource1 = dataSource.getClass().getDeclaredField("dataSource");
            dataSource1.setAccessible(true);
            UnpooledDataSource dataSource2 = (UnpooledDataSource)dataSource1.get(dataSource);
            url =dataSource2.getUrl();
        }else {
            //other dataSource expand
        }

        return url;
    }

    public String sqlResolve(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(resolveParameterValue(parameterObject)));

            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                Matcher matcher = PARAMETER_PATTERN.matcher(sql);
                StringBuffer sqlBuffer = new StringBuffer();
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    Object obj = null;
                    if (metaObject.hasGetter(propertyName)) {
                        obj = metaObject.getValue(propertyName);
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        obj = boundSql.getAdditionalParameter(propertyName);
                    }
                    if (matcher.find()) {
                        matcher.appendReplacement(sqlBuffer, Matcher.quoteReplacement(resolveParameterValue(obj)));
                    }
                }
                matcher.appendTail(sqlBuffer);
                sql = sqlBuffer.toString();
            }
        }
        return sql;
    }

    private String resolveParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format((Date) obj) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }

        }
        return value;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            this.target = (Executor) target;
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
    }

}

2.3 集成日志框架

CAT集成日志框架以logback日志框架为例:

添加依赖

xml
<dependency>
    <groupId>com.dianping.cat</groupId>
    <artifactId>cat-client</artifactId>
    <version>3.0.0</version>
</dependency>

创建CatLogbackAppender类,

java

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.LogbackException;
import com.dianping.cat.Cat;

import java.io.PrintWriter;
import java.io.StringWriter;

public class CatLogbackAppender extends AppenderBase<ILoggingEvent> {

	@Override
	protected void append(ILoggingEvent event) {
		try {
			boolean isTraceMode = Cat.getManager().isTraceMode();
			Level level = event.getLevel();
			if (level.isGreaterOrEqual(Level.ERROR)) {
				logError(event);
			} else if (isTraceMode) {
				logTrace(event);
			}
		} catch (Exception ex) {
			throw new LogbackException(event.getFormattedMessage(), ex);
		}
	}

	private void logError(ILoggingEvent event) {
		ThrowableProxy info = (ThrowableProxy) event.getThrowableProxy();
		if (info != null) {
			Throwable exception = info.getThrowable();

			Object message = event.getFormattedMessage();
			if (message != null) {
				Cat.logError(String.valueOf(message), exception);
			} else {
				Cat.logError(exception);
			}
		}
	}

	private void logTrace(ILoggingEvent event) {
		String type = "Logback";
		String name = event.getLevel().toString();
		Object message = event.getFormattedMessage();
		String data;
		if (message instanceof Throwable) {
			data = buildExceptionStack((Throwable) message);
		} else {
			data = event.getFormattedMessage().toString();
		}

		ThrowableProxy info = (ThrowableProxy) event.getThrowableProxy();
		if (info != null) {
			data = data + '\n' + buildExceptionStack(info.getThrowable());
		}

		Cat.logTrace(type, name, "0", data);
	}

	private String buildExceptionStack(Throwable exception) {
		if (exception != null) {
			StringWriter writer = new StringWriter(2048);
			exception.printStackTrace(new PrintWriter(writer));
			return writer.toString();
		} else {
			return "";
		}
	}

}

修改logback-spring.xml配置文件:

xml
    <appender name="CatAppender" class="com.itcast.logbackcat.cat.CatLogbackAppender"></appender>
    <root level="info">
        <appender-ref ref="consoleLog"/>
        <appender-ref ref="fileInfoLog"/>
        <appender-ref ref="fileErrorLog"/>
        <appender-ref ref="CatAppender" />
    </root>
</configuration>

2.4 集成springboot

添加如下配置类到config包中

java

import com.dianping.cat.servlet.CatFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CatFilterConfigure {

    @Bean
    public FilterRegistrationBean catFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        CatFilter filter = new CatFilter();
        registration.setFilter(filter);
        registration.addUrlPatterns("/*");
        registration.setName("cat-filter");
        registration.setOrder(1);
        return registration;
    }
}

2.5 Spring AOP拓展

使用Spring AOP技术可以简化我们的埋点操作,通过添加统一注解的方式,使得指定方法被能被CAT监控起来。

添加依赖

xml
<dependency>
    <groupId>com.dianping.cat</groupId>
    <artifactId>cat-client</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建AOP处理

java
package com.itcast.springaopcat.aop;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(ElementType.METHOD)
public @interface CatAnnotation {
}
java
package com.itcast.springaopcat.aop;

import java.lang.reflect.Method;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

import com.dianping.cat.Cat;
import com.dianping.cat.message.Transaction;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class CatAopService {

	@Around(value = "@annotation(CatAnnotation)")
	public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
		MethodSignature joinPointObject = (MethodSignature) pjp.getSignature();
		Method method = joinPointObject.getMethod();

		Transaction t = Cat.newTransaction("method", method.getName());

		try {
			Object res = pjp.proceed();
			t.setSuccessStatus();
			return res;
		} catch (Throwable e) {
			t.setStatus(e);
			Cat.logError(e);
			throw e;
		} finally {
			t.complete();
		}

	}

}

2.6 集成Spring MVC

Spring MVC的集成方式,官方提供的是使用AOP来进行集成,源码如下:

AOP接口

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CatTransaction {
    String type() default "Handler";//"URL MVC Service SQL" is reserved for Cat Transaction Type
    String name() default "";
}

AOP处理代码:

 @Around("@annotation(catTransaction)")
    public Object catTransactionProcess(ProceedingJoinPoint pjp, CatTransaction catTransaction) throws Throwable {
        String transName = pjp.getSignature().getDeclaringType().getSimpleName() + "." + pjp.getSignature().getName();
        if(StringUtils.isNotBlank(catTransaction.name())){
            transName = catTransaction.name();
        }
        Transaction t = Cat.newTransaction(catTransaction.type(), transName);
        try {
            Object result = pjp.proceed();
            t.setStatus(Transaction.SUCCESS);
            return result;
        } catch (Throwable e) {
            t.setStatus(e);
            throw e;
        }finally{
            t.complete();
        }
    }

因这部分与Spring AOP处理方式基本你一样,如需集成请详见Spring AOP。

三、API介绍

3.1 Transaction

Transaction 适合记录跨越系统边界的程序访问行为,比如远程调用,数据库调用,也适合执行时间较长的业务逻辑监控,Transaction用来记录一段代码的执行时间和次数。

手动编写一个本地方法,来测试Transaction的用法,创建TransactionController用于测试。

java

import com.dianping.cat.Cat;
import com.dianping.cat.message.Transaction;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/transaction")
public class TransactionController {

    @RequestMapping("/test")
    public String test(){
        //开启第一个Transaction,类别为URL,名称为test
        Transaction t = Cat.newTransaction("URL", "test");

        try {
            dubbo();
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
            t.setStatus(e);
            Cat.logError(e);
        } finally {
            t.complete();
        }

        return "test";
    }


    private String dubbo(){
        //开启第二个Transaction,类别为DUBBO,名称为dubbo
        Transaction t = Cat.newTransaction("DUBBO", "dubbo");

        try {
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
            t.setStatus(e);
            Cat.logError(e);
        } finally {
            t.complete();
        }

        return "test";
    }
}

上面的代码中,开启了两个Transaction,其中第一个Transaction为Controller接收到的接口调用,第二个编写的本地方法dubbo用来模拟远程调用。在方法内部,开启第二个Transaction。

扩展API

CAT提供了一系列 API 来对 Transaction 进行修改。

方法名作用
addData添加额外的数据显示
setStatus设置状态,成功可以设置SUCCESS,失败可以设置异常
setDurationInMillis设置执行耗时(毫秒)
setTimestamp设置执行时间
complete结束Transaction

编写如下代码进行测试:

java
  @RequestMapping("/api")
    public String api(){
        Transaction t = Cat.newTransaction("URL", "pageName");

        try {
            //设置执行时间1秒
            t.setDurationInMillis(1000);
            t.setTimestamp(System.currentTimeMillis());
            //添加额外数据
            t.addData("content");
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
            t.setStatus(e);
            Cat.logError(e);
        } finally {
            t.complete();
        }

        return "api";
    }

3.2 Event

Event 用来记录一件事发生的次数,比如记录系统异常,它和transaction相比缺少了时间的统计,开销比transaction要小。

Cat.logEvent

记录一个事件。

java
Cat.logEvent("URL.Server", "serverIp", Event.SUCCESS, "ip=${serverIp}");

Cat.logError

记录一个带有错误堆栈信息的 Error。

Error 是一种特殊的事件,它的 type 取决于传入的 Throwable e.

  1. 如果 e 是一个 Error, type 会被设置为 Error
  2. 如果 e 是一个 RuntimeException, type 会被设置为 RuntimeException
  3. 其他情况下,type 会被设置为 Exception

同时错误堆栈信息会被收集并写入 data 属性中。

java
try {
    int i = 1 / 0;
} catch (Throwable e) {
    Cat.logError(e);
}

你可以向错误堆栈顶部添加你自己的错误消息,如下代码所示:

java
Cat.logError("error(X) := exception(X)", e);

编写案例测试上述API:

java

import com.dianping.cat.Cat;
import com.dianping.cat.message.Event;
import com.dianping.cat.message.Transaction;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
    @RequestMapping("/event")
public class EventController {

    @RequestMapping("/logEvent")
    public String logEvent(){
        Cat.logEvent("URL.Server", "serverIp",
                Event.SUCCESS, "ip=127.0.0.1");
        return "test";
    }

    @RequestMapping("/logError")
    public String logError(){
        try {
            int i = 1 / 0;
        } catch (Throwable e) {
            Cat.logError("error(X) := exception(X)", e);
        }
        return "test";
    }

}

3.3 Metric

Metric 用于记录业务指标、指标可能包含对一个指标记录次数、记录平均值、记录总和,业务指标最低统计粒度为1分钟。

java
# Counter


<NolebasePageProperties />




Cat.logMetricForCount("metric.key");
Cat.logMetricForCount("metric.key", 3);

# Duration
Cat.logMetricForDuration("metric.key", 5);

每秒会聚合 metric。

举例来说,如果在同一秒调用 count 三次(相同的 name),累加他们的值,并且一次性上报给服务端。

duration 的情况下,用平均值来取代累加值。

编写案例测试上述API:

java

import com.dianping.cat.Cat;
import com.dianping.cat.message.Event;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/metric")
public class MetricController {

    @RequestMapping("/count")
    public String count(){
        Cat.logMetricForCount("count");
        return "test";
    }

    @RequestMapping("/duration")
    public String duration(){
        Cat.logMetricForDuration("duration", 1000);
        return "test";
    }
}

四、CAT监控界面

4.1 DashBoard

DashBoard仪表盘显示了每分钟出现错误的系统及其错误的次数和时间。

  • 点击右上角的时间按钮可以切换不同的展示时间,-7d代表7天前,-1h代表1小时前,now定位到当前时间
  • 上方的时间轴按照分钟进行排布,点击之后可以看到该时间到结束的异常情况
  • 下方标识了出错的系统和出错的时间、次数,点击系统名称可以跳转到Problem报表

4.2 Transaction

Transaction报表用来监控一段代码运行情况:运行次数、QPS、错误次数、失败率、响应时间统计(平均影响时间、Tp分位值)等等

应用启动后默认会打点的部分:

打点来源组件描述
Systemcat-client上报监控数据的打点信息
URL需要接入cat-filterURL访问的打点信息

小时报表

Type统计界面展示了一个Transaction的第一层分类的视图,可以知道这段时间里面一个分类运行的次数,平均响应时间,延迟,以及分位线。

从上而下分析报表:

  1. 报表的时间跨度 CAT默认是以一小时为统计时间跨度,点击[切到历史模式],更改查看报表的时间跨度:默认是小时模式;切换为历史模式后,右侧快速导航,变为month(月报表)、week(周报表)、day(天报表),可以点击进行查看,注意报表的时间跨度会有所不同。

  2. 时间选择 通过右上角时间导航栏选择时间:点击[+1h]/[-1h]切换时间为下一小时/上一小时;点击[+1d]/[-1d]切换时间为后一天的同一小时/前一天的同一小时;点击右上角[+7d]/[-7d]切换时间为后一周的同一小时/前一周的同一小时;点击[now]回到当前小时。

  3. 项目选择 输入项目名,查看项目数据;如果需要切换其他项目数据,输入项目名,回车即可。

  4. 机器分组 CAT可以将若干个机器,作为一个分组进行数据统计。默认会有一个All分组,代表所有机器的统计数据,即集群统计数据。

  5. 所有Type汇总表格 第一层分类(Type),点击查看第二级分类(称为name)数据

    • Transaction的埋点的Type和Name由业务自己定义,当打点了Cat.newTransaction(type, name)时,第一层分类是type,第二级分类是name。

    • 第二级分类数据叫是统计相同type下的所有name数据,数据均与第一级(type)一样的展示风格

  6. 单个Type指标图表 点击show,查看Type所有name分钟级统计,

  7. 指标说明 显示的是小时粒度第一级分类(type)的次数、错误数、失败率等数据。

  8. 样本logview L代表logview,为一个样例的调用链路。

  9. 分位线说明 小时粒度的时间第一级分类(type)相关统计

    • 95line表示95%的请求的响应时间比参考值要小,999line表示99.9%的响应时间比参考值要小,95line以及99line,也称之为tp95、tp99。

历史报表

Transaction历史报表支持每天、每周、每月的数据统计以及趋势图,点击导航栏的切换历史模式进行查询。Transaction历史报表以响应时间、访问量、错误量三个维度进行展示,以天报表为例:选取一个type,点击show,即可查看天报表。

4.3 Event

Event报表监控一段代码运行次数:例如记录程序中一个事件记录了多少次,错误了多少次。Event报表的整体结构与Transaction报表几乎一样,只缺少响应时间的统计。

第一级分类(Type)统计界面

Type统计界面展示了一个Event的第一层分类的视图,Event相对于Transaction少了运行时间统计。可以知道这段时间里面一个分类运行的次数,失败次数,失败率,采样logView,QPS。

第二级分类(Name)统计界面

第二级分类在Type统计界面中点击具体的Type进入,展示的是相同type下所有的name数据,可以理解为某type下更细化的分类。

4.4 Problem

Problem记录整个项目在运行过程中出现的问题,包括一些异常、错误、访问较长的行为。Problem报表是由logview存在的特征整合而成,方便用户定位问题。 来源:

  1. 业务代码显示调用Cat.logError(e) API进行埋点,具体埋点说明可查看埋点文档。
  2. 与LOG框架集成,会捕获log日志中有异常堆栈的exception日志。
  3. long-url,表示Transaction打点URL的慢请求
  4. long-sql,表示Transaction打点SQL的慢请求
  5. long-service,表示Transaction打点Service或者PigeonService的慢请求
  6. long-call,表示Transaction打点Call或者PigeonCall的慢请求
  7. long-cache,表示Transaction打点Cache.开头的慢请求

所有错误汇总报表 第一层分类(Type),代表错误类型,比如error、long-url等;第二级分类(称为Status),对应具体的错误,比如一个异常类名等。

错误数分布 点击type和status的show,分别展示type和status的分钟级错误数分布:

4.5 HeartBeat

Heartbeat报表是CAT客户端,以一分钟为周期,定期向服务端汇报当前运行时候的一些状态。

JVM相关指标

以下所有的指标统计都是1分钟内的值,cat最低统计粒度是一分钟。

JVM GC 相关指标描述
NewGc Count / PS Scavenge Count新生代GC次数
NewGc Time / PS Scavenge Time新生代GC耗时
OldGc Count老年代GC次数
PS MarkSweepTime老年代GC耗时
Heap UsageJava虚拟机堆的使用情况
None Heap UsageJava虚拟机Perm的使用情况
JVM Thread 相关指标描述
Active Thread系统当前活动线程
Daemon Thread系统后台线程
Total Started Thread系统总共开启线程
Started Thread系统每分钟新启动的线程
CAT Started Thread系统中CAT客户端启动线程

可以参考java.lang.management.ThreadInfo的定义

系统指标

System 相关指标描述
System Load Average系统Load详细信息
Memory Free系统memoryFree情况
FreePhysicalMemory物理内存剩余空间
/ Free/根的使用情况
/data Free/data盘的使用情况

4.6 Business

Business报表对应着业务指标,比如订单指标。与Transaction、Event、Problem不同,Business更偏向于宏观上的指标,另外三者偏向于微观代码的执行情况。

场景示例:

1. 我想监控订单数量。
2. 我想监控订单耗时。

基线

基线是对业务指标的预测值。

基线生成算法:

最近一个月的4个每周几的数据加权求和平均计算得出,秉着更加信任新数据的原则,cat会基于历史数据做异常点的修正,会把一些明显高于以及低于平均值的点剔除。

举例:今天是2018-10-25(周四),今天整天基线数据的算法是最近四个周四(2018-10-18,2018-10-11,2018-10-04,2018-09-27)的每个分钟数据的加权求和或平均,权重值依次为1,2,3,4。如:当前时间为19:56分设为value,前四周对应的19:56分数据(由远及近)分别为A,B,C,D,则value = (A+2B+3C+4D) / 10。

对于刚上线的应用,第一天没有基线,第二天的基线基线是前一天的数据,以此类推。

如何开启基线:

只有配置了基线告警的指标,才会自动计算基线。如需基线功能,请配置基线告警。

注意事项

  1. 打点尽量用纯英文,不要带一些特殊符号,例如 空格( )、分号(:)、竖线(|)、斜线(/)、逗号(,)、与号(&)、星号(*)、左右尖括号(<>)、以及一些奇奇怪怪的字符
  2. 如果有分隔需求,建议用下划线(_)、中划线(-)、英文点号(.)等
  3. 由于数据库不区分大小写,请尽量统一大小写,并且不要对大小写进行改动
  4. 有可能出现小数:趋势图每个点都代表一分钟的值。假设监控区间是10分钟,且10分钟内总共上报5次,趋势图中该点的值为5%10=0.5

4.7 State

State报表显示了与CAT相关的信息。

五、告警配置

CAT提供给我们完善的告警功能。合理、灵活的监控规则可以帮助更快、更精确的发现业务线上故障。

5.1 告警服务器配置

只有配置为告警服务器的机器,才会执行告警逻辑;只有配置为发送服务器的机器,才会发送告警。

进入功能 全局系统配置-服务端配置,修改服务器类型,对告警服务器增加<property name="alarm-machine" value="true"/>配置、以及<property name="send-machine" value="true"/>配置。

5.2 告警策略

告警策略:配置某种告警类型、某个项目、某个错误级别,对应的告警发送渠道,以及暂停时间。

举例:下述配置示例,说明对于Transaction告警,当告警项目名为demo_project:

  • 当告警级别为error时,发送渠道为邮件、短信、微信,连续告警之间的间隔为5分钟
  • 当告警级别为warning时,发送渠道为邮件、微信,连续告警之间的间隔为10分钟

配置示例

xml
<alert-policy>
	<type id="Transaction">
          <group id="default">
             <level id="error" send="mail,weixin" suspendMinute="5"/>
             <level id="warning" send="mail,weixin" suspendMinute="5"/>
          </group>
          <group id="demo-project">
             <level id="error" send="mail,weixin,sms" suspendMinute="5"/>
             <level id="warning" send="mail,weixin" suspendMinute="10"/>
          </group>
    </type>
</alert-policy>

配置说明:

  • type:告警的类型,可选:Transaction、Event、Business、Heartbeat
  • group id属性:group可以为default,代表默认,即所有项目;也可以为项目名,代表某个项目的策略,此时default策略不会生效
  • level id属性:错误级别,分为warning代表警告、error代表错误
  • level send属性:告警渠道,分为mail-邮箱、weixin-微信、sms-短信
  • level suspendMinute属性:连续告警的暂停时间

5.3 告警接收人

告警接收人,为告警所属项目的联系人:

  • 项目组邮件:项目负责人邮件,或项目组产品线邮件,多个邮箱由英文逗号分割,不要留有空格;作为发送告警邮件、微信的依据
  • 项目组号码:项目负责人手机号;多个号码由英文逗号分隔,不要留有空格;作为发送告警短信的依据

5.4 告警服务端

告警发送中心的配置。(什么是告警发送中心:提供发送短信、邮件、微信功能,且提供Http API的服务)

CAT在生成告警后,调用告警发送中心的Http接口发送告警。CAT自身并不集成告警发送中心,请自己搭建告警发送中心。

配置示例

<sender-config>
   <sender id="mail" url="http://test/" type="post" successCode="200" batchSend="true">
      <par id="type=1500"/>
      <par id="key=title,body"/>
      <par id="re=test@test.com"/>
      <par id="to=${receiver}"/>
      <par id="value=${title},${content}"/>
   </sender>
   <sender id="weixin" url="http://test/" type="post" successCode="success" batchSend="true">
      <par id="domain=${domain}"/>
      <par id="email=${receiver}"/>
      <par id="title=${title}"/>
      <par id="content=${content}"/>
      <par id="type=${type}"/>
   </sender>
   <sender id="sms" url="http://test/" type="post" successCode="200" batchSend="false">
      <par id="jsonm={type:808,mobile:'${receiver}',pair:{body='${content}'}}"/>
   </sender>
</sender-config>

配置说明:

  • sender id属性:告警的类型,可选:mail、sms、weixin
  • sender url属性:告警中心的URL
  • sender batchSend属性:是否支持批量发送告警信息
  • par:告警中心所需的Http参数。${argument}代表构建告警对象时,附带的动态参数;此处需要根据告警发送中心的需求,将动态参数加入到代码AlertEntity中的m_paras

5.5 告警规则

目前CAT的监控规则有五个要素

  • 告警时间段。同一项业务指标在每天不同的时段可能有不同的趋势。设定该项,可让CAT在每天不同的时间段执行不同的监控规则。注意:告警时间段,不是监控数据的时间段,只是告警从这一刻开始进行检查数据

  • 规则组合。在一个时间段中,可能指标触发了多个监控规则中的一个规则就要发出警报,也有可能指标要同时触发了多个监控规则才需要发出警报。

  • 监控规则类型。通过以下六种类型对指标进行监控:最大值、最小值、波动上升百分比、波动下降百分比、总和最大值、总和最小值

  • 监控最近分钟数。设定时间后(单位为分钟),当指标在设定的最近的时间长度内连续触发了监控规则,才会发出警报。比如最近分钟数为3,表明连续三分钟的数组都满足条件才告警。如果分钟数为1,表示最近的一分钟满足条件就告警

  • 规则与被监控指标的匹配。监控规则可以按照名称、正则表达式与监控的对象(指标)进行匹配

子条件类型:

有六种类型。子条件的内容为对应的阈值,请注意阈值只能由数字组成,当阈值表达百分比时,不能在最后加上百分号。八种类型如下:

类型说明
MaxVal 最大值(当前值)当前实际值 最大值,比如检查最近3分钟数据,3分钟数据会有3个value,是表示(>=N)个值都必须同时>=设定值
MinVal 最小值(当前值)当前实际值 最小值,比如检查最近3分钟数据,3分钟数据会有3个value,是表示(>=N)个值都必须同时比<=设定值
FluAscPer 波动上升百分比(当前值)波动百分比最大值。即当前最后(N)分钟值比监控周期内其它分钟值(M-N个)的增加百分比都>=设定的百分比时触发警报,比如检查最近10分钟数据,触发个数为3;10分钟内数据会算出7个百分比数据,是表示最后3分钟值分别相比前面7分钟值,3组7次比较的上升波动百分比全部>=配置阈值。比如下降50%,阈值填写50。
FluDescPer 波动下降百分比(当前值)波动百分比最小值。当前最后(N)分钟值比监控周期内其它(M-N个)分钟值的减少百分比都大于设定的百分比时触发警报,比如检查最近10分钟数据,触发个数为3;10分钟数据会算出7个百分比数据,是表示最后3分钟值分别相比前面7分钟值,3组7次比较的下降波动百分比全部>=配置阈值。比如下降50%,阈值填写50。
SumMaxVal 总和最大值(当前值)当前值总和最大值,比如检查最近3分钟数据,表示3分钟内的总和>=设定值就告警。
SumMinVal 总和最小值(当前值)当前值总和最小值,比如检查最近3分钟数据,表示3分钟内的总和<=设定值就告警。

5.6 Transaction告警

对Transaction的告警,支持的指标有次数、延时、失败率;监控周期:一分钟

配置说明:

  • 项目名:要监控的项目名
  • type:被监控transaction的type
  • name:被监控transaction的name;如果为All,代表全部name
  • 监控指标:次数、延时、失败率
  • 告警规则:详情见告警规则部分

5.7 Event告警

对Event的个数进行告警;监控周期:一分钟

配置说明:

  • 项目名:要监控的项目名
  • type:被监控event的type
  • name:被监控event的name;如果为All,代表全部name
  • 告警规则:详情见告警规则部分

5.8 心跳告警

心跳告警是对服务器当前状态的监控,如监控系统负载、GC数量等信息;监控周期:一分钟

配置说明:

  • 项目名:要监控的项目名
  • 指标:被监控的心跳指标名称;心跳告警是由两级匹配的:首先匹配项目,然后按照指标匹配
  • 告警规则:详情见告警规则部分

5.9 异常告警

对异常的个数进行告警;监控周期:一分钟

配置说明:

  • 项目名:要监控的项目名
  • 异常名称:被监控异常名称;当设置为“Total”时,是针对当前项目组所有异常总数阈值进行设置;当设置为特定异常名称时,针对当前项目组所有同名的异常阈值进行设定
  • warning阈值:到达该阈值,发送warning级别告警;当异常数小于该阈值时,不做任何警报
  • error阈值:到达该阈值,发送error级别告警
  • 总数大于Warning阈值,小于Error阈值,进行Warning级别告警;大于Error阈值,进行Error级别告警

5.10 告警接口编写

编写controller接口:

java
package com.itcast.springbootcat;

import com.dianping.cat.Cat;
import com.dianping.cat.message.Event;
import com.dianping.cat.message.Transaction;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

@RestController
public class AlertController {

    @RequestMapping(value = "/alert/msg")
    public String sendAlert(@RequestParam String to) {
        System.out.println("告警了" +to);
        return "200";
    }
}

修改告警服务端的配置,填写接口地址,以邮件为例:

配置示例

xml
 <sender id="mail" url="http://localhost:8085/alert/msg" type="post" successCode="200" batchSend="true">
      <par id="type=1500"/>
      <par id="key=title,body"/>
      <par id="re=test@test.com"/>
      <par id="to=${receiver}"/>
      <par id="value=${title},${content}"/>
   </sender>

测试结果,输出内容如下:

告警了testUser1@test.com,testUser2@test.com

六、安装

6.1 常规安装

从github上下载最新版本的源码。地址

模块介绍

  • cat-client: 客户端,上报监控数据
  • cat-consumer: 服务端,收集监控数据进行统计分析,构建丰富的统计报表
  • cat-alarm: 实时告警,提供报表指标的监控告警
  • cat-hadoop: 数据存储,logview 存储至 Hdfs
  • cat-home: 管理端,报表展示、配置管理等

CAT服务端的环境要求如下:

  • Linux 2.6以及之上(2.6内核才可以支持epoll),线上服务端部署请使用Linux环境,Mac以及Windows环境可以作为开发环境,美团点评内部CentOS 6.5
  • Java 6,7,8,服务端推荐使用jdk7的版本,客户端jdk6、7、8都支持
  • Maven 3及以上
  • MySQL 5.6,5.7,更高版本MySQL都不建议使用,不清楚兼容性
  • J2EE容器建议使用tomcat,建议使用推荐版本7.*.或8.0.
  • Hadoop环境可选,一般建议规模较小的公司直接使用磁盘模式,可以申请CAT服务端,500GB磁盘或者更大磁盘,这个磁盘挂载在/data/目录上

数据库脚本执行

  • 数据库的脚本文件 script/CatApplication.sql

    sh
    mysql -uroot -Dcat < CatApplication.sql
  • 说明:

    数据库编码使用utf8mb4,否则可能造成中文乱码等问题

tomcat配置

修改中文乱码 tomcat conf 目录下 server.xml

xml
<Connector port="8080" protocol="HTTP/1.1"
           URIEncoding="utf-8"    connectionTimeout="20000"
               redirectPort="8443" />  <!-- 增加  URIEncoding="utf-8"  -->

应用打包

  • 源码构建

    1. 在cat的源码目录,执行mvn clean install -DskipTests
    2. 如果发现cat的war打包不通过,CAT所需要依赖jar都部署在 http://unidal.org/nexus/
    3. 可以配置这个公有云的仓库地址到本地Maven配置(一般为~/.m2/settings.xml),理论上不需要配置即可,可以参考cat的pom.xml配置:
    xml
    <repositories>
      <repository>
         <id>central</id>
         <name>Maven2 Central Repository</name>
         <layout>default</layout>
         <url>http://repo1.maven.org/maven2</url>
      </repository>
      <repository>
         <id>unidal.releases</id>
         <url>http://unidal.org/nexus/content/repositories/releases/</url>
      </repository>
    </repositories>
  • 官方下载

    1. 如果自行打包仍然问题,请使用下面链接进行下载:

      http://unidal.org/nexus/service/local/repositories/releases/content/com/dianping/cat/cat-home/3.0.0/cat-home-3.0.0.war

    2. 官方的cat的master版本,重命名为cat.war进行部署,注意此war是用jdk8,服务端请使用jdk8版本

安装配置

程序对于/data/目录具体读写权限

  1. 要求/data/目录能进行读写操作,如果/data/目录不能写,建议使用linux的软链接链接到一个固定可写的目录。所有的客户端集成程序的机器以及CAT服务端机器都需要进行这个权限初始化。(可以通过公司运维工具统一处理)

  2. 此目录会存一些CAT必要的配置文件以及运行时候的数据存储目录。

  3. CAT支持CAT_HOME环境变量,可以通过JVM参数修改默认的路径。

    sh
    mkdir /data
    chmod -R 777 /data/

配置/data/appdatas/cat/client.xml ($CAT_HOME/client.xml)

sh
mkdir -p /data/appdatas/cat
cd /data/appdatas/cat
vi client.xml

编写程序运行盘下的/data/appdatas/cat/client.xml,代码如下:

xml
<?xml version="1.0" encoding="utf-8"?>
<config mode="client">
    <servers>
    	<!--下面的IP地址替换为主机的IP地址-->
        <server ip="192.168.1.101" port="2280" http-port="8080"/>
    </servers>
</config>

配置/data/appdatas/cat/datasources.xml($CAT_HOME/datasources.xml)

sh
vi datasources.xml
xml
<?xml version="1.0" encoding="utf-8"?>

<data-sources>
	<data-source id="cat">
		<maximum-pool-size>3</maximum-pool-size>
		<connection-timeout>1s</connection-timeout>
		<idle-timeout>10m</idle-timeout>
		<statement-cache-size>1000</statement-cache-size>
		<properties>
			<driver>com.mysql.jdbc.Driver</driver>
			<url><![CDATA[jdbc:mysql://127.0.0.1:3306/cat]]></url>  <!-- 请替换为真实数据库URL及Port  -->
			<user>root</user>  <!-- 请替换为真实数据库用户名  -->
			<password>root</password>  <!-- 请替换为真实数据库密码  -->
			<connectionProperties><![CDATA[useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&socketTimeout=120000]]></connectionProperties>
		</properties>
	</data-source>
</data-sources>

服务端配置

配置链接:http://{ip:port}/cat/s/config?op=serverConfigUpdate

输入账号密码admin/admin进行登录

以下所有IP地址为127.0.0.1内容,均修改为实际的IP地址!

输入以下内容:

xml
<?xml version="1.0" encoding="utf-8"?>
<server-config>
   <server id="default">
      <properties>
         <property name="local-mode" value="false"/>
         <property name="job-machine" value="false"/>
         <property name="send-machine" value="false"/>
         <property name="alarm-machine" value="false"/>
         <property name="hdfs-enabled" value="false"/>
         <property name="remote-servers" value="127.0.0.1:8080"/>
      </properties>
      <storage local-base-dir="/data/appdatas/cat/bucket/" max-hdfs-storage-time="15" local-report-storage-time="2" local-logivew-storage-time="1" har-mode="true" upload-thread="5">
         <hdfs id="dump" max-size="128M" server-uri="hdfs://127.0.0.1/" base-dir="/user/cat/dump"/>
         <harfs id="dump" max-size="128M" server-uri="har://127.0.0.1/" base-dir="/user/cat/dump"/>
         <properties>
            <property name="hadoop.security.authentication" value="false"/>
            <property name="dfs.namenode.kerberos.principal" value="hadoop/dev80.hadoop@testserver.com"/>
            <property name="dfs.cat.kerberos.principal" value="cat@testserver.com"/>
            <property name="dfs.cat.keytab.file" value="/data/appdatas/cat/cat.keytab"/>
            <property name="java.security.krb5.realm" value="value1"/>
            <property name="java.security.krb5.kdc" value="value2"/>
         </properties>
      </storage>
      <consumer>
         <long-config default-url-threshold="1000" default-sql-threshold="100" default-service-threshold="50">
            <domain name="cat" url-threshold="500" sql-threshold="500"/>
            <domain name="OpenPlatformWeb" url-threshold="100" sql-threshold="500"/>
         </long-config>
      </consumer>
   </server>
   <server id="127.0.0.1">
      <properties>
         <property name="job-machine" value="true"/>
         <property name="send-machine" value="true"/>
         <property name="alarm-machine" value="true"/>
      </properties>
   </server>
</server-config>

配置链接:http://{ip:port}/cat/s/config?op=routerConfigUpdate

xml
<?xml version="1.0" encoding="utf-8"?>
<router-config backup-server="127.0.0.1" backup-server-port="2280">
   <default-server id="127.0.0.1" weight="1.0" port="2280" enable="true"/>
   <network-policy id="default" title="默认" block="false" server-group="default_group">
   </network-policy>
   <server-group id="default_group" title="default-group">
      <group-server id="127.0.0.1"/>
   </server-group>
   <domain id="cat">
      <group id="default">
         <server id="127.0.0.1" port="2280" weight="1.0"/>
      </group>
   </domain>
</router-config>