如今越来越多的互联网公司在架构上开始走向分布式,比如微服务,分布式数据库,分布式缓存等等。好处很明显,高扩展性,高可用,高性能。缺点也有比如分布式事务,分布式锁,数据一致性等等,而这些缺点的解决方案也是我们面试过程中很容易被问到的。
今天我们来谈一下在分布式架构中另一个问题,如何进行链路追踪。为什么需要实现这个功能?在业务繁杂的分布式中,服务间的调用可能是比较复杂的,如果前台应用调用服务失败,我们如何快速的定位是哪个服务造成的。寻常的日志排查就非常费时了。所以分布式调用链的作用就显现出来了。
分布式调用链其实就是将一次分布式请求还原成调用链路。显式的在后端查看一次分布式请求的调用情况,比如各个节点上的耗时、请求具体打到了哪台机器上、每个服务节点的请求状态等等。
许多大型互联网公司都拥有自己的分布式链路追踪系统,比如阿里的鹰眼,谷歌的Drapper,Twitter的ZipKin等等。
这里我们通过ZipKin来了解链路追踪。集成ZipKin应该是较为简单的,我们大概的看一下,然后通过分析slueth和zipkin的源码,了解其实现原理。
首先是下载ZipKin的服务应用,之前的版本可以通过SpringBoot集成ZipKin自己开启一个Zipkin的应用,但是现在可以直接下载jar包启动。(链接:https://pan.baidu.com/s/1XcZfnI3nF7X8tLVNz9ELwQ 密码:5ki9)
启动方式非常简单 java -jar 即可。
启动服务之后,http://127.0.0.1:9411可看到ZipKin的UI界面。这个时候界面是空的,因为还没有链路请求发送到这里。
现在我们要做的就是在我们微服务中集成Zipkin,将请求信息发送到这个服务上。集成很简单,我们只要导入下面依赖就OK。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
接下来就是服务调用了,这里我使用Eureka作为注册中心,服务间通讯使用Feign(公众号有专门的微服务SpringCloud模块,这里就不在赘叙了)。
@Autowired
为了测试成功和失败,我在homeWorkServer的getHomeWork2打了断点,使其调用超时,而getHomeWork正常。调用之后我们进入ZipKin的UI界面。上面两个是错误,最后一个是正常的。
我们点进去看一下:可以看到如下信息,服务耗时,持续时间,深度,Span总数(Span可以理解为用来描述一次RPC调用)
我们继续点开,会发现error的信息,描述了超时的问题是由于http://server-homework/getHomeWork导致的,也就是我们断点导致超时的异常。
到这里我们就完成了简单的集成,ZipKin服务的数据是存在内存当中的,我们也可以通过配置,与ES或者MySQL做数据的集成,这里我就不多说了,因为我也没有集成过。
ZipKin的原理是什么呢?这里我就自己集成方案来进行解析(自己研究的,可能不是很正确和完善)。
这篇文章的集成方案中,不仅仅是ZipKin,还有slueth,但是slueth是有ZipKin去引入的,slueth本身也是SpringCloud提供的服务治理组件之一,其功能就是生成分布式链路调用日志,但是目前仍以日志的形式输出,Zipkin结合了slueth使我们能以界面形式展现。
图:zipkin引入slueth
说到这里我们并没有说到链路追踪的实现原理,在我看源码之前我就在想方案很有可能就是:拦截器(Interceptor),过滤器(Filter),切面织入(AOP),实际上三个都用到了。下图中我们可以看到instrment这个包下就是拦截组件(Spring Cloud Sleuth可以追踪10种类型的组件对应图上10个包名),由于个人精力问题,这里我目前只是看了web下的部分源码。
图:slueth支持的组件
是不是文件有点多,我一开始点开的时候,头皮发麻,脑海里想的是:这不是玩我吗。但是不要怂,看目录我们先找到我们能猜出来是干嘛的文件(大佬教我的方案:看别人的源码,首先从文件名找到感觉自己能看懂的,然后继续追踪)。那下图中哪些我们能猜到是干嘛的?首先Filter后缀的,十有八九就是过滤器,Interceptor那应该就是拦截器了,AutoConfiguration后缀的配置文件可能性是跑不了的,Aspect后缀绝对是AOP的应用。Processsor,Enable这些我们就扔一边吧。。。。
既然知道实现离不开过滤器,拦截器,AOP这些,参考我们自己实现这些功能的过程,我们可以想到有些必须的组件,比如Springboot中实现拦截器肯定要有WebMvcConfigure的实现类,或者WebMvcConfigurationSupport的子类,于是我就盯上了TraceWebMvcConfigurer,看着最像,打开源码(如下),果然是拦截器的配置。
package org.springframework.cloud.sleuth.instrument.web;
import brave.spring.webmvc.SpanCustomizingAsyncHandlerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Import({SpanCustomizingAsyncHandlerInterceptor.class})
class TraceWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
ApplicationContext applicationContext;
TraceWebMvcConfigurer() {
}
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor((HandlerInterceptor)this.applicationContext.getBean(SpanCustomizingAsyncHandlerInterceptor.class));
}
}
知道配置了拦截器,那么我就继续往下追踪SpanCustomizingAsyncHandlerInterceptor这个继承HandlerInterceptorAdapter的类了,我们可以发现getAttribute这个方法来获取SpanCustomizer。如果获取到就会填充SpanCustomizer的tag参数。
package brave.spring.webmvc;
import brave.SpanCustomizer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public final class SpanCustomizingAsyncHandlerInterceptor extends HandlerInterceptorAdapter {
@Autowired(
required = false
)
HandlerParser handlerParser = new HandlerParser();
SpanCustomizingAsyncHandlerInterceptor() {
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) {
SpanCustomizer span = (SpanCustomizer)request.getAttribute(SpanCustomizer.class.getName());
if (span != null) {
this.handlerParser.preHandle(request, o, span);
}
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
SpanCustomizer span = (SpanCustomizer)request.getAttribute(SpanCustomizer.class.getName());
if (span != null) {
SpanCustomizingHandlerInterceptor.setHttpRouteAttribute(request);
}
}
}
其次就是Filter,如TraceWebFilter。个人觉得Filter的作用应该在拦截器之上,因为SpingBoot项目中,如果我们自定义了拦截器,那么导入拦截器将不会生效,这个本人经过验证,在该项目中,当我实现自定义的拦截器的时候,slueth拦截器断点并不会进入,而放弃自定义拦截器或者采用继承WebMvcConfigure(我通常采用继承WebMvcConfigurationSupport实现拦截器)才会走进slueth的断点,但是无论slueth断点是否生效,ZipKin的数据仍然是全面的。
然后就是切面了,我们看下图,它拦截了RestController,Controller等注解,实现调用织入。
最后就是数据如何发送到ZipKin了。LoadBalancerClientZipkinLoadBalancer下有一个instance方法其作用就是发送数据到服务端,而getBaseUrl方法返回的就是服务的URL了,默认:http://localhost:9411
关于ZipKin与slueth本人目前也是学习到这里,自己给自己的定位是,大概理解实现原理的外壳,但是其核心并没有完全搞懂,很多困惑仍然没有得到答案,所以分享只能到这里结束,在后续将会继续分享通过源码学习到相关知识,也欢迎大家加群讨论。
关于分布式链路追踪的设计一般由如下几点(如果自己实现的话):
1.埋点日志:埋点即系统在当前节点的上下文信息,埋点日志通常要包含:
TraceId、RPCId、调用的开始时间,调用类型,协议类型,调用方ip和端口,请求的服务名等信息; 调用耗时,调用结果,异常信息,消息报文等; 预留可扩展字段,为下一步扩展做准备
2.然后是代码的侵入性要低,服务透明,低损耗,扩展性。
3.抓取和存储日志
4.分析和统计调用链数据
5.计算和展示