1、Docker快速部署Cat
下载Cat源码:
git clone https://github.com/dianping/cat.git
容器构建:
cd docker
docker-compose up
使用官方的脚本启动报错:
Creating cat-mysql ... done
Creating cat ... error
ERROR: for cat Cannot create container for service cat: conflicting options: host type networking can't be used with links. This would result in undefined behavior
ERROR: for cat Cannot create container for service cat: conflicting options: host type networking can't be used with links. This would result in undefined behavior
ERROR: Encountered errors while bringing up the project.
修改docker-compose.yml
文件,掉network_mode: "host"
即可,修改后的docker-compose.yml
文件如下:
# [email protected]
version: '2.2'
services:
cat:
image: rolesle/cat:0.0.1
container_name: cat
######## build from Dockerfile ###########
# build:
# context: ../
# dockerfile: ./docker/Dockerfile
######## End -> build from Dockerfile ###########
environment:
# if you have your own mysql, config it here, and disable the 'mysql' config blow
- MYSQL_URL=cat-mysql # links will maintain /etc/hosts, just use 'container_name'
- MYSQL_PORT=3306
- MYSQL_USERNAME=root
- MYSQL_PASSWD=
- MYSQL_SCHEMA=cat
# 必须设置成你的机器IP地址
# - SERVER_IP=YOUR IP
working_dir: /app
volumes:
# 默认127.0.0.1,可以修改为自己真实的服务器集群地址
- "./client.xml:/data/appdatas/cat/client.xml"
# 默认使用环境变量设置。可以启用本注解,并修改为自己的配置
# - "./datasources.xml:/data/appdatas/cat/datasources.xml"
command: /bin/sh -c 'chmod +x /datasources.sh && /datasources.sh && catalina.sh run'
links:
- mysql
depends_on:
- mysql
ports:
- "8080:8080"
- "2280:2280"
# network_mode: "host"
# disable this if you have your own mysql
mysql:
container_name: cat-mysql
image: mysql:5.7.22
# expose 33306 to client (navicat)
ports:
- 33306:3306
volumes:
# change './docker/mysql/volume' to your own path
# WARNING: without this line, your data will be lost.
- "./mysql/volume:/var/lib/mysql"
# 第一次启动,可以通过命令创建数据库表 :
# docker exec 容器id bash -c "mysql -uroot -Dcat < /init.sql"
- "../script/CatApplication.sql:/init.sql"
command: mysqld -uroot --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --init-connect='SET NAMES utf8mb4;' --innodb-flush-log-at-trx-commit=0
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
MYSQL_DATABASE: "cat"
MYSQL_USER: "root"
MYSQL_PASSWORD: ""
第一次运行以后,数据库中没有表结构,需要通过下面的命令创建表:
docker exec <container_id> bash -c "mysql -uroot -Dcat < /init.sql"
<container_id>
需要替换为容器的真实id。通过docker ps可以查看到mysql容器id
mysql占用的端口为33306,用户名为root,密码为空
访问http://localhost:8080/cat即可进行Cat的主页面
官方部署文档:https://github.com/dianping/cat/wiki/readme_server
2、SpringBoot项目集成Cat监控
1)、启动Cat客户端前的准备工作
创建/data/appdatas/cat
目录,并授权
mkdir -p /data/appdatas/cat
chmod -R 777 /data/
创建/data/appdatas/cat/client.xml
,内容如下
<?xml version="1.0" encoding="utf-8"?>
<config mode="client">
<servers>
<!-- ip:部署cat应用的服务器ip port:cat服务端接收客户端数据的端口 http-port:cat应用部署到的tomcat端口-->
<server ip="127.0.0.1" port="2280" http-port="8080"/>
</servers>
</config>
2)、pom.xml
引入cat-client的依赖
<dependency>
<groupId>com.dianping.cat</groupId>
<artifactId>cat-client</artifactId>
<version>3.0.0</version>
</dependency>
指定cat-client私有仓库地址
<repositories>
<repository>
<id>unidal-nexus-repo</id>
<url>http://unidal.org/nexus/content/repositories/releases</url>
</repository>
</repositories>
3)、app.properties文件
在你的项目中创建src/main/resources/META-INF/app.properties
文件, 并添加如下内容:
app.name={appkey}
4)、Cat消息链构建思路
cat链路树是通过消息编号串联起来的,编号模型:
public static interface Context {
// 根的编号
public final String ROOT = "_catRootMessageId";
// 上级编号
public final String PARENT = "_catParentMessageId";
// 子级编号
public final String CHILD = "_catChildMessageId";
public void addProperty(String key, String value);
public String getProperty(String key);
}
消息树就是上下级编号关联起来的,所以如果是跨服务通过HTTP调用,要把编号模型放到HTTP头中,从而使得下游服务能够获取到
以A服务调用B服务(A->B
)为例:
A客户端生成编号模型,然后通过HTTP请求调用(底层使用HTTP请求,上层可能是Feign或者RestTemplate等等)的时候带过去
B客户端接收到编号模型,在本地生成消息树的时候,将编号模型植入进去完成绑定
5)、代码实现
对一个服务的埋点包含三个部分:服务的入口点埋点、调用下游服务时埋点和数据库埋点
1)服务的入口点埋点
服务的入口点埋点是通过Filter来实现的,过滤器中先从HTTP请求头中获取编号模型来恢复调用链
public class CatServletFilter implements Filter {
private String[] urlPatterns = new String[0];
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String patterns = filterConfig.getInitParameter("CatHttpModuleUrlPatterns");
if (patterns != null) {
patterns = patterns.trim();
urlPatterns = patterns.split(",");
for (int i = 0; i < urlPatterns.length; i++) {
urlPatterns[i] = urlPatterns[i].trim();
}
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String url = request.getRequestURL().toString();
for (String urlPattern : urlPatterns) {
if (url.startsWith(urlPattern)) {
url = urlPattern;
}
}
// 恢复调用链
CatContext catContext = new CatContext();
catContext.addProperty(Cat.Context.ROOT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID));
catContext.addProperty(Cat.Context.PARENT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID));
catContext.addProperty(Cat.Context.CHILD, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID));
Cat.logRemoteCallServer(catContext);
Transaction t = Cat.newTransaction(CatConstants.TYPE_URL, url);
try {
Cat.logEvent("Service.method", request.getMethod(), Message.SUCCESS, request.getRequestURL().toString());
Cat.logEvent("Service.client", request.getRemoteHost());
filterChain.doFilter(servletRequest, servletResponse);
t.setStatus(Transaction.SUCCESS);
} catch (Exception ex) {
t.setStatus(ex);
Cat.logError(ex);
throw ex;
} finally {
t.complete();
}
}
@Override
public void destroy() {
}
}
public class CatContext implements Cat.Context {
private Map<String, String> properties = new HashMap<>();
@Override
public void addProperty(String key, String value) {
properties.put(key, value);
}
@Override
public String getProperty(String key) {
return properties.get(key);
}
}
public class CatHttpConstants {
public static final String CAT_HTTP_HEADER_CHILD_MESSAGE_ID = "X-CAT-CHILD-ID";
public static final String CAT_HTTP_HEADER_PARENT_MESSAGE_ID = "X-CAT-PARENT-ID";
public static final String CAT_HTTP_HEADER_ROOT_MESSAGE_ID = "X-CAT-ROOT-ID";
}
@Configuration
public class CatFilterConfig {
@Bean
public FilterRegistrationBean catFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
CatServletFilter filter = new CatServletFilter();
registration.setFilter(filter);
registration.addUrlPatterns("/*");
registration.setName("cat-filter");
registration.setOrder(1);
return registration;
}
}
2)调用下游服务时埋点
由于调用下游服务时使用的是RestTemplate,所以这里用到了RestTemplate拦截器,这里会把编号模型放到HTTP请求头中以便下游服务能够获取到
public class CatRestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
Transaction t = Cat.newTransaction(CatConstants.TYPE_CALL, request.getURI().toString());
try {
HttpHeaders headers = request.getHeaders();
// 保存和传递CAT调用链上下文
Context ctx = new CatContext();
Cat.logRemoteCallClient(ctx);
headers.add(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT));
headers.add(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT));
headers.add(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD));
// 保证请求继续被执行
ClientHttpResponse response = execution.execute(request, body);
t.setStatus(Transaction.SUCCESS);
return response;
} catch (Exception e) {
Cat.getProducer().logError(e);
t.setStatus(e);
throw e;
} finally {
t.complete();
}
}
}
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
// 保存和传递调用链上下文
restTemplate.setInterceptors(Collections.singletonList(new CatRestInterceptor()));
return restTemplate;
}
}
如果使用Feign进行调用,可以实现RequestInterceptor:
@Component
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
Cat.Context ctx = new CatContext();
Cat.logRemoteCallClient(ctx);
requestTemplate.header(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT));
requestTemplate.header(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT));
requestTemplate.header(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD));
}
}
3)数据库埋点
集成mybatis拦截器,这里数据源用的是HikariDataSource,如果是其他数据源修改getSqlURL()
方法中的判断即可
@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})
})
@Component
public class CatMybatisInterceptor implements Interceptor {
private static Log logger = LogFactory.getLog(CatMybatisInterceptor.class);
//缓存,提高性能
private static final Map<String, String> sqlURLCache = new ConcurrentHashMap<String, String>(256);
private static final String EMPTY_CONNECTION = "jdbc:mysql://unknown:3306/%s?useUnicode=true";
private Executor target;
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
//得到类名,方法
String[] strArr = mappedStatement.getId().split("\\.");
String methodName = strArr[strArr.length - 2] + "." + strArr[strArr.length - 1];
Transaction t = Cat.newTransaction("SQL", methodName);
//得到sql语句
Object parameter = null;
if (invocation.getArgs().length > 1) {
parameter = invocation.getArgs()[1];
}
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
Configuration configuration = mappedStatement.getConfiguration();
String sql = showSql(configuration, boundSql);
//获取SQL类型
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
Cat.logEvent("SQL.Method", sqlCommandType.name().toLowerCase(), Message.SUCCESS, sql);
String s = this.getSQLDatabase();
Cat.logEvent("SQL.Database", s);
Object returnObj = null;
try {
returnObj = invocation.proceed();
t.setStatus(Transaction.SUCCESS);
} catch (Exception e) {
Cat.logError(e);
} finally {
t.complete();
}
return returnObj;
}
private javax.sql.DataSource getDataSource() {
org.apache.ibatis.transaction.Transaction transaction = this.target.getTransaction();
if (transaction == null) {
logger.error(String.format("Could not find transaction on target [%s]", this.target));
return null;
}
if (transaction instanceof SpringManagedTransaction) {
String fieldName = "dataSource";
Field field = ReflectionUtils.findField(transaction.getClass(), fieldName, javax.sql.DataSource.class);
if (field == null) {
logger.error(String.format("Could not find field [%s] of type [%s] on target [%s]",
fieldName, javax.sql.DataSource.class, this.target));
return null;
}
ReflectionUtils.makeAccessible(field);
javax.sql.DataSource dataSource = (javax.sql.DataSource) ReflectionUtils.getField(field, transaction);
return dataSource;
}
logger.error(String.format("---the transaction is not SpringManagedTransaction:%s", transaction.getClass().toString()));
return null;
}
private String getSqlURL() {
javax.sql.DataSource dataSource = this.getDataSource();
if (dataSource == null) {
return null;
}
if (dataSource instanceof HikariDataSource) {
return ((HikariDataSource) dataSource).getJdbcUrl();
}
return null;
}
private String getSQLDatabase() {
// String dbName = RouteDataSourceContext.getRouteKey();
//根据设置的多数据源修改此处,获取dbname
String dbName = null;
if (dbName == null) {
dbName = "DEFAULT";
}
String url = CatMybatisInterceptor.sqlURLCache.get(dbName);
if (url != null) {
return url;
}
//目前监控只支持mysql ,其余数据库需要各自修改监控服务端
url = this.getSqlURL();
if (url == null) {
url = String.format(EMPTY_CONNECTION, dbName);
}
CatMybatisInterceptor.sqlURLCache.put(dbName, url);
return url;
}
/**
* 解析sql语句
*
* @param configuration
* @param boundSql
* @return
*/
public String showSql(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(getParameterValue(parameterObject)));
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
}
}
}
}
return sql;
}
/**
* 参数解析
*
* @param obj
* @return
*/
private String getParameterValue(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(new Date()) + "'";
} 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) {
}
}
@Configuration
public class CatMyBatisConfig {
@Resource
private CatMybatisInterceptor catMybatisInterceptor;
@Bean
public Interceptor[] plugins() {
return new Interceptor[]{
catMybatisInterceptor};
}
}
请求接口,调用链信息如下:
参考:
Cat提供的框架集成方案:https://github.com/dianping/cat/tree/v2.0.0/%E6%A1%86%E6%9E%B6%E5%9F%8B%E7%82%B9%E6%96%B9%E6%A1%88%E9%9B%86%E6%88%90
Cat Client for Java:https://github.com/dianping/cat/blob/master/lib/java/README.zh-CN.md
https://blog.csdn.net/lkx444368875/article/details/80887496