traceId跟踪请求全流程日志

news2024/11/30 10:33:51

一个系统被拆分成N多个模块,这些模块负责不同的功能,组合成一套系统,最终可以提供丰富的功能。在这种分布式架构中,一次请求往往需要涉及到多个服务,如下图:

 

服务之间的调用错综复杂,对于维护的成本成倍增加,势必存在以下几个问题:

服务之间的依赖与被依赖的关系如何能够清晰的看到?
出现异常时如何能够快速定位到异常服务?
出现性能瓶颈时如何能够迅速定位哪个服务影响的?


为了能够在分布式架构中快速定位问题,分布式链路追踪应运而生。将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

在典型的微服务体系结构中,我们有许多单独部署的小型应用程序,它们经常需要相互通信。开发人员面临的挑战之一是要跟踪日志的完整请求,以调试或检查下游服务中的延迟。

一个 Web 请求从网关流入,有可能会调用多个服务对请求进行处理,拿到最终结果。这个过程中每个服务之间的通信又是单独的网络请求,无论请求流经的哪个服务出了故障或者处理过慢都会对前端造成影响。

为了进一步增加复杂性,某些服务可以运行多个实例。很难在多个服务中跟踪特定的请求日志,尤其是在特定服务具有多个实例的情况下。

核心思想:将​​trace_id​​设置到请求头中透传给下游服务,下游服务按同样的方式再透传给下游服务。

步骤:

当请求达到网关时,网关中的拦截器会从请求头中获取trace_id,如果为空,则生成一个trace_id,并设置到SLF4J的MDC中,在网关转发请求时将trace_id设置到请求头中。
当请求达到服务A时,应用中的拦截器会从请求头中获取trace_id,如果为空,则生成一个trace_id,并设置到SLF4J的MDC中,如果服务A调用服务B/服务C,那么需要将trace_id设置到请求头中。
其他服务(服务B/服务C)的处理方式与步骤2的处理方式一致。将trace_id设置到MDC中的目的是为了SLF4J在打印日志时将trace_id打印出来。
常见的链路追踪技术有哪些?
市面上有很多链路追踪的项目,其中也不乏一些优秀的,如下:

cat:由大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监控 。 集成方案是通过代码埋点的方式来实现监控,比如: 拦截器,过滤器等。 对代码的侵入性很大,集成成本较高,风险较大。
zipkin:由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。该产品结合spring-cloud-sleuth使用较为简单, 集成很方便, 但是功能较简单。
pinpoint:韩国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件,UI功能强大,接入端无代码侵入
skywalking:SkyWalking是本土开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件,UI功能较强,接入端无代码侵入。目前已加入Apache孵化器。
Sleuth:SpringCloud 提供的分布式系统中链路追踪解决方案。很可惜的是阿里系并没有链路追踪相关的开源项目,我们可以采用Spring Cloud Sleuth+Zipkin来做链路追踪的解决方案。
Spring Cloud Sleuth会自动向您的日志和服务间通信中添加一些跟踪/元数据(通过请求标头),因此可以通过Zipkins,ELK等日志聚合器轻松跟踪请求。

Spring Cloud Sleuth


学习Sleuth之前必须了解它的几个概念:

Span:基本的工作单元,相当于链表中的一个节点,通过一个唯一ID标记它的开始、具体过程和结束。我们可以通过其中存储的开始和结束的时间戳来统计服务调用的耗时。除此之外还可以获取事件的名称、请求信息等。
Trace:一系列的Span串联形成的一个树状结构,当请求到达系统的入口时就会创建一个唯一ID(traceId),唯一标识一条链路。这个traceId始终在服务之间传递,直到请求的返回,那么就可以使用这个traceId将整个请求串联起来,形成一条完整的链路。
Annotation:一些核心注解用来标注微服务调用之间的事件,重要的几个注解如下:
cs(Client Send):客户端发出请求,开始一个请求的生命周期
sr(Server Received):服务端接受请求并处理;sr-cs = 网络延迟 = 服务调用的时间
ss(Server Send):服务端处理完毕准备发送到客户端;ss - sr = 服务器上的请求处理时间
cr(Client Reveived):客户端接受到服务端的响应,请求结束; cr - sr = 请求的总时间
Spring Cloud Sleuth在您的日志中添加了唯一的ID,这些ID在许多微服务之间保持不变,并且可由普通的日志聚合器用来查看请求的流向。

要添加此功能,我们需要在pom.xml每个下游服务的文件中添加一个依赖项:

<dependency>  
 <groupId>org.springframework.cloud</groupId>  
 <artifactId>spring-cloud-starter-sleuth</artifactId>  
</dependency>


Spring Cloud Sleuth将两种类型的ID添加到您的日志记录中:

  • 跟踪ID:唯一的ID,在包含多个微服务的整个请求中保持不变。
  • Span ID:每个微服务的唯一ID。

在分布式链路跟踪中有两个重要的概念:跟踪(trace)和 跨度( span)。trace 是请求在分布式系统中的整个链路视图,span 则代表整个链路中不同服务内部的视图,span 组合在一起就是整个 trace 的视图。

在整个请求的调用链中,请求会一直携带 traceid 往下游服务传递,每个服务内部也会生成自己的 spanid 用于生成自己的内部调用视图,并和traceid一起传递给下游服务。

基本上,跟踪ID将包含多个Span ID,日志聚合工具可以轻松使用它们。

Sleuth不仅将这些ID添加到我们的日志中,还将它们传播到下一个服务调用(基于HTTP或MQ)。而且,它可以将随机样本日志发送到开箱即用的Zipkins等外部应用程序。

traceid 在请求的整个调用链中始终保持不变,所以在日志中可以通过 traceid 查询到整个请求期间系统记录下来的所有日志

请求到达每个服务后,服务都会为请求生成spanid,而随请求一起从上游传过来的上游服务的 spanid 会被记录成parent-spanid或者叫 pspanid。当前服务生成的 spanid 随着请求一起再传到下游服务时,这个spanid 又会被下游服务当做 pspanid 记录。

之后,我们需要在​​application.properties​​每个服务的文件中添加以下属性:

spring.sleuth.sampler.probability=100  
spring.zipkin.baseUrl= http://localhost:9411/


通过在访问日志和业务日志里记录的traceid、spanid 和 pspanid 能完整的还原出整个请求的调用链路视图,对错误排查能起到很大的帮助。
该spring.zipkin.baseUrl属性告诉Spring和Sleuth将数据推送到何处。另外,默认情况下,Spring Cloud Sleuth会将所有范围设置为nonexportable。这意味着这些跟踪(跟踪ID和跨度ID)会显示在日志中,但不会导出到其他远程存储(如Zipkin)。

想要跟踪请求,第一个想到的就是当请求来时生成一个traceId放在ThreadLocal里,然后打印时去取就行了。但在不改动原有输出语句的前提下自然需要日志框架的支持了,搜索的一番发现主流日志框架都提供了MDC功能。

MDC


MDC 介绍 MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

简而言之,MDC就是日志框架提供的一个InheritableThreadLocal,项目代码中可以将键值对放入其中,然后使用指定方式取出打印即可。

首先创建拦截器,加入拦截列表中,在请求到达时生成traceId。当然你还可以根据需求在此处后或后续流程中放入spanId、订单流水号等需要打印的信息。

public class Constants {


    /**
     * 日志跟踪id名。
     */
    public static final String LOG_TRACE_ID = "traceid";
    /**
     * 请求头跟踪id名。
     */
    public static final String HTTP_HEADER_TRACE_ID = "app_trace_id";
}

import org.slf4j.MDC;


public class TraceInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // "traceId"
        MDC.put(Constants.LOG_TRACE_ID, TraceLogUtils.getTraceId());
        return true;
    }
}



然后在日志配置xml文件中添加traceId打印:

<property name="normal-pattern" value="[%p][%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ,Asia/Shanghai}][%X{traceid}][%15.15t][%c:%L] %msg%n"/>


1.仅仅这样的改造在实际使用过程中会遇到以下问题:

线程池中的线程会打印错误的traceId
调用下游服务后会生成新的traceId,无法继续跟踪
MDC使用的InheritableThreadLocal只是在线程被创建时继承,但是线程池中的线程是复用的,后续请求使用已有的线程将打印出之前请求的traceId。这时候就需要对线程池进行一定的包装,在线程在执行时读取之前保存的MDC内容。

​​ThreadPoolExecutor​​的包装也类似,注意为了严谨考虑,需要对连接池中的所有调用方法进行封装。

提供一下我的工具类:

public class ThreadMdcUtil {


    public static void setTraceIdIfAbsent() {
        if (MDC.get(Constants.LOG_TRACE_ID) == null) {
            MDC.put(Constants.LOG_TRACE_ID, TraceLogUtils.getTraceId());
        }
    }


    public static void setTraceId() {
        MDC.put(Constants.LOG_TRACE_ID, TraceLogUtils.getTraceId());
    }


    public static void setTraceId(String traceId) {
        MDC.put(Constants.LOG_TRACE_ID, traceId);
    }


    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }


    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }


    public static class ThreadPoolTaskExecutorMdcWrapper extends ThreadPoolTaskExecutor {
        @Override
        public void execute(Runnable task) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }


        @Override
        public void execute(Runnable task, long startTimeout) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), startTimeout);
        }


        @Override
        public <T> Future<T> submit(Callable<T> task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }


        @Override
        public Future<?> submit(Runnable task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }


        @Override
        public ListenableFuture<?> submitListenable(Runnable task) {
            return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }


        @Override
        public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
            return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    }


    public static class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
        public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        }


        public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
        }


        public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
        }


        public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
                                            RejectedExecutionHandler handler) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
        }


        @Override
        public void execute(Runnable task) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }


        @Override
        public <T> Future<T> submit(Runnable task, T result) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
        }


        @Override
        public <T> Future<T> submit(Callable<T> task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }


        @Override
        public Future<?> submit(Runnable task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    }


    public static class ForkJoinPoolMdcWrapper extends ForkJoinPool {
        public ForkJoinPoolMdcWrapper() {
            super();
        }


        public ForkJoinPoolMdcWrapper(int parallelism) {
            super(parallelism);
        }


        public ForkJoinPoolMdcWrapper(int parallelism, ForkJoinWorkerThreadFactory factory,
                                      Thread.UncaughtExceptionHandler handler, boolean asyncMode) {
            super(parallelism, factory, handler, asyncMode);
        }


        @Override
        public void execute(Runnable task) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }


        @Override
        public <T> ForkJoinTask<T> submit(Runnable task, T result) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
        }


        @Override
        public <T> ForkJoinTask<T> submit(Callable<T> task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    }
}


以上方式在多级服务调用中每个服务都会生成新的traceId,导致无法衔接跟踪。这时就需要对http调用工具进行相应的改造了,在发送http请求时自动将traceId添加到header中,以RestTemplate为例,注册拦截器:

// 以下省略其他相关配置
RestTemplate restTemplate = new RestTemplate();
// 使用拦截器包装http header
restTemplate.setInterceptors(new ArrayList<ClientHttpRequestInterceptor>() {
    {
        add((request, body, execution) -> {
            String traceId = MDC.get(Constants.LOG_TRACE_ID);
            if (StringUtils.isNotEmpty(traceId)) {
                request.getHeaders().add(Constants.HTTP_HEADER_TRACE_ID, traceId);
            }
            return execution.execute(request, body);
        });
    }
});


HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
// 注意此处需开启缓存,否则会报getBodyInternal方法“getBody not supported”错误
factory.setBufferRequestBody(true);
restTemplate.setRequestFactory(factory);



下游服务的拦截器改为:

public class TraceInterceptor extends HandlerInterceptorAdapter {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String traceId = request.getHeader(Constants.HTTP_HEADER_TRACE_ID);
        if (StringUtils.isEmpty(traceId)) {
            traceId = TraceLogUtils.getTraceId();
        }
        MDC.put(Constants.LOG_TRACE_ID, traceId);
        return true;
    }
}


若使用自定义的http客户端,则直接修改其工具类即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/741118.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue路由传递对象数组,打印数据 [object Object] 解决方法

1、router路由传参一般两种方式。一种是query传参&#xff0c;另外一种则是params传参。由于params传参刷新页面&#xff0c;会导致数据丢失。所以采用query传参方式比较多&#xff0c;但当使用query传递对象&#xff0c;数组时&#xff0c;刷新页面会报[object Object]&#x…

J. Not Another Path Query Problem

Problem - J - Codeforces 思路&#xff1a;因为我们要让路径的与大于等于V&#xff0c;假设某个路径的与为S&#xff0c;存在两种可能&#xff0c;一种是SV&#xff0c;另一种可能是在第i个位置V的二进制为零&#xff0c;而S的二进制为1&#xff0c;且前i-1个二进制是相等的。…

FLAC格式如何转换为MP3?分享三种方法!

在数字音乐的世界中&#xff0c;FLAC和MP3是两种常见的音频格式。FLAC (Free Lossless Audio Codec)提供无损的音质&#xff0c;但文件大小较大。而MP3文件较小&#xff0c;更易于传输和保存&#xff0c;但可能牺牲一些音质。如果你想将FLAC音频转换成MP3格式&#xff0c;本文将…

【算法】数位DP

文章目录 数位DP前置知识——位运算与集合论 例题——2376. 统计特殊整数思路代码模板&#xff08;重要&#xff01;⭐⭐⭐⭐⭐&#xff09;针对这道题&#xff0c;可以去掉 isNum 参数 相关题目练习233. 数字 1 的个数⭐⭐⭐⭐⭐代码模板修改——记录cnt&#xff08;前面已经选…

Docker(三)之容器管理工具 Docker生态架构及部署

容器管理工具Docker生态架构及部署 一、Docker生态架构 1.1 Docker Containers Are Everywhere 1.2 生态架构 1.2.1 Docker Host 用于安装Docker daemon的主机&#xff0c;即为Docker Host&#xff0c;并且该主机中可基于容器镜像运行容器。 1.2.2 Docker daemon 用于管理…

基础篇-STM32初体验

MDK5编译例程 串口下载程序 DAP下载程序 DAP调试程序

多领域入选!棱镜七彩上榜《嘶吼2023网络安全产业图谱》

2023年7月10日&#xff0c;国内网络安全行业第三方研究机构嘶吼安全产业研究院联合国家网络安全产业园区&#xff08;通州园&#xff09;正式发布《嘶吼2023网络安全产业图谱》&#xff0c;棱镜七彩凭借在开源安全领域的创新性及服务能力&#xff0c;上榜开发与应用、应用于产业…

解决2003-Host‘ ‘is not allowed to connect to this MySQL server,实现远程连接本地数据库

目录 1.打开终端控制面板 2.进入mysql库 3.执行更新权限语句 4.查看权限 5.刷新服务器配置 6.进入Navict测试连接 在使用Navicat远程连接本地数据库时&#xff0c;遇到了这样一个问题&#xff0c; 我使用 本地主机的地址&#xff0c;连接本地的数据库&#xff0c;报错host…

(CentOS 7)nvidia-smi:Failed to initialize NVML: Driver/library version mismatch

[CentOS 7]nvidia-smi:Failed to initialize NVML: Driver/library version mismatch 问题源头&#xff1a; nvidia-smi \text{nvidia-smi} nvidia-smi报错问题 CUDA \text{CUDA} CUDA安装时的问题 这里仅描述自身发现的一种情况&#xff0c;希望对大家有所帮助。 问题源头&…

JMX+Prometheus监控Grafana展示

文章目录 概述Java代码使用PrometheusApi统计监控指标PrometheusGrafana展示 概述 最近在阅读InLong的源码&#xff0c;发现它采用通过JMXPrometheus进行指标监控。 这里做了下延伸将介绍使用JMXPrometheusGrafana进行监控指标展示&#xff0c;这里单独将Metric部分代码抽离出…

网络配置管理器中的系统日志配置

包含许多设备的大型网络基础设施将在其清单中具有某些重要和关键设备&#xff0c;例如核心路由器或防火墙。这些设备必须始终受到有关任何配置更改的持续监视。 在如此庞大的网络中&#xff0c;手动跟踪所有这些重要设备并在每次进行新更改时触发备份几乎是不可能的。如果管理…

windows环境部署seata注意事项

1.将seata放置微服务项目中&#xff1a; 1.服务端下载地址&#xff1a;https://github.com/seata/seata/releases/download/v1.4.2/seata-server-1.4.2.zip 2.源码下载地址: https://github.com/seata/seata &#xff08;将script目录以及里面文件放至seata-server中&#xff…

力扣挑战:中枢整数的定义与寻找方法

本篇博客讲解力扣“2485. 找出中枢整数”的解题思路&#xff0c;这是题目链接。 给定一个正整数n&#xff0c;如果它存在一个中枢整数x&#xff0c;那么满足以下等式&#xff1a; 123…x x(x1)(x2)…n 利用等差数列求和公式&#xff1a;(首项末项)项数2&#xff0c;以及项数…

优思学院|TQM与六西格玛完美契合:质量和利润的共赢之道

TQM的本质乃无止境地追求质量&#xff0c;然而在解决各个问题点时&#xff0c;直到目的逹成之前必须不断地转动PDCA或者六西格玛方法中的DMAIC这些个活动&#xff0c;究竟与经营有什么关连呢&#xff1f; 我们都知道企业的目的是生产好的产品、提供好的服务&#xff0c;并以合…

火热的低代码和无代码赛道

一、背景 星霜荏苒&#xff0c;居诸不息。互联网技术飞速发展&#xff0c;软件的设计、开发、应用也是风发泉涌&#xff0c;无论是开发工具还是应用程序&#xff0c;都在不断追求降本增效&#xff0c;极大地推动了软件研发的长足进步。但然而&#xff0c;长期以来&#xff0c;我…

elementui-drawer模板

1、效果图 2、上代码 <template><div><el-drawersize"100%":visible.sync"drawer"style"position: absolute;"class"details":modal-append-to-body"false":modal "false":before-close"ha…

Linux环境基础开发工具使用(yum软件安装工具的使用、vim编辑器使用及握gcc/g++编译器的使用等)

Linux环境基础开发工具使用 1.Linux 软件包管理器 yum1.1 什么是软件包1.2 yum常用命令1.3 好玩的yum包 2.Linux开发工具2.1 vim工具的由来2.2 vim模式①基本模式②派生模式 2.3 vim的基本操作2.4 vim正常模式命令集2.5 vim末行模式命令集2.6 简单vim配置 3.Linux编译器 - gcc/…

并发容器(三)BlockigQueue

阻塞队列 看几个常用的实现&#xff1a; 1.ArrayBlockingQueue是最简单的一种阻塞队列&#xff0c;底层是由数组实现 2.LinkedBlockingQueue 底层是由链表实现的&#xff0c;锁的粒度更细&#xff0c;但是占用的内存更大 当移除元素的时候takeLock和putLock一起加 3.Synchrono…

HCIP第七天

题目 拓扑图 1.所有路由器各自创建一个环回接口&#xff0c;合理规划IP地址 测试 2. R1-R2-R3-R4-R6之间使用OSPF协议&#xff0c;R4-R5-R6之间使用RIP协议 3. R1环回重发布方式引入OSPF网络 4. R4/R6上进行双点双向重发布 将OSPF中的环回接口改成broadcast 因为华为默认环回接…

用ChatGPT解析Wireshark抓取的数据包样例

用Wireshark抓取的数据包&#xff0c;常用于网络故障排查、分析和应用程序通信协议开发。其抓取的分组数据结果为底层数据&#xff0c;看起来比较困难&#xff0c;现在通过chatGPT大模型&#xff0c;可以将原始抓包信息数据提交给AI进行解析&#xff0c;本文即是进行尝试的样例…