前言 Cat是美团开源的一套监控系统,功能非常强大 一般对方法进行打点,它会自动生成每个方法的耗时,同时也会记录全链路的每个调用方法的耗时
对于查系统的性能瓶颈和稳定性有非常大的帮助
基本用法1 2 3 4 5 Transaction tranx = Cat.newTransaction("Cache" , "get" ); tranx.addData("key" , "name" ); 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 > { 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
大家可以参考文档进行使用