分布式系统中日志追踪需要考虑的几个点?

  1. 需要一个全服务唯一的id,即traceId,如何保证?
  2. traceId如何在服务间传递?
  3. traceId如何在服务内部传递?
  4. traceId如何在多线程中传递?

我们一一来解答:

  1. 全服务唯一的traceId,可以使用uuid生成,正常来说不会出现重复的;
  2. 关于服务间传递,对于调用者,在协议头加上traceId,对于被调用者,通过前置拦截器或者过滤器统一拦截;
  3. 关于服务内部传递,可以使用ThreadLocal传递traceId,一处放置,随处可用;
  4. 关于多线程传递,分为两种情况:子线程,可以使用InheritableThreadLocal线程池,需要改造线程池对提交的任务进行包装,把提交者的traceId包装到任务中

比如,上面这个系统,系统入口在A处,A调用B的服务,B里面又起了一个线程B1去访问D的服务,B本身又去访问C服务。

我们就可以这么来跟踪日志:

  1. 所有服务都需要一个全局的InheritableThreadLocal保存服务内部traceId的传递;
  2. 所有服务都需要一个前置拦截器或者过滤器,检测如果请求头没有traceId就生成一个,如果有就取出来,并把traceId放到全局的InheritableThreadLocal里面;
  3. 一个服务调用另一个服务的时候把traceId塞到请求头里,比如http header,本文来源于工从号彤哥读源码;
  4. 改造线程池,在提交的时候包装任务,这个工作量比较大,因为服务内部可能依赖其它框架,这些框架的线程池有可能也需要修改;

实现

我们模拟A到B这两个服务来实现一个日志跟踪系统。

为了简单起见,我们使用SpringBoot,它默认使用的日志框架是logback,而且Slf4j提供了一个包装了InheritableThreadLocal的类叫MDC,我们只要把traceId放在MDC中,打印日志的时候统一打印就可以了,不用显式地打印traceId。

我们分成三个模块:

  1. 公共包:封装拦截器,traceId的生成,服务内传递,请求头的传递等;
  2. A服务:只依赖于公共包,并提供一个接口接收外部请求;
  3. B服务:依赖于公共包,并内部起一个线程池,用于发送B1->D的请求,当然我们这里不发送请求,只在线程池中简单地打印一条日志;

公共包

  1. TraceFilter.java

前置过滤器,用拦截器实现也是一样的。

从请求头中获取traceId,如果不存在就生成一个,并放入MDC中。

@Slf4j
@WebFilter("/**")
@Component
public class TraceFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;// 从请求头中获取traceIdString traceId = request.getHeader("traceId");// 不存在就生成一个if (traceId == null || "".equals(traceId)) {traceId = UUID.randomUUID().toString();}// 放入MDC中,本文来源于工从号彤哥读源码MDC.put("traceId", traceId);chain.doFilter(servletRequest, servletResponse);}@Overridepublic void destroy() {}
}

  1. TraceThreadPoolExecutor.java

改造线程池,提交任务的时候进行包装。

public class TraceThreadPoolExecutor extends ThreadPoolExecutor {public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);}public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<runnable> workQueue, ThreadFactory threadFactory) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);}public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<runnable> workQueue, RejectedExecutionHandler handler) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);}public TraceThreadPoolExecutor(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 command) {// 提交者的本地变量Map<string, string> contextMap = MDC.getCopyOfContextMap();super.execute(()->{if (contextMap != null) {// 如果提交者有本地变量,任务执行之前放入当前任务所在的线程的本地变量中MDC.setContextMap(contextMap);}try {command.run();} finally {// 任务执行完,清除本地变量,以防对后续任务有影响MDC.clear();}});}
}

  1. TraceAsyncConfigurer.java

改造Spring的异步线程池,包装提交的任务。

@Slf4j
@Component
public class TraceAsyncConfigurer implements AsyncConfigurer {@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(8);executor.setMaxPoolSize(16);executor.setQueueCapacity(100);executor.setThreadNamePrefix("async-pool-");executor.setTaskDecorator(new MdcTaskDecorator());executor.setWaitForTasksToCompleteOnShutdown(true);executor.initialize();return executor;}@Overridepublic AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {return (throwable, method, params) -> log.error("asyc execute error, method={}, params={}", method.getName(), Arrays.toString(params));}public static class MdcTaskDecorator implements TaskDecorator {@Overridepublic Runnable decorate(Runnable runnable) {Map<string, string> contextMap = MDC.getCopyOfContextMap();return () -> {if (contextMap != null) {MDC.setContextMap(contextMap);}try {runnable.run();} finally {MDC.clear();}};}}}

  1. HttpUtils.java

封装Http工具类,把traceId加入头中,带到下一个服务。

@Slf4j
public class HttpUtils {public static String get(String url) throws URISyntaxException {RestTemplate restTemplate = new RestTemplate();MultiValueMap<string, string> headers = new HttpHeaders();headers.add("traceId", MDC.get("traceId"));URI uri = new URI(url);RequestEntity<!--?--> requestEntity = new RequestEntity<>(headers, HttpMethod.GET, uri);ResponseEntity<string> exchange = restTemplate.exchange(requestEntity, String.class);if (exchange.getStatusCode().equals(HttpStatus.OK)) {log.info("send http request success");}return exchange.getBody();}}

A服务

A服务通过Http调用B服务。

@Slf4j
@RestController
public class AController {@RequestMapping("a")public String a(String name) {log.info("Hello, " + name);try {// A中调用Breturn HttpUtils.get("http://localhost:8002/b");} catch (Exception e) {log.error("call b error", e);}return "fail";}
}

A服务的日志输出格式:

中间加了[%X{traceId}]一串表示输出traceId。

# 本文来源于工从号彤哥读源码
logging:pattern:console: '%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr([%X{traceId}]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx'

B服务

B服务内部有两种跨线程调用:

  • 利用Spring的异步线程池
  • 使用自己的线程池

BController.java

@Slf4j
@RestController
public class BController {@Autowiredprivate BService bService;@RequestMapping("b")public String b() {log.info("Hello, b receive request from a");bService.sendMsgBySpring();bService.sendMsgByThreadPool();return "ok";}
}

BService.java

@Slf4j
@Service
public class BService {public static final TraceThreadPoolExecutor threadPool = new TraceThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));@Asyncpublic void sendMsgBySpring() {log.info("send msg by spring success");}public void sendMsgByThreadPool() {threadPool.execute(()->log.info("send msg by thread pool success"));}
}

B服务的日志输出格式:

中间加了[%X{traceId}]一串表示输出traceId。

logging:pattern:console: '%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr([%X{traceId}]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx'

测试

打开浏览器,输入http://localhost:8001/a?name=andy。

A服务输出日志:

2019-12-26 21:36:29.132  INFO 5132 --- [nio-8001-exec-2] [8a59cb96-bbc8-42a9-aa62-df7a52875447] com.alan.trace.a.AController             : Hello, andy
2019-12-26 21:36:35.380  INFO 5132 --- [nio-8001-exec-2] [8a59cb96-bbc8-42a9-aa62-df7a52875447] com.alan.trace.common.HttpUtils          : send http request success

B服务输出日志:

2019-12-26 21:36:29.244  INFO 2368 --- [nio-8002-exec-1] [8a59cb96-bbc8-42a9-aa62-df7a52875447] com.alan.trace.b.BController             : Hello, b receive request from a
2019-12-26 21:36:29.247  INFO 2368 --- [nio-8002-exec-1] [8a59cb96-bbc8-42a9-aa62-df7a52875447] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService
2019-12-26 21:36:35.279  INFO 2368 --- [   async-pool-1] [8a59cb96-bbc8-42a9-aa62-df7a52875447] com.alan.trace.b.BService                : send msg by spring success
2019-12-26 21:36:35.283  INFO 2368 --- [pool-1-thread-1] [8a59cb96-bbc8-42a9-aa62-df7a52875447] com.alan.trace.b.BService                : send msg by thread pool success

可以看到,A服务成功生成了traceId,并且传递给了B服务,且B服务线程间可以保证同一个请求的traceId是可以传递的。

文章来源:https://my.oschina.net/u/4108008/blog/3152201

关注我了解更多程序员资讯技术,领取丰富架构资料。

拦截器如何获取@requestbody_分布式系统中如何优雅地追踪日志(原理篇)相关推荐

  1. java任务追踪预警怎么写_分布式系统中如何优雅地追踪日志(原理篇)

    本文只讲原理,不讲框架. 分布式系统中日志追踪需要考虑的几个点? 需要一个全服务唯一的id,即traceId,如何保证? traceId如何在服务间传递? traceId如何在服务内部传递? trac ...

  2. 拦截器获取请求参数post_SpringBoot拦截器如何获取http请求参数

    1.1.获取http请求参数是一种刚需 我想有的小伙伴肯定有过获取http请求的需要,比如想 前置获取参数,统计请求数据 做服务的接口签名校验 敏感接口监控日志 敏感接口防重复提交 等等各式各样的场景 ...

  3. springboot拦截请求路径_SpringBoot拦截器如何获取http请求参数

    1.1.获取http请求参数是一种刚需 我想有的小伙伴肯定有过获取http请求的需要,比如想 前置获取参数,统计请求数据 做服务的接口签名校验 敏感接口监控日志 敏感接口防重复提交 等等各式各样的场景 ...

  4. android 方法拦截器,Android的OkHttp包中的HTTP拦截器Interceptor用法示例

    OkHttp(GitHub:https://github.com/square/okhttp) 的 Interceptor 就如同名称「拦截器」一样,拦截你的 Request 做一些你想做的事情再送出 ...

  5. struts拦截器+注解实现网络安全要求中的日志审计功能

    J2EE项目中出于安全的角度考虑,用户行为审计日志功能必不可少,通过本demo可以实现如下功能: 1.项目中记录审计日志的方法. 2.struts拦截器的基本配置和使用方法. 3.struts拦截器中 ...

  6. Java调用跟踪系统_Tracer:在分布式系统中的调用跟踪和日志相关

    Tracer: Distributed system tracing Tracer noun, /ˈtɹeɪsɚ/: A round of ammunition that contains a fla ...

  7. spring boot 拦截器获取controller返回的数据_高级码农Spring Boot实战与进阶之过滤器和拦截器的使用及其区别...

    众所周知的Spring Boot是很优秀的框架,它的出现简化了新Spring应用的初始搭建以及开发过程,大大减少了代码量,目前已被大多数企业认可和使用.这个专栏将对Spring Boot框架从浅入深, ...

  8. Java中的过滤器和拦截器

    一.简介 1.什么是拦截器? (1)在AOP中用于在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作.拦截是AOP的一种实现策略. (2)拦截器是动态拦截Action调用的对象.它提供了 ...

  9. 框架:Spring Aop、拦截器、过滤器的区别

    Spring Aop.拦截器.过滤器的区别 Filter过滤器:拦截web访问的url地址. Interceptor拦截器:拦截以.action结尾的url,拦截Action的访问. Spring A ...

最新文章

  1. linux ssh免密登陆远程服务器
  2. elasticSearch6源码分析(7)node
  3. 【数据竞赛】Kaggle竞赛宝典国内外竞赛方案汇总
  4. 西门子逻辑运算指令_西门子S7-300PLC逻辑运算指令
  5. 八款JS框架介绍及比较
  6. Python系列之入门篇——HDFS
  7. python 批量打印文档_使用python将Excel数据填充Word模板并生成Word
  8. NASA 美国国家航空航天局开源项目列表
  9. KingDZ 变菜鸟,每日一个C#小实例之---C#MessageBox小技巧
  10. java int stack_java stack总结
  11. 激光打标机金橙子软件画出五角星最简单方法图解
  12. 如何在线免费caj转word
  13. 删除文件时出现找不到该项目 请确认该项目位置 怎么办【转】
  14. VB+ADO+Access如何修改数据
  15. Tensor.shape[0]的理解
  16. MarkDown基本语法--程序员必修
  17. 环境参数智能监测站设计(说明书篇)
  18. 网页播放器看视频页面绿屏解决方法
  19. 微信小程序:强大工具箱组合源码
  20. 【sql查询】使用sql查询一个物品是否在有效期内的方法(数据库无这个字段 通过生产日期和保质期进行计算得出)

热门文章

  1. unity5.x C# 获取屏幕宽度 设置不受重力影响
  2. db2和mysql语句区别_db2和mysql语法的区别是什么
  3. php的_auto,AutoPHPCheck下载
  4. arduino代码_纯纯小白开发arduino--我的调试经验
  5. 晚上我们一起去白码会所玩啊!
  6. 菜鸟学习数据科学家 5 大误区
  7. 一个女程序员征男友的需求说明书
  8. 卸载wrapt_[python] 安装TensorFlow问题 解决Cannot uninstall 'wrapt'. It is a distutils installed project...
  9. 抽屉远离在计算机的应用,抽屉原理的应用及其推广优秀毕业论文
  10. html怎么做交互留言,简单html与servlet交互