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