监控之traceid

作者: 码农戏码 | 来源:发表于2019-05-07 18:54 被阅读0次

    监控之前只总结了一篇《微服务-监控》,比较宏观。其中很多细节没有过深关注到,主要还是没有实践过,更没有去深度思考,所以很多有意思的技术点都错过了,比如traceid的生成,传递

    大牛圈总的大作《微服务系统架构之分布式traceId追踪参考实现》已经给出解决方案,但还是再主动总结一下

    意义

    为什么需要traceid,为了查看完整的调用链,一旦调用过程中出现问题,可以第一时间定位到问题现场

    整个调用链是一棵树形结构,traceid的传递涉及到主干与支干,进程内与进程外

    生成

    原则是唯一不重复,比如现成的UUID

    但UUID一是丑、无意义,二是string;

    从字面意义以及未来落盘都不能说是最佳方案,比如想让traceid包含信息更丰富一些,能一眼看出此traceid是主干还是分支

    此traceid有没有最终落盘(这儿涉及到落盘抽样率,每天服务处理海量请求,总不能每个traceid都落盘)

    Random

    这儿引申到如何更好地获取一个随机数又是一个课题,另开篇吧

    传递

    《熔断机制》中提过,服务调用是一个1->N扇出,调用链展现出对应的树形结构,但调用嵌套都不会深,一般两层就差不多了

    • traceId1
      • traceId1.1
        • traceId1.1.1
      • traceId2.1
      • traceId3.1

    进程外

    服务之间的传递

    serverA --> serverB -- serverC

    这儿在设计传输协议时,在协议头里面带上traceid

    进程内

    主干

    这种场景ThreadLocal是最佳手法

    支干

    比如serviceA -- > remote.serviceB

    trace是个树形结构,可以将remote.serviceB的traceId.parentId = serviceA.traceId

    异步子任务

    子线程可以通过InheritableThreadLocal传递traceid

    顺带一下,InheritableThreadLocal的详细实现,先可补习一下ThreadLocal《解析ThreadLocal》

    在创建Thread时,会从父线程的inheritableThreadLocals复制到子线程中去,这样在子线程中就能拿到在父线程中的赋值

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    

    线程池

    如果没有线程池,以上就算是解决所有问题了,可实现毕竟是实现

    /**
     * 子线程从父线程中取值
     * @throws InterruptedException
     */
    private static void testThreadpool() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        final ThreadLocal<String>  threadLocal = threadLocal();//new InheritableThreadLocal<>()
        threadLocal.set("parent");
        for(int i=0;i<1;i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() +" get parent value:" + threadLocal.get());
                    threadLocal.set("sun");
                    System.out.println(Thread.currentThread().getId() + "==" + threadLocal.get());
                }
            };
            executorService.execute(runnable);
            Thread.sleep(100);
            executorService.execute(runnable);
            Thread.sleep(100);
            System.out.println("main:" + threadLocal.get());
        }
        executorService.shutdown();
    }
    

    为了好重现问题,线程池大小为1,但会连续跑两次任务

    pool-1-thread-1 get parent value:parent
    11==sun
    pool-1-thread-1 get parent value:sun
    11==sun
    main:parent
    

    在第二次取父线程值时,却是第一次任务线程中的赋值,在线程池中子线程不能正常获取父线程值

    线程池中,线程会复用,线程中的inheritableThreadLocals没有被清空

    解决方法一是:池中线程数大于任务线程,让线程没有重用机会

    ExecutorService executor = Executors.newFixedThreadPool(>=[任务线程数])
    

    但在多线程应用中,明显不能解决问题,任务数肯定远远超过线程数

    解决方法二是:自定义实现在使用完线程主动清空inheritableThreadLocals

    阿里开源transmittable-thread-local就实现这样的功能

    整体思路也是从主线程复制,使用,再清理

    TtlRunnable 构造方法中,调用了 TransmittableThreadLocal.Transmitter.capture() 获取当前线程中所有的上下文,并储存在 AtomicReference 中

    当线程执行时,调用 TtlRunnable run 方法,TtlRunnable 会从 AtomicReference 中获取出调用线程中所有的上下文,并把上下文给 TransmittableThreadLocal.Transmitter.replay 方法把上下文复制到当前线程。并把上下文备份

    当线程执行完,调用 TransmittableThreadLocal.Transmitter.restore 并把备份的上下文传入,恢复备份的上下文,把后面新增的上下文删除,并重新把上下文复制到当前线程

    transmittable-thread-local代码不多,但有很多亮点,可以自行膜拜

    在此场景,transmittable-thread-local还是太重了,其实可以简单借鉴一下transmittable-thread-local的思路,自定义Runnable

    public TransRunnable(Runnable runnable){
        this.runnable = runnable;
        //在创建时,获取父traceId
        this.parentId = TranceContext.getParentTrace();
    }
    @Override
    public void run() {
        //
        String old = TranceContext.getParentTrace();
        //设置父traceid
        TranceContext.setParentTrace(parentId);
        runnable.run();
        //还原
        TranceContext.setParentTrace(old);
    }
    

    在创建子线程时,把父traceId带进去,就能在子线程业务方法中拿到父traceId,这样整个调用链也不会断

    schedule

    traceid生成,有主动请求时,会生成,但如果是个系统的定时任务呢?

    1. 让taskService调用一下入口,类似模拟用户行为
    2. 主动生成一个parent traceId

    总结

    到此,对于traceid的知识结构丰满了很多

    相关文章

      网友评论

        本文标题:监控之traceid

        本文链接:https://www.haomeiwen.com/subject/sdwyoqtx.html