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>