轻量式RPC调用日志链路设计方案

导语:

调用链跟踪系统,又称为tracing,是微服务设计架构中,从系统层面对整体的monitoring和profiling的一种技术手

背景说明

由于我们的项目是微服务方向,中后台服务调用链路过深,追踪路径过长,其中某个服务报错或者异常很难追踪到对应链路和报错,且各个服务/模块之间的调用关系复杂,部分服务与服务之间还存在一些proxy服务(实现服务的多活部署)。这些现象就导致在开发调试、问题跟踪上都会逐步出现问题。因此,对MDC进行了调研。

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

跟踪链


图示中,A~E五个节点表示五个服务。用户发起一次请求RequestX到A,同时由于该请求依赖服务B与C,因此A分别发送RPC请求到B和C,B处理完请求后会直接返回到A,但是服务C还依赖服务D和E,因此还要发起两个RPC请求分别到D和E,D和E处理完毕后回到C,C才继续回复到A,最终A会回复用户ReplyX。对于这样一个请求,简单实用的分布式跟踪的实现,就是为服务器上每一次发送和接收动作来收集跟踪标识符和时间戳。
Trace和Span是两个很重要的名词。我们使用Trace表示对一次请求完整调用链的跟踪,而将两个服务例如上面的服务A和服务B的请求/响应过程叫做一次Span,trace是通过span来体现的, 通过一句话总结,我们可以将一次trace,看成是span的有向图,而这个有向图的边即为span。为了可以更好的理解这两个名词,我们可以再来看一下面的调用图。

[Span A]  ←←←(the root span)
|
+------+------+
|             |
[Span B]      [Span C] ←←←(Span C is a `ChildOf` Span A)
|             |
[Span D]      +---+-------+
|           |
[Span E]    [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G `FollowsFrom` Span F)上图中,包含了8个span信息

tracing过程的spans图

––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|> time[Span A···················································][Span B··············································][Span D··········································][Span C········································][Span E·······]        [Span F··] [Span G··] [Span H··]

Spans的时间轴关系图
而分布式跟踪系统要做的,就是记录每次发送和接受动作的标识符和时间戳,将一次请求涉及到的所有服务串联起来,只有这样才能搞清楚一次请求的完整调用链。

设计思路

如何获取 TraceId?

虽然TraceId 贯穿于整个 IT 系统,只不过大部分时候,它只是默默配合上下文承担着链路传播的职责,没有显式的暴露出来。常见的 TraceId 获取方式有以下几种:

  • 前端请求 Header 或响应体 Response:大部分用户请求都是在端上设备发起的,因此 TraceId 生成的最佳地点也是在端上设备,通过请求 Header 透传给后端服务。因此,我们在通过浏览器开发者模式调试时,就可以获取当前测试请求 Header 中的 TraceId 进行筛选。如果端上设备没有接入分布式链路追踪埋点,也可以将后端服务生成的 TraceId 添加到 Response 响应体中返回给前端。这种方式非常适合前后端联调场景,可以快速找到每一次点击对应的 TraceId,进而分析行为背后的链路轨迹与状态。
  • 网关日志:网关是所有用户请求发往后端服务的代理中转站,可以视为后端服务的入口。在网关的 access.log 访问日志中添加 TraceId,可以帮助我们快速分析每一次异常访问的轨迹与原因。比如一个超时或错误请求,到底是网关自身的原因,还是后端某个服务的原因,可以通过调用链中每个 Span 的状态得到确定性的结论。
  • 应用日志:应用日志可以说是我们最熟悉的一种日志,我们会将各种业务或系统的行为、中间状态和结果,在开发编码的过程中顺手记录到应用日志中,使用起来非常方便。同时,它也是可读性最强的一类日志,即使是非开发运维人员也能大致理解应用日志所表达的含义。因此,我们可以将 TraceId 也记录到应用日志中进行关联,一旦出现某种业务异常,我们可以先通过当前应用的日志定位到报错信息,再通过关联的 TraceId 去追溯该应用上下游依赖的其他信息,最终定位到导致问题出现的根因节点。
  • 组件日志:在分布式系统中,大部分应用都会依赖一些外部组件,比如数据库、消息、配置中心等等。这些外部组件也会经常发生这样或那样的异常,最终影响应用服务的整体可用性。但是,外部组件通常是共用的,有专门的团队进行维护,不受应用 Owner 的控制。因此,一旦出现问题,也很难形成有效的排查回路。此时,我们可以将 TraceId 透传给外部组件,并要求他们在自己的组件日志中进行关联,同时开放组件日志查询权限。举个例子,我们可以通过 SQL Hint 传播链 TraceId,并将其记录到数据库服务端的 Binlog 中,一旦出现慢 SQL 就可以追溯数据库服务端的具体表现,比如一次请求记录数过多,查询语句没有建索引等等。
基于日志模板实现日志与 TraceId 自动关联示例

基于 SDK 手动埋点需要一行行的修改代码,无疑是非常繁琐的,如果需要在日志中批量添加 TraceId,可以采用日志模板注入的方式。
目前大部分的日志框架都支持 Slf4j 日志门面,它提供了一种 MDC(Mapped Dignostic Contexts)机制,可以在多线程场景下线程安全的实现用户自定义标签的动态注入。
第一步,我们先通过 MDC 的 put 方法将自定义标签添加到诊断上下文中:

@Test 
public void testMDC() {     MDC.put("userName", "xiaoming");     MDC.put("traceId", GlobalTracer.get().activeSpan().context().toTraceId());     log.info("Just test the MDC!"); 
}

第二步,在日志配置文件的 Pattern 描述中添加标签变量 %X{userName} 和 %X{traceId}。

<appender name="console" class="ch.qos.logback.core.ConsoleAppender">     <filter class="ch.qos.logback.classic.filter.ThresholdFilter">         <level>INFO</level>     </filter>     <encoder>         <pattern>%d{HH:mm:ss} [%thread] %-5level [userName=%X{userName}] [traceId=%X{traceId}] %msg%n</pattern>         <charset>utf-8</charset>     </encoder> 
</appender>

这样,我们就完成了 MDC 变量注入的过程,最终日志输出效果如下所示:


15:17:47 [http-nio-80-exec-1] INFO [userName=xiaoming] [traceId=ee14662c52387763] Just test t
组件抽成
RestTemplate的http请求的处理方法

要将MDC(Mapped Diagnostic Context)的上下文信息传递到目标服务器,需要自定义一个Interceptor并添加到RestTemplate中。

  • 首先,创建一个类来实现ClientHttpRequestInterceptor接口,该接口提供了对每次HTTP请求前后进行处理的能力。在preHandle()方法中获取MDC的上下文信息,然后设置到HttpHeaders中;在postHandle()方法中清除MDC的上下文信息。

添加HandlerInterceptor拦截器,塞入上下文TRACE_ID

import org.slf4j.MDC;@Component
public class LoggerAdapterHandler implements HandlerInterceptor {private final static String TRACE_ID = "TRACEID";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { {MDC.put(TRACE_ID,  String spanIdNew = UUID.randomUUID().toString().replace("-","").substring(0,16));return true;}}

拦截客户端请求

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URI;
import java.util.Enumeration;@Component
public class MdcInterceptor implements ClientHttpRequestInterceptor {private static final Logger logger = LoggerFactory.getLogger(MdcInterceptor.class);@Overridepublic ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {HttpServletRequest servletRequest = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();// 从MDC中获取上下文信息Enumeration<String> mdcKeys = MDC.getCopyOfContextMap().keys();while (mdcKeys.hasMoreElements()) {String key = mdcKeys.nextElement();if (!key.equals("TRACEID")) {continue;}String value = MDC.get(key);request.getHeaders().add(key, value);}return execution.execute(request, body);}
}
  • 然后,配置RestTemplate时注入这个Interceptor:
@Configuration
public class RestConfig {@Beanpublic RestTemplate restTemplate() {RestTemplate restTemplate = new RestTemplate();List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();interceptors.add(new MdcInterceptor());restTemplate.setInterceptors(interceptors);return restTemplate;}
}

最后,就可以在其他地方使用RestTemplate发起HTTP请求了,MDC的上下文信息会被自动传递到目标服务器。
tracingId生成规则:A->ThreadPool(多线程情况下tracingId不一致,重写线程池,手动赋值父线程id)->发起http请求->B系统响应或者回调,携带A的tracingId+B的tracingId->B重复A的流程调用C或者整个流程结束
A->TracingA规则->B->TracingA规则+TracingB规则->C->TracingA规则+TracingB规则+TracingC规则
A<-TracingA规则+TracingB规则+TracingC规则<-B<-TracingA规则+TracingB规则+TracingC规则<-C<-TracingA规则+TracingB规则+TracingC规则

okhhtp请求的处理方式

编写okHttp的拦截器OkHttpLoggerInterceptor。此时需要日志打印的也可以在拦截器中编写请求的url,请求报文以及返回报文日志。

import com.csrcb.constants.MyConstant;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okio.Buffer;
import okio.BufferedSource;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.http.HttpStatus;import java.io.IOException;
import java.util.UUID;/*** @Classname OkHttpLoggerInterceptor* @Description okhttp请求的拦截器,将traceId及parentSpanId塞入*/
@Slf4j
public class OkHttpLoggerInterceptor implements Interceptor {@Overridepublic Response intercept(Chain chain) throws IOException {//目前默认使用的是post请求,且格式是utf8,使用的是application/jsonRequest request = chain.request();//copy all headers to newheadersHeaders.Builder headers = request.headers().newBuilder();//add traceid | spanid | parentspanid to headersif (StringUtils.isNotBlank(MDC.get(MyConstant.TRACE_ID_MDC_FIELD))){headers.add(MyConstant.TRACE_ID_HTTP_FIELD, MDC.get(MyConstant.TRACE_ID_MDC_FIELD));//设置X-B3-TraceId}if (StringUtils.isNotBlank(MyConstant.SPAN_ID_MDC_FIELD)){headers.add(MyConstant.PARENT_SPAN_ID_HTTP_FIELD,MDC.get(MyConstant.SPAN_ID_MDC_FIELD));}String spanIdNew = UUID.randomUUID().toString().replace("-","").substring(0,16);headers.add(MyConstant.SPAN_ID_HTTP_FIELD, spanIdNew);//设置X-B3-SpanId供外部使用//rebuild a new requestrequest = request.newBuilder().headers(headers.build()).build();Buffer buffer = new Buffer();request.body().writeTo(buffer);String requestBody = buffer.readUtf8();String requestUrl = request.url().toString();String[] url = requestUrl.split("/");log.info("[Request Addr]: " + request.url());log.info("[Service Name]: " + url[url.length - 1]+ "; [Request Body]: " + requestBody);Response response = chain.proceed(request);BufferedSource source = response.body().source();source.request(Long.MAX_VALUE);buffer = source.buffer();String responseBody = buffer.readUtf8();log.info("[Response Status Code]: " + response.code() + "; [Resonse Status Text]: " + HttpStatus.valueOf(response.code()).name());log.info("[Service Name]: " + url[url.length - 1]+ "; [Response Body]: " + responseBody);return response.newBuilder().body(ResponseBody.create(response.body().contentType(), responseBody)).build();}
}

引入方式

<!--minio-->
<dependency><groupId>cn.lifecycle</groupId><artifactId>tracing-spring-boot-starter</artifactId><version>${minio.version}</version>
</dependency>

MDC 在处理多线程父子线程无法传递数据的问题

重写线程池

public 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);}@Overridepublic void execute(Runnable runnable) {super.execute(MDCUtil.wrap(runnable,MDC.getCopyOfContextMap()));}@Overridepublic Future<?> submit(Runnable runnable) {return super.submit(MDCUtil.wrap(runnable,MDC.getCopyOfContextMap()));}@Overridepublic <T> Future<T> submit(Runnable runnable, T result) {return super.submit(MDCUtil.wrap(runnable,MDC.getCopyOfContextMap()), result);}@Overridepublic <T> Future<T> submit(Callable<T> callable) {return super.submit(MDCUtil.wrap(callable,MDC.getCopyOfContextMap()));}
}

工具类
手动set 上下文

public class MDCUtil {public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {return () -> {if (context == null) {MDC.clear();}else {MDC.setContextMap(context);}//如果不是子线程的话先生成traceIdsetTraceIdIfAbsent();try {runnable.run();}finally {MDC.clear();}};}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 ((T) callable.call());}finally {MDC.clear();}};}public static void setTraceIdIfAbsent() {if (MDC.get(Constants.TRACE_ID) == null) {MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());}}
}

延展

Zipkin(github ,homepage),是一款由java开发的分布式追踪系统。在微服务架构下,它用于帮助收集排查潜在问题的时序数据,同时管理数据收集和数据查询。
Jaeger(github ,homepage),则是受Dapper和OpenZipkin启发,由Uber使用golang开发的分布式跟踪系统。由于我们项目的技术栈为golang,所以重点调研了Jaeger并在此列出。

Jaeger的instrumentation过程
开源链路追踪比较
image.png

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

什么样的评论更容易得到别人的关注

要发表吸引人的评论&#xff0c;可以注意这些个方面&#xff1a; 合适的软件&#xff1a;用DT浏览器的笔记本写文本&#xff0c;保存为图片&#xff0c;用图片的方式评论更容易得到别人的关注。 特别的观点&#xff1a;发表与众不同的观点&#xff0c;或者从不同的角度看待问…

solr的原理是什么

1 Java程序里如果有无限for循环的代码导致CPU负载超高&#xff0c;如何排查&#xff1f; 排查Java程序中由于无限循环导致的CPU负载过高的问题&#xff0c;可以按照以下步骤进行&#xff1a; 资源监控&#xff1a; 使用系统命令行工具&#xff08;如Linux上的top或htop&#xf…

C语言第十四弹---函数递归

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 函数递归 1、递归是什么&#xff1f; 1.1、递归的思想 1.2、递归的限制条件 2、递归举例 2.1、举例1&#xff1a;求n的阶乘 2.1.1、分析和代码实现 2.1.2、…

三子棋游戏小课堂

&#x1fa90;&#x1fa90;&#x1fa90;欢迎来到程序员餐厅&#x1f4ab;&#x1f4ab;&#x1f4ab; 今天的主菜是&#xff0c;C语言实现的三子棋小游戏&#xff0c; 所属专栏&#xff1a; C语言知识点 主厨的主页&#xff1a;Chef‘s blog 前言&…

gitlab-runner注册到gitlab时报错:ERROR: Registering runner... failed xxxxxxxx

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

vscode 插件 Tailwind CSS IntelliSense 解决 class 提示问题

问题描述&#xff1a; 如下写js字符串是没有class智能提示的&#xff1a; const clsName bg-[#123456] text-[#654321] return <div className{clsName}></div>解决方案&#xff1a; 安装 clsx 依赖 pnpm i clsx设置 vscode 的 settings.json {"tailwin…

Android组件化中的Arouter学习

假设现在有两个业务组件登录和问答模块之间需要进行通信&#xff0c;可能会想到用反射的方式&#xff0c;是可以但是会影响性能&#xff0c;而写的代码比较多类名这些要记清楚。 路由可以看做表&#xff0c;每个map对应一张表 我们可以试着这么写&#xff0c;完成MainActivity跳…

LTC5548 具宽带 DC 至 6GHz IF 的 2GHz 至 14GHz 微波混频器

优势和特点 上变频或下变频高 IIP3&#xff1a;在 5.8GHz 为 24.4dBm在 9GHz 为 21.4dBm7.1dB 转换损耗 (在 5.8GHz)15.2dBm 输入 P1dB (在 5.8GHz)集成型 LO 缓冲器&#xff1a;0dBm LO 驱动可选的集成型 LO 倍频器低 LO-RF 泄漏&#xff1a;< –30dBm50Ω 宽带匹配 RF 和…

【数据结构】(三)树Tree

目录 1、基本概念 2、二叉树Binary Tree 3、树、森林与二叉树的转换 4、赫夫曼树Huffman Tree与赫夫曼编码Huffman Coding 1、基本概念 &#xff08;1&#xff09;树&#xff08;Tree&#xff09;是 n&#xff08;n ≥\geq 1&#xff09;个节点的有限集&#xff0c;n 0时称…

【word visio绘图】关闭visio两线交叉的跳线(跨线)

【visio绘图】关闭visio两线交叉的跳线&#xff08;跨线&#xff09; 1 如何在Visio绘图中关闭visio两线交叉的跳线&#xff08;跨线&#xff09;第一步&#xff1a;打开Visio并创建您的图形第二步&#xff1a;绘制您的连接线第三步&#xff1a;关闭跳线第四步&#xff1a;手动…

Android 拍照以及相册中选择(适配高版本)————上传头像并裁剪(一)

前言 在项目研发中&#xff0c;相信大家都遇到过给用户增加头像照片的需求。 随着手机版本的不断更新&#xff0c;android 8、android 9、android 10、android 12、android 13、鸿蒙系统等等&#xff1b;遇到这个功能需求&#xff0c;大家肯定会想&#xff0c;“这还不好写&…

C++文件操作(2)

文件操作&#xff08;2&#xff09; 1.二进制模式读取文本文件2.使用二进制读写其他类型内容3.fstream类4.文件的随机存取文件指针的获取文件指针的移动 1.二进制模式读取文本文件 用二进制方式打开文本存储的文件时&#xff0c;也可以读取其中的内容&#xff0c;因为文本文件…

RabbitMQ快速上手

首先他的需求实在什么地方。我美哟明显的感受到。 它给我的最大感受就是脱裤子放屁——多此一举&#xff0c;的感觉。 他将信息发送给服务端中间件。在由MQ服务器发送消息。 服务器会监听消息。 但是它不仅仅局限于削峰填谷和稳定发送信息的功能&#xff0c;它还有其他重要…

15.Golang中的反射机制及应用

目录 概述实践基本应用复杂应用 结束 概述 Golang中的反射用法还是比较简单的 reflect.TypeOf(arg)reflect.ValueOf(arg) 实践 基本应用 package mainimport ("fmt""reflect" )func reflectNum(arg interface{}) {fmt.Println("type ", re…

十分钟快速上手Spring Boot与微信小程序API接口的调用,快速开发小程序后端服务

1.1 微信小程序API接口介绍 微信小程序API接口是连接小程序前端与后端服务器的桥梁&#xff0c;它提供了丰富的功能接口&#xff0c;包括用户信息、支付、模板消息、数据存储等。这些API接口能够满足开发者在小程序中实现各种复杂业务逻辑的需求。 用户信息接口 用户信息接口…

全流程机器视觉工程开发(四)PaddleDetection C++工程化应用部署到本地DLL以供软件调用

前言 我们之前跑了一个yolo的模型&#xff0c;然后我们通过PaddleDetection的库对这个模型进行了一定程度的调用&#xff0c;但是那个调用还是基于命令的调用&#xff0c;这样的库首先第一个不能部署到客户的电脑上&#xff0c;第二个用起来也非常不方便&#xff0c;那么我们可…

【C++干货基地】C++引用与指针的区别:深入理解两者特性及选择正确应用场景

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引入 哈喽各位铁汁们好啊&#xff0c;我是博主鸽芷咕《C干货基地》是由我的襄阳家乡零食基地有感而发&#xff0c;不知道各位的…

MYSQL的配置和安装

下载安装 打开官网 MYSQL官网 点击DOWNLOADS 滑到最低下点击&#xff1a;MYSQL Community(GPL) Downlads 点击Download Archives 点击MySQL Community Server进入网站 选择相应版本下载&#xff0c;这里选择的是5.7.24版本,x86 64位【按需选择】 下载解压 配置文件…

GitLab16.8配置webhooks、Jenkins2.4配置GitLab插件实现持续集成、配置宝塔面板实现持续部署(其三)

看本篇文章的前提是已经部署完GItlab和Jenkins服务器&#xff0c;已经可以手动构建成功&#xff0c;并且经过了很多次实践&#xff0c;对这两款软件基本熟悉。 建议大家按以下顺序看 前端自动化&#xff08;其一&#xff09;部署gitlab 前端自动化&#xff08;其二&#xff0…

LeetCode--171

171. Excel 表列序号 给你一个字符串 columnTitle &#xff0c;表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。 例如&#xff1a; A -> 1 B -> 2 C -> 3 ... Z -> 26 AA -> 27 AB -> 28 ... 示例 1: 输入: columnTitle "A" 输出:…