25 | 分布式Trace:横跨几十个分布式组件的慢请求要如何排查?

经过前面几节课的学习,你的垂直电商系统在引入 RPC 框架和注册中心之后已经完成基本的服务化拆分了,系统架构也有了改变:

img

现在,你的系统运行平稳,老板很高兴,你也安心了很多。而且你认为,在经过了服务化拆分之后,服务的可扩展性增强了很多,可以通过横向扩展服务节点的方式平滑地扩容了,对于应对峰值流量也更有信心了。

但是这时出现了问题:你通过监控发现,系统的核心下单接口在晚高峰的时候,会有少量的慢请求,用户也投诉在 APP 上下单时,等待的时间比较长。而下单的过程可能会调用多个 RPC 服务或者使用多个资源,一时之间,你很难快速判断究竟是哪个服务或者资源出了问题,从而导致整体流程变慢。于是你和你的团队开始想办法如何排查这个问题。

一体化架构中的慢请求排查如何做

因为在分布式环境下,请求要在多个服务之间调用,所以对于慢请求问题的排查会更困难,我们不妨从简单的入手,先看看在一体化架构中是如何排查这个慢请求的问题的。

最简单的思路是:打印下单操作的每一个步骤的耗时情况,然后通过比较这些耗时的数据,找到延迟最高的一步,然后再来看看这个步骤要如何优化。如果有必要的话,你还需要针对步骤中的子步骤,再增加日志来继续排查,简单的代码就像下面这样:



long start = System.currentTimeMillis();
processA();
Logs.info("process A cost " + (System.currentTimeMillis() - start));//打印A步骤的耗时
start = System.currentTimeMillis();
processB();
Logs.info("process B cost " + (System.currentTimeMillis() - start));//打印B步骤的耗时
start = System.currentTimeMillis();
processC();
Logs.info("process C cost " + (System.currentTimeMillis() - start));//打印C步骤的耗时

这是最简单的实现方式,打印出日志后,我们可以登录到机器上搜索关键词来查看每个步骤的耗时情况。

虽然这个方式比较简单,但你可能很快就会遇到问题:由于同时会有多个下单请求并行处理,所以,这些下单请求的每个步骤的耗时日志是相互穿插打印的。你无法知道这些日志哪些是来自于同一个请求,也就不能很直观地看到某一次请求耗时最多的步骤是哪一步了。那么你要如何把单次请求,每个步骤的耗时情况串起来呢?

一个简单的思路是:给同一个请求的每一行日志增加一个相同的标记。这样,只要拿到这个标记就可以查询到这个请求链路上所有步骤的耗时了,我们把这个标记叫做 requestId,我们可以在程序的入口处生成一个 requestId,然后把它放在线程的上下文中,这样就可以在需要时随时从线程上下文中获取到 requestId 了。

有了 requestId,你就可以清晰地了解一个调用链路上的耗时分布情况了。

于是,你给你的代码增加了大量的日志来排查下单操作缓慢的问题。很快, 你发现是某一个数据库查询慢了才导致了下单缓慢,然后你优化了数据库索引,问题最终得到了解决。

一般来说,切面编程的实现分为两类:

  • 一类是静态代理,典型的代表是 AspectJ,它的特点是在编译期做切面代码注入;
  • 另一类是动态代理,典型的代表是 Spring AOP,它的特点是在运行期做切面代码注入。

这两者有什么差别呢?以 Java 为例,源代码 Java 文件先被 Java 编译器编译成 Class 文件,然后 Java 虚拟机将 Class 装载进来之后,进行必要的验证和初始化后就可以运行了。

静态代理是在编译期插入代码,增加了编译的时间,给你的直观感觉就是启动的时间变长了,但是一旦在编译期插入代码完毕之后在运行期就基本对于性能没有影响。

而动态代理不会去修改生成的 Class 文件,而是会在运行期生成一个代理对象,这个代理对象对源对象做了字节码增强,来完成切面所要执行的操作。由于在运行期需要生成代理对象,所以动态代理的性能要比静态代理要差。

如何来做分布式 Trace

你可能会问:题目既然是“分布式 Trace:横跨几十个分布式组件的慢请求要如何排查?”,那么我为什么要花费大量的篇幅来说明在一体化架构中如何排查问题呢?这是因为在分布式环境下,你基本上也是依据上面我提到的这几点来构建分布式追踪的中间件的。

在一体化架构中,单次请求的所有的耗时日志都被记录在一台服务器上,而在微服务的场景下,单次请求可能跨越多个 RPC 服务,这就造成了单次的请求的日志会分布在多个服务器上。

那么 spanId 是何时生成的,又是如何传递的呢?这部分内容可以算作一个延伸点,能够帮你了解分布式 Trace 中间件的实现原理。

img

我来给你讲讲图中的内容:

  1. 用户到 A 服务之后会初始化一个 traceId 为 100,spanId 为 1;
  2. A 服务调用 B 服务时,traceId 不变,而 spanId 用 1.1 标识代表上一级的 spanId 是 1,这一级的调用次序是 1;
  3. A 调用 C 服务时,traceId 依然不变,spanId 则变为了 1.2,代表上一级的 spanId 还是 1,而调用次序则变成了 2,以此类推。

通过这种方式,我们可以在日志中清晰地看出服务的调用关系是如何的,方便在后续计算中调整日志顺序,打印出完整的调用链路。

那么 spanId 是何时生成的,又是如何传递的呢?这部分内容可以算作一个延伸点,能够帮你了解分布式 Trace 中间件的实现原理。

而在这里,你大概率会遇到的问题还是性能的问题,也就是因为引入了分布式追踪中间件,导致对于磁盘 I/O 和网络 I/O 的影响,而我给你的“避坑”指南就是:如果你是自研的分布式 Trace 中间件,那么一定要提供一个开关,方便在线上随时将日志打印关闭;如果使用开源的组件,可以开始设置一个较低的日志采样率,观察系统性能情况再调整到一个合适的数值。

你可能感兴趣的:(25 | 分布式Trace:横跨几十个分布式组件的慢请求要如何排查?)