乐趣区

关于java:通用的底层埋点都是怎么做的

想要在程序里监控数据库的操作耗时,想要在底层框架中主动传递链路跟踪信息,这些需要常常会碰到,特地是在构建根底框架的时候。

外围指标只有一个,那就是在底层封装好,不必下层应用人员关怀。明天跟大家聊聊罕用的底层扩大埋点形式是怎么解决的。

框架自带扩大点

如果你应用的框架在设计的时候,就预留了扩大点就很不便了。比方 Mybatis 的拦截器,咱们能够在拦截器中对 Sql 进行监控,改写。

比方阿里的 Sentinel 框架,能够通过 SPI 来扩大 Slot,调整编排程序,新增自定义的 Slot 来实现限流告警等。

开源框架的品质参差不齐,有在晚期设计比拟好的,留足了各种扩大点,不便使用者。也有一些没有思考那么全面,导致你在应用的时候须要进行扩大,发现找不到扩大点,对于框架自身没有提供扩大点的场景,请接着看上面。

批改源码

如果框架没有扩大点,最间接的形式就是批改开源框架的源码来扩大本人想要的性能,通常的做法就是克隆源码到本人的公有仓库中,而后批改,测试,从新打包应用。

像咱们之前用了 XXL-JOB 做任务调度,也是批改了某些代码,在界面上扩大了监控告诉的配置信息,默认是只反对邮箱,能够扩大出手机,钉钉等。

批改源码不好的点在于须要跟原框架的版本进行对齐,如果不对齐,轻易改都没事。不对齐意味着修复了某些 bug 和新增了某些性能,就无奈应用了。要对齐,就须要一直的将新版本的代码合并到你本人的分支上。

还有很多公司,就是基于开源的版本,构建了公司外部本人的版本,后续间接就是跟着外部的应用需要去扩大和保护了,不会跟社区的版本进行同步,如果你们有专门的团队去做这件事件,也是一种形式。

同名文件笼罩

改源码的形式须要常常同步新版本的代码,有的时候往往只想批改某一个类而已,比方对底层的某些操作进行埋点监控,如果框架自身没有提供扩大点的话只能改源码来实现。

其实还有个投机取巧的形式,就是在我的项目中创立一个跟你要批改的截然不同的类,包名 + 类目都一样,办法名也一样,办法的实现你能够改。这样就能笼罩 jar 包中的类了,还是跟类加载程序有关系,先加载你本人定义的。

这样的形式益处在于不必常常去同步新版本的代码,如果你用的框架版本升级了,只有包名和类名不变,你这个笼罩的只是那个类而已,新增的性能和修复的 bug 都不会有影响。

切面拦挡

切面在做很多对立解决的时候十分有用,同样在做底层埋点的场景也实用。

比方咱们要对我的项目中 Mongodb 的所有操作都进行埋点监控,能够批改 MongoDB 的驱动源码,能够创立同名文件进行笼罩,形式有很多种,找到一个适合,又能实现需求的最重要。

以 Spring 中操作 Mongodb 来阐明,在 Spring Data Mongodb 中会 MongoTemplate 来操作 Mongodb。最简略的形式就是间接对 MongoTemplate 类进行埋点,这样所有的操作都能够监控起来。

用切面间接切到 MongoTemplate 的所有办法上,而后进行埋点,就很简略了。

@Aspect
public class MongoTemplateAspect {@Pointcut("execution(* org.springframework.data.mongodb.core.MongoTemplate.*(..))")
    public void pointcut() {}
    @Around("pointcut()")
    public Object around(final ProceedingJoinPoint pjp) throws Throwable {String callMethod = pjp.getSignature().getDeclaringType().getSimpleName() + "." + pjp.getSignature().getName();
        Map<String, Object> data = new HashMap<>();
        data.put("params", JsonUtils.toJson(pjp.getArgs()));
        return CatTransactionManager.newTransaction(() -> {
            try {return pjp.proceed();
            } catch (Throwable throwable) {throw new RuntimeException(throwable);
            }
        }, "Mongo", callMethod, data);
    }
}

又比方,你还想监控 Redis 相干的,Redis 用的也是跟 Spring 整合的框架,那么也有 RedisTemplate 这个类,同样也能够用切面来实现。

基于 Template 类来埋点,绝对比拟下层,如果还想在底层一点进行监控,也就是 Connection 这层,Template 外面的操作都是基于 Connection 来实现的。

同样咱们能够用切面来替换 Connection 相干的实现,比方能够用切面切到获取 Connection 的办法,而后替换 Connection 的对象为具备埋点监控的对象。

@Aspect
public class RedisAspect {@Pointcut("target(org.springframework.data.redis.connection.RedisConnectionFactory)")
    public void connectionFactory() {}
    @Pointcut("execution(org.springframework.data.redis.connection.RedisConnection *.getConnection(..))")
    public void getConnection() {}
    @Pointcut("execution(org.springframework.data.redis.connection.RedisClusterConnection *.getClusterConnection(..))")
    public void getClusterConnection() {}
    @Around("getConnection() && connectionFactory()")
    public Object aroundGetConnection(final ProceedingJoinPoint pjp) throws Throwable {RedisConnection connection = (RedisConnection) pjp.proceed();
        return new CatMonitorRedisConnection(connection);
    }
    @Around("getClusterConnection() && connectionFactory()")
    public Object aroundGetClusterConnection(final ProceedingJoinPoint pjp) throws Throwable {RedisClusterConnection clusterConnection = (RedisClusterConnection) pjp.proceed();
        return new CatMonitorRedisClusterConnection(clusterConnection);
    }
}

CatMonitorRedisConnection 中对原生的 RedisConnection 做了加强,也不会影响原有的 RedisConnection 的性能。

public class CatMonitorRedisConnection implements RedisConnection {
    private final RedisConnection connection;
    private CatMonitorHelper catMonitorHelper;
    public CatMonitorRedisConnection(RedisConnection connection) {
        this.connection = connection;
        this.catMonitorHelper = new CatMonitorHelper();}

    @Override
    public byte[] get(byte[] key) {return catMonitorHelper.execute(RedisCommand.GET, key, () -> connection.get(key));
    }
}

Java Agent

Java Agent 能够在运行期将曾经加载的类的字节码进行变更,能够退出咱们须要进行监控的代码逻辑。无需对原有代码进行革新,零侵入性。

在十分多优良的开源框架中都看到了 Java Agent 的利用,像 APM 框架 SkyWalking,异步传递上下文 transmittable-thread-local 等。

Java Agent 绝对其余的形式来说,还是有肯定的门槛,毕竟不是日常开发中常常会用到的技术点。如果想理解这种扩大形式,能够看看一些曾经用了的开源框架的源码,就晓得大略怎么应用了。上面贴一段 transmittable-thread-local 中对线程池进行扩大的代码吧,次要就是利用了 javassist 操作字节码。

try {final CtMethod afterExecute = clazz.getDeclaredMethod("afterExecute", new CtClass[]{runnableClass, throwableClass});
// unwrap runnable if IsAutoWrapper
String code = "$1 = com.alibaba.ttl.threadpool.agent.internal.transformlet.impl.Utils.unwrapIfIsAutoWrapper($1);";
logger.info("insert code before method" + signatureOfMethod(afterExecute) + "of class" + afterExecute.getDeclaringClass().getName() + ":" + code);
afterExecute.insertBefore(code);
modified = true;
} catch (NotFoundException e) {// clazz does not override afterExecute method, do nothing.}

对于作者 :尹吉欢,简略的技术爱好者,《Spring Cloud 微服务 - 全栈技术与案例解析》,《Spring Cloud 微服务 入门 实战与进阶》作者, 公众号 猿天地 发起人。

我整顿了一份很全的学习材料,感兴趣的能够微信搜寻「猿天地 」,回复关键字「 学习材料」获取我整顿好了的 Spring Cloud,Spring Cloud Alibaba,Sharding-JDBC 分库分表,任务调度框架 XXL-JOB,MongoDB,爬虫等相干材料。

退出移动版