本文为美团点评基于 Java 开发的一套开源的分布式实时监控系统Cat的入门学习笔记整理,主要参考以下资料:
本文涉及到的代码仓库链接: https://gitee.com/DaHuYuXiXi/cat-demo-project
在微服务架构体系下,微服务系统中各个微服务之间的调用关系链路十分复杂,并且考虑到不同的微服务可能是不同团队进行的开发与维护,因此一旦在某个调用链路节点上出现问题,问题的排查就变得十分困难,困难的原因主要在于我们需要将当前业务的执行原始现场进行还原,从而方便我们对业务执行情况进行分析和问题的定位。
因此调用链路监控是微服务架构体系中非常重要的一环,它除了能帮助我们定位问题以外,还能帮助项目成员清晰的去了解项目部署结构,毕竟一个几十上百的微服务,相信在运行时间久了之后,项目的结构会出现上述非常复杂的调用链,在这种情况下,团队开发者甚至是架构师都不一定能对项目的网络结构有很清晰的了解,那就更别谈系统优化了。
微服务架构下,如果没有一款强大的调用链监控工具,势必会产生如下问题:
由上可知,我们需要一个调用链路监控工具,该工具至少需要帮助我们完成以下几点需求:
在2010年,google发表了一篇名为“Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”的论文,在文中介绍了google生产环境中大规模分布式系统下的跟踪系统Dapper的设计和使用经验。而如今很多的调用链系统如zipkin/pinpoint等系统都是基于这篇文章而实现的。
接下来我们就简单的介绍一下Dapper中调用链监控的原理:
如上图所示,这是一个查询订单的简单业务,他有如下的步骤:
这几个步骤中,有几个核心概念需要了解:
那么回到上面的案例中,查询订单数据和查询商品数据这两个过程,就分别是两个span,我们记为span A和B。B的parent也就是父span就是A。这两个span都拥有同一个Trace Id:1。
并且在信息收集过程中,会记录调用的开始时间,结束时间,从中计算出调用的耗时。
这样,就可以清楚的知道,每笔调用:
CAT是由大众点评开源的一款调用链监控系统,服务端基于JAVA开发的。有很多互联网企业在使用,热度非常高。它有一个非常强大和丰富的可视化报表界面,这一点其实对于一款调用链监控系统而来非常的重要。在CAT提供的报表界面中有非常多的功能,几乎能看到你想要的任何维度的报表数据。
Pinpoint是由一个韩国团队实现并开源,针对Java编写的大规模分布式系统设计,通过JavaAgent的机制做字节代码植入,实现加入traceid和获取性能数据的目的,对应用代码零侵入。
SkyWalking是apache基金会下面的一个开源APM项目,为微服务架构和云原生架构系统设计。它通过探针自动收集所需的指标,并进行分布式追踪。通过这些调用链路以及指标,Skywalking APM会感知应用间关系和服务间关系,并进行相应的指标统计。Skywalking支持链路追踪和监控应用组件基本涵盖主流框架和容器,如国产RPC Dubbo和motan等,国际化的spring boot,spring cloud。
Zipkin是由Twitter开源,是分布式链路调用监控系统,聚合各业务系统调用延迟数据,达到链路调用监控跟踪。Zipkin基于Google的Dapper论文实现,主要完成数据的收集、存储、搜索与界面展示。
本部分内容截取至官方文档报表简介章节,链接:官方文档: 报表介绍篇
CAT支持如下报表:
报表名称 | 报表内容 |
---|---|
Transaction报表 | 一段代码的运行时间、次数、比如URL/cache/sql执行次数相应时间 |
Event报表 | 一段代码运行次数,比如出现一次异常 |
Problem报表 | 根据Transaction/Event数据分析出系统可能出现的一次,慢程序 |
Heartbeat报表 | JVM状态信息 |
Business报表 | 业务指标等,用户可以自己定制 |
#由于仓库的git历史记录众多,对于不关注历史,只关注最新版本或者基于最新版本贡献的新用户,可以在第一次克隆代码时增加--depth=1参数以加快下载速度,如
git clone --depth=1 https://github.com/dianping/cat.git
模块介绍:
服务端安装:
CAT服务端的环境要求如下:
数据库安装:
数据库的脚本文件 script/CatApplication.sql
mysql -uroot -Dcat < CatApplication.sql
说明:数据库编码使用utf8mb4,否则可能造成中文乱码等问题
应用打包:
mvn clean install -DskipTests
, 构建完毕后,生成的war包位于cat-home目录下重命名为cat.war进行部署,注意此war是用jdk8,服务端请使用jdk8版本
本文安装的cat版本为3.0.0,如果版本与本文安装的不一致,可能会存在各种各样的兼容性问题
cat 3.0.0版本对应的cat.war已经在本文开头给出的代码仓库中给出,大家可以拉取工程获取
Liunx源码安装步骤:
注意:
mkdir /data
chmod -R 777 /data/
mkdir -p /data/appdatas/cat
cd /data/appdatas/cat
vi client.xml
修改client.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>
vi datasources.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>
#1.拉取镜像
docker pull tomcat:8.5.40
注意: 将cat-home.war更名为cat.war。
from tomcat:8.5.40
RUN rm -rf /usr/local/tomcat/webapps/*
COPY cat.war /usr/local/tomcat/webapps
RUN mkdir -p /data/appdatas/cat
RUN mkdir -p /data/applogs/cat
RUN chmod -R 777 /data/
COPY client.xml /data/appdatas/cat
COPY datasources.xml /data/appdatas/cat
COPY server.xml /data/appdatas/cat
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
EXPOSE 8080
EXPOSE 2280
docker build -t cat:3.0 .
docker run -di --name=tomcat -p 8080:8080 -p 2280:2280 -v /root/tomcat/webapps:/usr/local/tomcat/webapps -v /root/tomcat/conf:/usr/local/tomcat/conf -v /data:/data tomcat:8
记得将防火墙的8080和2280端口都进行开放
#修改/root/tomcat/conf目录下 server.xml配置文件,防止中文乱码产生
<Connector port="8080" protocol="HTTP/1.1"
URIEncoding="utf-8" connectionTimeout="20000"
redirectPort="8443" /> <!-- 增加 URIEncoding="utf-8" -->
注意: 修改完下面的配置,去掉注释后,直接粘贴到上面给出的页面中即可。
<?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-machine" value="false"/>
#替换为当前cat所在服务器ip,如果cat采用集群部署,这里可以指定各个节点的地址
<property name="remote-servers" value="10.1.1.1:8080,10.1.1.2:8080,10.1.1.3:8080"/>
</properties>
# 没有使用分布式文件系统HDFS,下面这段配置可以不用管
<storage local-base-dir="/data/appdatas/cat/bucket/" max-hdfs-storage-time="15" local-report-storage-time="7" local-logivew-storage-time="7">
<hdfs id="logview" max-size="128M" server-uri="hdfs://10.1.77.86/" base-dir="user/cat/logview"/>
<hdfs id="dump" max-size="128M" server-uri="hdfs://10.1.77.86/" base-dir="user/cat/dump"/>
<hdfs id="remote" max-size="128M" server-uri="hdfs://10.1.77.86/" base-dir="user/cat/remote"/>
</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>
#替换为当前cat所在服务器ip
<server id="10.1.1.1">
<properties>
<property name="job-machine" value="true"/>
<property name="alarm-machine" value="true"/>
<property name="send-machine" value="true"/>
</properties>
</server>
</server-config>
注意: 修改完下面的配置,去掉注释后,直接粘贴到上面给出的页面中即可。
<?xml version="1.0" encoding="utf-8"?>
<router-config backup-server="cat所在服务器IP" backup-server-port="2280">
<default-server id="cat所在服务器IP" 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="cat所在服务器IP"/>
</server-group>
<domain id="cat">
# 对服务器进行分组,如果配置了多个服务器,下面需要指明这多个服务器的地址
<group id="default">
<server id="cat所在服务器IP" port="2280" weight="1.0"/>
</group>
</domain>
</router-config>
<dependency>
<groupId>com.dianping.cat</groupId>
<artifactId>cat-client</artifactId>
<version>3.0.1</version>
</dependency>
src/main/resources/META-INF/app.properties
文件, 并添加如下内容:app.name={appkey}
appkey 只能包含英文字母 (a-z, A-Z)、数字 (0-9)、下划线 (_) 和中划线 (-)
/data/appdatas/cat
目录—>确保你具有这个目录的读写权限。/data/applogs/cat
目录 (可选)—>这个目录是用于存放运行时日志的,这将会对调试提供很大帮助,同样需要读写权限。/data/appdatas/cat/client.xml
,内容如下<?xml version="1.0" encoding="utf-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema" xsi:noNamespaceSchemaLocation="config.xsd">
<servers>
<server ip="127.0.0.1" port="2280" http-port="8080" />
</servers>
</config>
@RestController
public class TestController {
@GetMapping("/test")
public String test() {
Transaction t = Cat.newTransaction("URL", "pageName");
try {
Cat.logEvent("URL.Server", "serverIp", Event.SUCCESS, "ip=${serverIp}");
Cat.logMetricForCount("metric.key");
Cat.logMetricForDuration("metric.key", 5);
//让代码抛出异常
int i = 1 / 0;
t.setStatus(Transaction.SUCCESS);
} catch (Exception e) {
t.setStatus(e);
Cat.logError(e);
} finally {
t.complete();
}
return "test";
}
}
如上图所示,dhy-cat应用已经出现了4次接口调用错误,我们具体查看以下:
查看具体的错误信息:
很显然看出上图所示其实是一个除0异常,到此为止SpringBoot客户端集成Cat就完成了。
Transaction 适合记录跨越系统边界的程序访问行为,比如远程调用,数据库调用,也适合执行时间较长的业务逻辑监控,Transaction用来记录一段代码的执行时间和次数。
现在我们的框架还没有与dubbo、mybatis做集成,所以我们通过手动编写一个本地方法,来测试Transaction的用法,创建TransactionController用于测试。
@GetMapping("/test1")
public String test1(){
//开启第一个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。
启动项目,访问接口http://localhost:9200/test1
点击左侧菜单Transaction报表,选中URL类型对应的Log View查看调用链关系。
如图所示调用链已经形成,可以看到类型为URL的test调用了类型为DUBBO的dubbo方法,分别耗时0.47ms和 1.02ms。
CAT提供了一系列 API 来对 Transaction 进行修改。
编写如下代码进行测试:
@GetMapping("/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";
}
启动项目,访问接口: http://localhost:9200/api
点击左侧菜单Transaction报表,选中URL类型对应的Log View查看调用链关系。
点击左侧菜单Transaction报表,选中URL类型对应的Log View查看调用链关系。
如图所示,调用耗时已经被手动修改成了1000ms,并且添加了额外的信息content。
在使用 Transaction API 时,你可能需要注意以下几点:
addData
多次,添加的数据会被 &
连接起来。Event 用来记录一件事发生的次数,比如记录系统异常,它和transaction相比缺少了时间的统计,开销比transaction要小。
Cat.logEvent("URL.Server", "serverIp", Event.SUCCESS, "ip=${serverIp}");
Error 是一种特殊的事件,它的 type
取决于传入的 Throwable e
:
1. 如果 `e` 是一个 `Error`, `type` 会被设置为 `Error`。
2. 如果 `e` 是一个 `RuntimeException`, `type` 会被设置为 `RuntimeException`。
3. 其他情况下,`type` 会被设置为 `Exception`。
同时错误堆栈信息会被收集并写入 data
属性中。
try {
int i = 1 / 0;
} catch (Throwable e) {
Cat.logError(e);
}
你可以向错误堆栈顶部添加你自己的错误消息,如下代码所示:
Cat.logError("error(X) := exception(X)", e);
编写案例测试上述API:
@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";
}
}
启动项目,访问接口http://localhost:9200/event/logEvent和http://localhost:9200/event/logError。
通过上图可以看到,增加了两个事件:URL.Server和RuntimeException。点开LOG查看。
这里出现了两个Event的详细内容:
1.URL.Server是一个正常的事件,打印出了IP=127.0.0.1的信息。
2.RuntimeException是一个错误Event,不仅打印出了错误堆栈,还将我们打印的
error(X) := exception(X)
内容放到了堆栈的最上方便于查看。
Metric 用于记录业务指标、指标可能包含对一个指标记录次数、记录平均值、记录总和,业务指标最低统计粒度为1分钟。
# Counter
Cat.logMetricForCount("metric.key");
Cat.logMetricForCount("metric.key", 3);
# Duration
Cat.logMetricForDuration("metric.key", 5);
我们每秒会聚合 metric。
举例来说,如果你在同一秒调用 count 三次(相同的 name),累加他们的值,并且一次性上报给服务端。
在 duration
的情况下,用平均值来取代累加值。
编写案例测试上述API:
@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";
}
}
启动项目,访问接口http://localhost:9200/metric/count 点击5次和http://localhost:9200/metric/duration。
通过上图可以看到,count和duration的具体数值。
count一共点击了5次,所以这一分钟内数值为5。而duration不管点击多少次,由于取的是平均值,所以一直是1000。
统计粒度是分钟
DashBoard仪表盘显示了每分钟出现错误的系统及其错误的次数和时间。
Transaction报表用来监控一段代码运行情况:运行次数、QPS、错误次数、失败率、响应时间统计(平均影响时间、Tp分位值)等等
。
打点 | 来源组件 | 描述 |
---|---|---|
System | cat-client | 上报监控数据的打点信息 |
URL | 需要接入cat-filter | URL访问的打点信息 |
从上而下分析报表:
Transaction历史报表支持每天、每周、每月的数据统计以及趋势图,点击导航栏的切换历史模式进行查询。Transaction历史报表以响应时间、访问量、错误量三个维度进行展示,以天报表为例:选取一个type,点击show,即可查看天报表。
Event报表监控一段代码运行次数:例如记录程序中一个事件记录了多少次,错误了多少次
。Event报表的整体结构与Transaction报表几乎一样,只缺少响应时间的统计。
Type统计界面展示了一个Event的第一层分类的视图,Event相对于Transaction少了运行时间统计。可以知道这段时间里面一个分类运行的次数,失败次数,失败率,采样logView,QPS。
第二级分类在Type统计界面中点击具体的Type进入,展示的是相同type下所有的name数据,可以理解为某type下更细化的分类。
Problem记录整个项目在运行过程中出现的问题,包括一些异常、错误、访问较长的行为。Problem报表是由logview存在的特征整合而成,方便用户定位问题。 来源:
所有错误汇总报表 第一层分类(Type),代表错误类型,比如error、long-url等;第二级分类(称为Status),对应具体的错误,比如一个异常类名等。
错误数分布 点击type和status的show,分别展示type和status的分钟级错误数分布:
Heartbeat报表是CAT客户端,以一分钟为周期,定期向服务端汇报当前运行时候的一些状态
以下所有的指标统计都是1分钟内的值,cat最低统计粒度是一分钟。
JVM GC 相关指标 | 描述 |
---|---|
NewGc Count / PS Scavenge Count | 新生代GC次数 |
NewGc Time / PS Scavenge Time | 新生代GC耗时 |
OldGc Count | 老年代GC次数 |
PS MarkSweepTime | 老年代GC耗时 |
Heap Usage | Java虚拟机堆的使用情况 |
None Heap Usage | Java虚拟机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盘的使用情况 |
Business报表对应着业务指标,比如订单指标。与Transaction、Event、Problem不同,Business更偏向于宏观上的指标,另外三者偏向于微观代码的执行情况。
场景示例:
1. 我想监控订单数量。
2. 我想监控订单耗时。
举例:今天是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。
对于刚上线的应用,第一天没有基线,第二天的基线基线是前一天的数据,以此类推。
打点尽量用纯英文,不要带一些特殊符号,例如 空格( )、分号(:)、竖线(|)、斜线(/)、逗号(,)、与号(&)、星号(*)、左右尖括号(<>)、以及一些奇奇怪怪的字符
如果有分隔需求,建议用下划线(_)、中划线(-)、英文点号(.)等
由于数据库不区分大小写,请尽量统一大小写,并且不要对大小写进行改动
有可能出现小数:趋势图每个点都代表一分钟的值。假设监控区间是10分钟,且10分钟内总共上报5次,趋势图中该点的值为5%10=0.5
State报表显示了与CAT相关的信息。
完整代码可以参考本文一开始给出的仓库链接
注意: install前,可以先修改pom文件,将cat-client依赖改为3.0.1
如果出现CatLogger不存在的错误,替换为Cat.logError即可
生产者配置:
<dependency>
<groupId>net.dubboclub</groupId>
<artifactId>cat-monitor</artifactId>
<version>0.0.6</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>net.dubboclub</groupId>
<artifactId>cat-monitor</artifactId>
<version>0.0.6</version>
</dependency>
server.port=9300
spring.application.name=dubbo_provider_cat
# 默认为dubbo协议
dubbo.protocol.name=dubbo
# dubbo协议默认通信端口号为20880
dubbo.protocol.port=20880
# 为了简化环境搭建,采用了本地直接调用的方式,所以将注册中心写成N/A表示不注册到注册中心
dubbo.registry.address=N/A
public interface HelloService {
String hello();
}
@DubboService(interfaceClass = HelloService.class)
public class HelloServiceImpl implements HelloService {
public String hello() {
return "hello cat";
}
}
@EnableDubbo(scanBasePackages = "org.example.dubbo")
@SpringBootApplication
public class CatProviderDemo {
public static void main(String[] args) {
SpringApplication.run(CatProviderDemo.class,args);
}
}
消费者配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>net.dubboclub</groupId>
<artifactId>cat-monitor</artifactId>
<version>0.0.6</version>
</dependency>
server.port=9500
spring.application.name=dubbo_consumer_cat
public interface HelloService {
public String hello();
}
@EnableDubbo(scanBasePackages = "org.example.dubbo")
@SpringBootApplication
public class CatConsumerDemo {
public static void main(String[] args) {
SpringApplication.run(CatConsumerDemo.class,args);
}
}
@SpringBootTest(classes = CatConsumerDemo.class)
public class ConsumerTest {
//采用直连而非从注册中心获取服务地址的方式,在@Reference注解中声明
@DubboReference(url = "dubbo://127.0.0.1:20880")
private HelloService helloService;
@Test
public void test(){
for (int i = 0; i < 1000; i++) {
System.out.println(helloService.hello());
}
}
}
如图所示dubbo的调用已经被正确显示在transaction报表中。点击log view查看详细的调用。
如图所示,调用的日志已经被成功打印。
dubbo插件的日志打印内容显示的并不是十分的良好,如果在企业中应用,可以基于dubbo插件进行二次开发。
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`password` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.27</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.dianping.cat</groupId>
<artifactId>cat-client</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
</dependencies>
# datasource
spring:
datasource:
url: jdbc:mysql://119.91.143.140:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: xfx123xfx
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# mybatis
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml # mapper映射文件路径
type-aliases-package: org.example.dao
server:
port: 9500
@Mapper
public interface UserXmlMapper {
List<User> findAll();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mapper.UserXmlMapper">
<select id="findAll" resultType="user">
select * from t_user
</select>
</mapper>
@SpringBootApplication
public class CatMybaitsDemo {
public static void main(String[] args) {
SpringApplication.run(CatMybaitsDemo.class,args);
}
}
@SpringBootTest(classes = CatMybaitsDemo.class)
public class CatMybaitsDemoTest {
@Resource
private UserXmlMapper userXmlMapper;
@Test
public void testSearchUser() throws InterruptedException {
try {
userXmlMapper.findAll().forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}
Thread.sleep(30000);
}
}
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) {
}
}
将此文件和所有其他cat插件一同打包放到私有仓库上是一种更好的选择。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<plugins>
<plugin interceptor="org.example.cat.CatMybatisPlugin"/>
</plugins>
</configuration>
# mybatis
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml # mapper映射文件路径
type-aliases-package: org.example.dao
# config-location: # 指定mybatis的核心配置文件
config-location: classpath:mybatis/mybatis-config.xml
已经能够显示出有部分语句执行错误,如果要查看具体的错误,点击Log View查看:
图中不止能看到具体的sql语句,也可以看到报错的堆栈信息。
CAT集成日志框架的思路大体上都类似,所以课程中采用Spring Boot默认的logback日志框架来进行讲解,如果使用了log4j、log4j2处理方式也是类似的。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.dianping.cat</groupId>
<artifactId>cat-client</artifactId>
<version>3.0.1</version>
</dependency>
logging:
level:
root: info
path: ./logs
config: classpath:logback-spring.xml
server:
port: 9600
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- 属性文件:在properties文件中找到对应的配置项 -->
<springProperty scope="context" name="logging.path" source="logging.path"/>
<contextName>cat</contextName>
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出(配色):%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%yellow(%d{yyyy-MM-dd HH:mm:ss}) %red([%thread]) %highlight(%-5level) %cyan(%logger{50}) -
%magenta(%msg) %n
</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!--根据日志级别分离日志,分别输出到不同的文件-->
<appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n
</pattern>
<charset>UTF-8</charset>
</encoder>
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--按时间保存日志 修改格式可以按小时、按天、月来保存-->
<fileNamePattern>${logging.path}/cat.info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!--保存时长-->
<MaxHistory>90</MaxHistory>
<!--文件大小-->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
</appender>
<appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--路径-->
<fileNamePattern>${logging.path}/cat.error.%d{yyyy-MM-dd}.log</fileNamePattern>
<MaxHistory>90</MaxHistory>
</rollingPolicy>
</appender>
<root level="info">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileInfoLog"/>
<appender-ref ref="fileErrorLog"/>
</root>
</configuration>
package org.example.appender;
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 "";
}
}
}
<appender name="CatAppender" class="org.example.appender.CatLogbackAppender"/>
<root level="info">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileInfoLog"/>
<appender-ref ref="fileErrorLog"/>
<appender-ref ref="CatAppender" />
</root>
</configuration>
@SpringBootTest(classes = CatDemoMain.class)
@Slf4j
public class CatLogBackDemoTest {
@Test
public void testLog() throws InterruptedException {
//需要开启跟踪模式,才会生效--可以参考CatLogbackAppender的append方法,一看便知
Cat.getManager().setTraceMode(true);
log.info("cat info");
try {
int i = 1/0;
}catch (Exception e){
log.error("cat error",e);
}
//睡眠一会,让cat客户端有时间上报错误
Thread.sleep(100000);
}
}
Cat列出的信息还是相对详细的,有INFO级别的日志与ERROR级别的日志,其中ERROR级别的日志显示出了所有的堆栈信息方便分析问题。
Spring Boot的集成方式相对比较简单,我们使用已经搭建完的Mybatis框架来进行测试。
@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;
}
}
图中的调用先经过了Controller,所以打印出了相关信息:
使用Spring AOP技术可以简化我们的埋点操作,通过添加统一注解的方式,使得指定方法被能被CAT监控起来。
@Retention(RUNTIME)
@Target(ElementType.METHOD)
public @interface CatAnnotation {
}
@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();
}
}
}
@RestController
public class AopController {
@RequestMapping("aop")
@CatAnnotation
public String aop1(){
return "aop";
}
}
Spring MVC的集成方式,官方提供的是使用AOP来进行集成,源码如下:
@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 "";
}
@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();
}
}
CAT提供给我们完善的告警功能。合理、灵活的监控规则可以帮助更快、更精确的发现业务线上故障。
只有配置为告警服务器的机器,才会执行告警逻辑;只有配置为发送服务器的机器,才会发送告警。
进入功能 全局系统配置-服务端配置,修改服务器类型,对告警服务器增加<property name="alarm-machine" value="true"/>
配置、以及<property name="send-machine" value="true"/>
配置。如下图所示:
告警策略:配置某种告警类型、某个项目、某个错误级别,对应的告警发送渠道,以及暂停时间。
举例:下述配置示例,说明对于Transaction告警,当告警项目名为demo_project:
<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>
告警发送中心的配置。(什么是告警发送中心:提供发送短信、邮件、微信功能,且提供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>
目前CAT的监控规则有五个要素
子条件类型:
有六种类型。子条件的内容为对应的阈值,请注意阈值只能由数字组成,当阈值表达百分比时,不能在最后加上百分号。八种类型如下:
类型 | 说明 |
---|---|
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分钟内的总和<=设定值就告警。 |
对Transaction的告警,支持的指标有次数、延时、失败率;监控周期:一分钟
配置说明:
对Event的个数进行告警;监控周期:一分钟
配置说明:
心跳告警是对服务器当前状态的监控,如监控系统负载、GC数量等信息;监控周期:一分钟
配置说明:
对异常的个数进行告警;监控周期:一分钟
配置说明:
@RestController
public class AlertController {
@RequestMapping(value = "/alert/msg")
public String sendAlert(@RequestParam String to) {
System.out.println("告警了" +to);
return "200";
}
}
<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
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有