Dubbo分布式日志跟踪实现

前言

随着越来越多的应用逐渐微服务化后,分布式服务之间的RPC调用使得异常排查的难度骤增,最明显的一个问题,就是整个调用链路的日志不在一台机器上,往往定位问题就要花费大量时间。如何在一个分布式网络中把单次请求的整个调用日志给串起来,变得刻不容缓。

笔者基于 Dubbo 框架的 Filter 扩展点实现了一个分布式日志跟踪工具 dubbo-tracing,源码地址:https://github.com/panchanghe/dubbo-tracing

实现思路

Dubbo 作为国内最热门的 RPC 框架之一,对外提供了丰富的功能扩展点,日志跟踪就需要用到其中org.apache.dubbo.rpc.Filter扩展点。

Filter 扩展点可以在 Consumer 发起 RPC 调用前和 Provider 处理请求前发起拦截,执行我们特定的业务逻辑来对 Dubbo 做增强。另外,Dubbo RPC 调用除了方法入参,还额外提供了 Map 类型的 attachments 来隐式的传递参数。

有了这些前提,要实现分布式日志跟踪就简单了。通过实现 Filter 扩展点拦截 RPC 调用,最早的 Consumer 端生成一个唯一的 TraceId 进行透传,TraceId 在整个调用链路里保持一致,TraceId 会被写到日志上下文 MDC 中,最终和业务日志一起打印到日志文件里,这样通过 TraceId 检索就能获取整个调用链路的所有日志。一个完整的 RPC 调用链路是一个树状结构,最早发起调用的节点是根节点,一直向下延伸,为了把整个链路的日志构造成树状结构展示,我们还需要一个 SpanId,它代表了当前日志在整个调用链路中的层级。有了这些日志数据,再搭配日志检索服务 + 图形化展示,分布式问题的排查就会简单很多。

TraceId和SpanId生成规则

这里借鉴一下阿里的做法。

TraceId一般由接受请求的第一个服务器产生,具有唯一性,且在整个调用链路中保持不变。

TraceId的生成规则是:服务器IP + 时间戳 + 自增序列 + 进程号,比如:

c0a861711731309291125100068524

前8位c0a86171是生成TraceId的服务器IP,它被编码为十六进制,每2位代表IP地址中的一段,转换成十进制结果就是192.168.97.113,可以根据该号段快速定位到生成TraceId的服务器。

后面的13位1731309291125是生成TraceId的毫秒级时间戳;之后的4位1000是一个自增的序列,从 1000 开始,涨到 9999 后又会回到 1000;最后的部分68524是当前进程的ID,主要是为了防止单机多进程间产生的TraceId发生冲突。

SpanId 代表本次调用在整个调用链路树中的位置。

假设一个 Web 系统 A 接收了一次用户请求,那么在这个系统的 MVC 日志中,记录下的 SpanId 是 0,代表是整个调用的根节点,如果 A 系统处理这次请求,需要通过 RPC 依次调用 B、C、D 三个系统,那么在 A 系统的 RPC 客户端日志中,SpanId 分别是 0.1,0.2 和 0.3,在 B、C、D 三个系统的 RPC 服务端日志中,SpanId 也分别是 0.1,0.2 和 0.3;如果 C 系统在处理请求的时候又调用了 E,F 两个系统,那么 C 系统中对应的 RPC 客户端日志是 0.2.1 和 0.2.2,E、F 两个系统对应的 RPC 服务端日志也是 0.2.1 和 0.2.2。

根据上面的描述可以知道,如果把一次调用中所有的 SpanId 收集起来,可以组成一棵完整的链路树。

假设一次分布式调用中产生的 TraceId 是 0a1234(实际不会这么短),那么根据上文 SpanId 的产生过程,如下图所示:

具体实现

1、首先是实现一个根据 机器IP、时间戳、自增序列、进程ID 生成 TraceId 的方案:

public class IdUtils {private static final String PROCESS_ID;private static final String IP_HEX_CODE;private static final AtomicInteger COUNTER;private static final int COUNT_INIT_VALUE = 1000;private static final int COUNT_MAX_VALUE = 9999;private static long lastTimestamp = 0L;static {PROCESS_ID = ProcessIdUtil.getProcessId();IP_HEX_CODE = getIpHexCode();COUNTER = new AtomicInteger(COUNT_INIT_VALUE);}/*** 8位         13位            4位* 服务器 IP + ID 产生的时间 + 自增序列 + 当前进程号** @return*/public static synchronized String newTraceId() {final long timestamp = System.currentTimeMillis();long count;if (timestamp > lastTimestamp) {COUNTER.set(COUNT_INIT_VALUE);count = COUNT_INIT_VALUE;lastTimestamp = timestamp;} else {count = COUNTER.incrementAndGet();if (count == COUNT_MAX_VALUE) {COUNTER.set(COUNT_INIT_VALUE - 1);}}return IP_HEX_CODE + timestamp + count + PROCESS_ID;}private static String getIpHexCode() {final StringBuilder builder = new StringBuilder();String host = NetUtils.getLocalHost();String[] split = host.split("\\.");for (String s : split) {String hex = Integer.toHexString(Integer.valueOf(s));if (hex.length() == 1) {hex = "0" + hex;}builder.append(hex);}return builder.toString();}
}

2、为了方便本地透传 TraceId 等信息,必然要用到 ThreadLocal 来记录,所以我们创建一个 TraceContext 类来读写当前线程的 Trace 信息。

public class TraceContext {private static final ThreadLocal<Map<String, Object>> TRACE_THREAD_LOCAL = new ThreadLocal() {@Overrideprotected Object initialValue() {return new HashMap<>();}};public static boolean isStarted() {return !get().isEmpty();}public static void start(String traceId) {start(traceId, "0");}public static void start(String traceId, String spanId) {get().put(TracingConstant.TRACE_ID, traceId);get().put(TracingConstant.SPAN_ID, spanId);get().put(TracingConstant.LOGIC_ID, new AtomicInteger(0));}public static String getTraceId() {return (String) get().get(TracingConstant.TRACE_ID);}public static String getSpanId() {String s = (String) get().get(TracingConstant.SPAN_ID);return s;}public static int nextLogicId() {return ((AtomicInteger) get().get(TracingConstant.LOGIC_ID)).incrementAndGet();}private static Map<String, Object> get() {return TRACE_THREAD_LOCAL.get();}public static void clear() {TRACE_THREAD_LOCAL.remove();}
}

3、Consumer 端的 Filter 扩展,判断当前线程是否已经生成 TraceId,如果没有则生成新的 TraceId 和 SpanId 写入到 ThreadLocal 同时通过 attachments 透传到 Provider。

@Activate(group = {"consumer"})
public class ConsumerTraceFilter implements Filter {@Overridepublic Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {if (!TraceContext.isStarted()) {TraceContext.start(getTraceId());}ThreadContext.put(TracingConstant.TRACE_ID, TraceContext.getTraceId());ThreadContext.put(TracingConstant.SPAN_ID, TraceContext.getSpanId());invocation.setAttachment(TracingConstant.DUBBO_TRACE_ID, TraceContext.getTraceId());invocation.setAttachment(TracingConstant.DUBBO_SPAN_ID, TraceContext.getSpanId() + "." + TraceContext.nextLogicId());return invoker.invoke(invocation);}private String getTraceId() {String traceId = ThreadContext.get(TracingConstant.TRACE_ID);if (StringUtils.isEmpty(traceId)) {traceId = IdUtils.newTraceId();}return traceId;}
}

4、Provider 端的 Filter 扩展,读取 attachments 透传过来的 TraceId 和 SpanId,如果能读到,就将它们写入本地 ThreadLocal 里,开启 TraceContext,后续如果自己再发起下游的 RPC 调用,则会以它们为基础数据,发给下游节点,整个链路就能串起来了。

@Activate(group = {"provider"})
public class ProviderTraceFilter implements Filter {@Overridepublic Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {final String traceId = RpcContext.getServerAttachment().getAttachment(TracingConstant.DUBBO_TRACE_ID);final String spanId = RpcContext.getServerAttachment().getAttachment(TracingConstant.DUBBO_SPAN_ID);if (StringUtils.isAnyEmpty(traceId, spanId)) {return invoker.invoke(invocation);}TraceContext.start(traceId, spanId);ThreadContext.put(TracingConstant.TRACE_ID, TraceContext.getTraceId());ThreadContext.put(TracingConstant.SPAN_ID, TraceContext.getSpanId());try {return invoker.invoke(invocation);} catch (Throwable e) {throw e;} finally {TraceContext.clear();ThreadContext.remove(TracingConstant.TRACE_ID);ThreadContext.remove(TracingConstant.SPAN_ID);}}
}

5、为了让我们自定义的 Filter 能被 Dubbo 加载并执行,还需要在 META-INF/dubbo/org.apache.dubbo.rpc.Filter文件里配置一下:

ProviderTraceFilter=top.javap.dubbo.tracing.ProviderTraceFilter
ConsumerTraceFilter=top.javap.dubbo.tracing.ConsumerTraceFilter

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

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

相关文章

企业网络转型:优势与挑战

◎ 网络研究观 事实上&#xff0c;现代企业网络是一个由相互连接的数据、应用程序和基础设施组成的复杂网络。然而&#xff0c;企业不应让这种复杂性成为服务不可靠、安全漏洞或网络停机的借口。 由于组织和公司面临着从并购到云扩展的诸多挑战&#xff0c;以及网络技术日益复…

【算法一周目】双指针(1)

目录 1.双指针介绍 2.移动零 解题思路 C代码实现 3.复写零 解题思路 C代码实现 4.快乐数 解题思路 C代码实现 5.盛水最多的容器 解题思路 C代码实现 1.双指针介绍 常见的双指针有两种形式&#xff0c;一种是对撞指针&#xff0c;一种是快慢指针。 对撞指针&#x…

6547网:青少年软件编程Python等级考试(六级)真题试卷

2024年9月青少年软件编程Python等级考试&#xff08;六级&#xff09;真题试卷 题目总数&#xff1a;38 总分数&#xff1a;100 选择题 第 1 题 单选题 下面Python代码运行后出现的图像是&#xff1f;&#xff08; &#xff09; import matplotlib.pyplot as plt im…

【5种灵活有效方式】如何从死机手机中恢复内部数据?

本文介绍了5种方法来从死机的Android设备中恢复数据&#xff0c;包括使用U1tData安卓数据恢复软件、SD卡、OTG、Google云端硬盘和SamsungCloud。这些方法覆盖了不同情况下的数据恢复需求。 摘要由CSDN通过智能技术生成 我的手机掉在地上&#xff0c;现在无法开机。我丢失了所…

【安全测试】sqlmap工具(sql注入)学习

前言&#xff1a;sqimap是一个开源的渗透测试工具&#xff0c;它可以自动化检测和利用SQL注入缺陷以及接管数据库服务器的过程。它有一个强大的检测引擎&#xff0c;许多适合于终极渗透测试的小众特性和广泛的开关&#xff0c;从数据库指纹、从数据库获 取数据到访问底层文件系…

行业类别-智慧城市-子类别智能交通-细分类别自动驾驶技术-应用场景城市公共交通优化

1.大纲分析 针对题目“8.0 行业类别-智慧城市-子类别智能交通-细分类别自动驾驶技术-应用场景城市公共交通优化”的大纲分析&#xff0c;可以从以下几个方面进行展开&#xff1a; 一、引言 简述智慧城市的概念及其重要性。强调智能交通在智慧城市中的核心地位。引出自动驾驶…

24.11.11 JavaScript1

JavaScript&#xff08;简称js&#xff09;是⼀种描述语⾔&#xff0c;基于对象和事件驱动的脚本语⾔ JavaScript特点:脚本语⾔&#xff08;⼀种轻量级的编程语⾔&#xff09; ⼀种解释性语⾔&#xff08;⽆需预编译&#xff09; 被设计为向HTML⻚⾯添加交互⾏为 运⾏于客户端&…

PDF24:多功能 PDF 工具使用指南

PDF24&#xff1a;多功能 PDF 工具使用指南 在日常工作和学习中&#xff0c;PDF 是一种常见且重要的文档格式。无论是查看、编辑、合并&#xff0c;还是转换 PDF 文件&#xff0c;能够快速高效地处理 PDF 文档对于提高工作效率至关重要。PDF24 是一款免费、功能全面的 PDF 工具…

计算机的错误计算(一百五十一)

摘要 探讨 MATLAB 中反正弦 asin 与反余弦 acos 函数的计算精度问题。 例1. 已知 计算 及 直接贴图吧&#xff1a; 另外&#xff0c;16位的正确值分别为 0.1570785896071048e1、0.1043072384837152e-4、-0.1570785896071048e1 与 0.3141582222865945e1&#xff08;I…

Lua进阶用法之Lua和C的接口设计

一&#xff1a;lua/c的接口编程 首先skynet、openresty 都是深度使用 lua 语言的典范&#xff1b;学习 lua 不仅仅要学习基本用法&#xff0c;还要学会使用 c 与 lua 交互&#xff0c;这样才学会了 lua 作为胶水语言的精髓&#xff0c;下面看一下他们两个的调用过程。 虚拟栈&a…

macOS 下的 ARM 裸机嵌入式开发入门- 第二部分:实现第一个裸机应用并且调试

1、准备二进制运行程序镜像 利用 QEMU 仿真一个完整的系统&#xff0c;并创建最简单的“Hello world!”示例。 QEMU 模拟器支持 VersatilePB 平台&#xff0c;该平台包含一个 ARM926EJ-S 核心&#xff0c;以及其他外设&#xff0c;四个 UART 串行端口&#xff1b;特别是第一个…

【网络面试篇】其他面试题——Cookie、Session、DNS、CDN、SSL/TLS、加密概念

目录 一、HTTP 相关问题 1. Cookie 和 Session 是什么&#xff1f; &#xff08;1&#xff09;Cookie &#xff08;2&#xff09;Session 2. Cookie 的工作原理&#xff1f; 3. Session 的工作原理&#xff1f; 4. Cookie 和 Session 有什么区别&#xff1f; 二、其他问…

【数值分析】复习1---牛顿迭代法

首先&#xff0c;我们先来回顾一下牛顿迭代法的概念。 这里注意的是&#xff0c;牛顿迭代法是一种线性方法&#xff0c;它在点 x k x_k xk​处进行线性展开&#xff0c;而且展开成一阶泰勒公式&#xff01;注意是一阶&#xff0c;不是二阶&#xff0c;不是更高阶&#xff0c;所…

文本语义分块、RAG 系统的分块难题:小型语言模型如何找到最佳断点

文本语义分块、RAG 系统的分块难题&#xff1a;小型语言模型如何找到最佳断点&#xff1f; 转自jina最新的关于文本语义分块的分享和模型 之前我们聊过RAG 里文档分块 (Chunking) 的挑战&#xff0c;也介绍了 迟分 (Late Chunking) 的概念&#xff0c;它可以在向量化的时候减…

PostgreSQL中如果有Left Join的时候索引怎么加

在PostgreSQL中&#xff0c;当你的查询包含多个LEFT JOIN和WHERE条件时&#xff0c;合理地添加索引可以显著提高查询性能。以下是一些具体的优化步骤和建议&#xff1a; 1. 分析查询 使用 EXPLAIN ANALYZE 命令分析你的查询&#xff0c;了解查询的执行计划&#xff0c;识别出连…

温度虽寒,其道犹变:OpenAI接口之温度参数设置为0,为何每次回复仍有不确定性?

问题描述 调用openai API&#xff0c;使用templature 0&#xff0c;每次返回的内容仍有一些不同 >>> client OpenAI( ... api_keyapi_key, ... base_urlapi_base) #第一次尝试 >>> response client.chat.completions.create(mo…

vue-h5:在h5中实现相机拍照加上身份证人相框和国徽框

参考&#xff1a; https://blog.csdn.net/weixin_45148022/article/details/135696629 https://juejin.cn/post/7327353533618978842?searchId20241101133433B2BB37A081FD6A02DA60 https://www.freesion.com/article/67641324321/ https://github.com/AlexKratky/vue-camer…

国标GB28181视频平台EasyCVR私有化部署视频平台对接监控录像机NVR时,录像机“资源不足”是什么原因?

EasyCVR视频融合云平台&#xff0c;是TSINGSEE青犀视频“云边端”架构体系中的“云平台”系列之一&#xff0c;是一款针对大中型项目设计的跨区域、网络化、视频监控综合管理系统平台&#xff0c;通过接入视频监控设备及视频平台&#xff0c;实现视频数据的集中汇聚、融合管理、…

【Android、IOS、Flutter、鸿蒙、ReactNative 】标题栏

Android 标题栏 参考 Android Studio版本 配置gradle镜像 阿里云 Android使用 android:theme 显示标题栏 添加依赖 dependencies {implementation("androidx.appcompat:appcompat:1.6.1")implementation("com.google.android.material:material:1.9.0")…

歌尔微拟赴港IPO,揭示AI+终端升级的供给革命

1959年&#xff0c;美国物理学家理查德费曼在他著名的演讲“底部有足够的空间”中&#xff0c;首次提出了将机器小型化到原子和分子尺度的想法。这个充满想象力的观点&#xff0c;为世界科技发展开启了一扇新的窗口。 时至今日&#xff0c;应这一理念而生的MEMS产品已经成为各…