调用链监控Cat实战

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

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/109888155