zoukankan      html  css  js  c++  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,爬虫等相关资料。

  • 相关阅读:
    《人月神话》阅读笔记(三)
    记账软件开发进度(六)
    记账软件开发进度(五)
    记账软件开发进度(四)
    《人月神话》阅读笔记(二)
    记账软件开发进度(三)
    package bufio
    Go语言:net/http包的使用模式和源码解析
    package http
    Golang系列中常用包
  • 原文地址:https://www.cnblogs.com/yinjihuan/p/13851669.html
Copyright © 2011-2022 走看看