给RedisTemplate插入Cat打点

前言

Cat是美团开源的一套监控系统,功能非常强大
一般对方法进行打点,它会自动生成每个方法的耗时,同时也会记录全链路的每个调用方法的耗时

对于查系统的性能瓶颈和稳定性有非常大的帮助

基本用法

1
2
3
4
5
Transaction tranx = Cat.newTransaction("Cache", "get");
tranx.addData("key", "name");
//do something
tranx.setStatus("0");
tranx.complete();

上面的方法就是对中间的代码执行进行耗时打点,这里假设的是我们对Redis的get方法进行打点

  • 第一句:new一个Transaction出来,Type是Cache,也就是Transaction属于Cache,然后具体的方法是get
  • 第二句:addData,在执行过程中进行关键日志的记录,我们这里记录了get的key是name,方面查询长耗时的方法,增加一些提示性的参数
  • 第三句:执行具体的方法
  • 第四句:执行成功,设置status=0,0表示成功的意思,当然也有失败的方法,可以把具体的Exception传递进去
  • 第五句:标记Transaction完成

框架集成

Cat只是提供了一些工具,并没有直接提供方法与常见的方法集成,让我们在业务代码的每个方法都手动编码上面这些流程肯定不现实,可以借助于很多的方法进行隐式的插入逻辑。

与Dubbo集成

Dubbo提供了Filter机制,可以声明一个Filter进行对Dubbo服务方法的打点

Dubbo

在Cat的官方仓库中收集了此集成方式,可以直接使用

与Mybatis集成

和Dubbo一个,Mybatis也提供了Filter插件

Mybatis

在Cat的官方仓库中收集了此集成方式,可以直接使用

上面两种插件几乎是最常用的两个了,但是Redis的需求也比较强烈

Redis打点

Cat的官方仓库并没有提供Redis的打点插件,借着Filter的简单的逻辑,我准备找找现有框架的逻辑插入方法

在正常的SpringBoot应用中,默认的Redis使用类是RedisTemplate,如果具体到某个操作,在内部声明了多个具体的类

1
2
3
4
5
6
7
8
9
public class RedisTemplate<K, V>    {
// cache singleton objects (where possible)
private @Nullable ValueOperations<K, V> valueOps;
private @Nullable ListOperations<K, V> listOps;
private @Nullable SetOperations<K, V> setOps;
private @Nullable ZSetOperations<K, V> zSetOps;
private @Nullable GeoOperations<K, V> geoOps;
private @Nullable HyperLogLogOperations<K, V> hllOps;
}

比如当我们调用

redisTemplate.opsForSet().members(cacheName)时,

调用的是

DefaultSetOperations.members(K key)方法

所以我们只要对上面提到的具体操作的类的一些方法进行打点就行

但是很可惜,RedisTemplate并没有提供

方案一

直接使用SpringAop对具体的类进行代理

这当时是我觉得最简单的方法,但是很遗憾

1
2
class DefaultSetOperations<K, V> extends AbstractOperations<K, V> implements SetOperations<K, V> {}
class DefaultValueOperations<K, V> extends AbstractOperations<K, V> implements ValueOperations<K, V> {}

这些具体实现类都不是public的,对这些方法进行切面处理是处理不了的

方案二

最简单的方法被否决了,于是只能找一些其他的方法

当时看到Java的Agent可以在类被加载时进行一些修改,于是产生了写一个javaagent的方法

目标效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public V get(Object key) {

return execute(new ValueDeserializingRedisCallback(key) {
@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.get(rawKey);
}
}, true);
}

=>

public V get(Object key) {
Transaction tranx = Cat.newTransaction("Cache", "get");
tranx.addData("key", key);
V res = execute(new ValueDeserializingRedisCallback(key) {
@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.get(rawKey);
}
}, true);

tranx.setStatus("0");
tranx.complete();
return res;
}

但是为了得到失败的效果,同时防止Cat方法抛出异常影响正常逻辑,需要多加几个try catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public V get(Object key) {
Transaction tranx = null;
try {
tranx = Cat.newTransaction("Cache", "get");
tranx.addData("key", key);
} catch (Throwable e) {}

V res = null;

try {
V res = execute(new ValueDeserializingRedisCallback(key) {

@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.get(rawKey);
}
}, true);
} catch (Throwable e) {
try {
if (tranx != null) {
tranx.setStatus(e);
tranx.complete();
}
} catch (Throwable e) {

}
throw e;
}

try {
if (tranx != null) {
tranx.setStatus("0");
tranx.complete();
}
} catch(Throwable e) {}

return res;
}

可以看到,代码非常长,但是不用担心性能,经过编译优化之后很多其实都被优化掉了

java.lang.instrument包

Provides services that allow Java programming language agents to instrument programs running on the JVM. The mechanism for instrumentation is modification of the byte-codes of methods.
Package Specification

Oracle的官网上对这个包的定义如上,简单的说就是给与我们能力动态的修改Java类的字节码
一般可以用来监控,织入类似于AOP的逻辑

实现

当时选择了javaassit进行字节码的织入,但是javaassit有一个很大的局限就是不能使用本地变量

比如Transaction tranx这个我们在声明出来之后,在下面的代码就获取不到这个变量了

但是整个方法不会触及多线程的场景,所以想到的方案就是放在一个ThreadLocal中

先构造出一个ThreadLocal的类进行封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class RedisCatLog {
public static final ThreadLocal<RedisCatLog> THREAD_LOCAL_CAT_LOG = new ThreadLocal<>();
public static void startLog(String action, Object data) {
THREAD_LOCAL_CAT_LOG.remove();
RedisCatLog redisCatLog = new RedisCatLog(action);
redisCatLog.before(String.valueOf(data));
THREAD_LOCAL_CAT_LOG.set(redisCatLog);
}

public static void endLog(boolean success) {
RedisCatLog redisCatLog = THREAD_LOCAL_CAT_LOG.get();
if (Objects.nonNull(redisCatLog)) {
redisCatLog.after(success);
THREAD_LOCAL_CAT_LOG.remove();
}
}

private String action;
private Transaction tranx;

public RedisCatLog(String action) {
this.action = action;
}

public void before(String data) {
this.tranx = Cat.newTransaction("Cache.", this.action);
if (this.tranx instanceof NullMessage) {
log.error("is null message");
}
this.tranx.addData("key", data);
}

public void after(boolean success) {
if (!success) {
this.tranx.setStatus("failed");
} else {
this.tranx.setStatus("0");
}
this.tranx.complete();
}
}

这样,有了这个类之后,我们的织入代码就比较简单了

  • 给现有方法的开始加入RedisCatLog.startLog()
  • 给方法的结尾加上RedisCatLog.endLog()
  • 给原有的完整代码加上try catch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public V get(Object key) {
try {RedisCatLog.startLog("get", key);} catch(Throwable e) {}

try {
V res = execute(new ValueDeserializingRedisCallback(key) {
@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.get(rawKey);
}
}, true);
} catch(Throwable e) {
try {RedisCatLog.endLog(false);} catch(Throwable e) {}
throw e;
}

try {RedisCatLog.endLog(true);} catch(Throwable e) {}
return res;
}

整体来看是不是简单的了很多

下面就是具体的javaassit代码编写了

在编写时参考了文档,并没有系统的学习javaassit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String methodName = methods[i].getName();
CtClass etype = ClassPool.getDefault().get("java.lang.Throwable");
methods[i].addCatch("{ RedisCatLog.endLog(false); throw $e; }", etype);
methods[i].insertBefore(before(classMethodNameInfo.getType() + "-" + methodName));
methods[i].insertAfter(after());


public static String before(String action) {
return String.format("try { RedisCatLog.startLog(\"%s\", $1); } catch (Throwable e) {}", action);
}

public static String after() {
return "try{ RedisCatLog.endLog(true);} catch (Throwable e) {}";
}

大概的整体逻辑如下

项目我传到了Github上,https://github.com/zhyzhyzhy/CatRedisLogAspect

大家可以参考文档进行使用