介绍分布式链路的思想
一、基本概念
1、Span:Span是链路追踪中的基本工作单元。当一个远程调度任务(如RPC调用)发生时,会产生一个Span。Span通过一个64位ID进行唯一标识,并且包含其他数据信息,如摘要、时间戳事件、关键值注释(tags)、Span的ID以及进度ID(通常是IP地址)。Span会不断地启动和停止,并记录时间信息。
2、Trace:Trace是由一系列Span组成的一个树状结构。它表示对一次外部请求的追踪。一次外部请求可能由多次内部服务调用(即多个Span)组成。这些Span之间有顺序和层级关系,共同构成了一个完整的请求链路。
3、Annotation:Annotation用于及时记录一个事件的存在。一些核心Annotation用于定义一个请求的开始和结束。常见的Annotation包括:
- cs(Client Sent):客户端发起一个请求,这个Annotation描述了这个Span的开始。
- sr(Server Received):服务端收到请求并开始处理它。
- ss(Server Sent):表明请求处理的完成(当请求返回客户端)。
- cr(Client Received):表明Span的结束,客户端成功接收到服务端的回复。
二、工作原理
1、拦截请求:Spring Cloud Sleuth通过拦截请求的方式,在日志中加入额外的Span和Trace的相关信息。这些信息对于追踪请求链路至关重要。
2、传播上下文:Sleuth支持进程之间的上下文传播。它可以在Span上设置添加额外的信息,并通过HTTP或其他消息传递机制给其他进程传递这些信息。例如,在HTTP请求中,Sleuth会在请求头中增加实现跟踪的信息,如x-b3-spanid、x-b3-parentspanid、x-b3-traceid等。
3、生成和上报数据:对于每个被拦截的请求,Sleuth会生成相应的Span和Trace信息,并将这些信息上报给Zipkin(如果需要的话)。Zipkin是一个分布式跟踪系统,它可以收集、存储和展示这些链路追踪数据。
4、日志记录:Sleuth还会将链路追踪数据记录到日志中。这些日志信息包含了Span和Trace的详细信息,以及请求的处理过程和时间戳等。
三、集成与配置
1、集成Zipkin:Spring Cloud Sleuth可以很容易地与Zipkin集成。通过配置spring-cloud-starter-zipkin依赖,Sleuth可以将追踪数据上报给Zipkin Server。Zipkin Server负责存储这些数据,并提供一个Web界面来展示和分析这些链路追踪信息。
2、配置采样率:为了减少日志采集输出对应用性能的影响,Sleuth支持采样率配置。通过配置采样率,可以控制哪些请求被追踪和记录。
四、应用场景
Spring Cloud Sleuth在微服务架构中具有广泛的应用场景。它可以帮助开发人员快速发现错误根源、监控分析每条请求链路上的请求性能以及优化系统架构。通过链路追踪信息,开发人员可以清晰地看到一次请求经过了哪些服务、服务的响应状态、服务间的调用顺序和响应时长等信息。这对于定位问题和优化系统性能非常有帮助。
Spring Cloud Sleuth通过拦截请求、传播上下文、生成和上报数据以及日志记录等方式实现了分布式系统中的链路追踪功能。
实战
为什么要用链路追踪?
微服务架构下,一个复杂的电商应用,完成下单可能依赖商品、库存、结算、风控等一系列服务,并且依赖的服务又依赖一堆服务,其中任何一个环节出错都可能导致服务调用失败。 一旦服务调用失败,对于问题排查的成本是非常高的。
如何自己实现简单的链路追踪?
我们可以按照下面流程实现一个简单的链路追踪,当然不包含链路上报和检索功能。
以常见的 Dubbo 和 Spring MVC 请求举例,实现将请求Trace Id传递下去,并且满足如下特征
- 对业务代码无入侵
- 业务代码可以通过API方式操作Trace Id
Dubbo
我们可以通过Dubbo的attachment 实现Trace Id 透传,实现对业务代码无入侵。 同时为了业务代码获取方便,我们将TraceId 放入ThreadLocal中,这样业务代码可以通过ThreadLocal获取,而不必依赖Dubbo 的 RpcContext 。
java
String traceId = RpcContext.getContext().getAttachment("traceId");
if(traceId == null){
traceId = UUID.randomUUID().toString().replace("-","");
RpcContext.getContext().setAttachment("traceId",traceId);
}
TraceUtil.setTraceId(traceId);
public class TraceUtil {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static String getTraceId(){
return threadLocal.get();
}
public static void setTraceId(String traceId){
threadLocal.set(traceId);
}
public static void remove(){
threadLocal.remove();
}
}
Spring MVC
Spring MVC 有自己的拦截器,我们也可以直接使用 servlet-api 中Filter 。同样的,我们将Trace Id 放入 ThreadLocal ,这样后面业务就可以通过ThreadLocal 操作Trace Id。
java
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String traceId = request.getHeader("traceId");
if(traceId == null) {
traceId = UUID.randomUUID().toString().replace("-","");
}
TraceUtil.setTraceId(traceId);
try{
filterChain.doFilter(servletRequest,servletResponse);
}finally {
TraceUtil.remove();
}
}
}
上面实现比较粗糙,信息也不够全,还存在异步方法链路无法透传问题,当然更重要的缺乏链路查询的支持。
Spring Cloud Sleuth
Spring Cloud Sleuth 是一个分布式追踪的组件,我们无需再重复造轮子。
相关概念
-
Trace ID: 整个调用链全局唯一的Id,无论经过多少个服务,整个调用链中 TraceId都相同
-
Span ID: 标识一次具体的操作或服务调用,同样全局唯一。
-
Parent ID: 记录当前服务调用发起方的 Span Id。
一个复杂的微服务调用,可能出现服务的层层嵌套,通过Parent ID ,可以梳理整个调用链的上下游关系。
如图服务A 调用服务B ,服务B 又调用了服务C ,TraceId 对于整个调用链是不变的。 各种的服务都有自己的Span ID ,Parent ID记录了其直接调用方服务对应的Span ID 。
Zipkin
Sleuth可以实现服务调用的链路透传, 如果需要实现链路检索功能,可以使用Zipkin,Zipkin核心功能日志收集和链路检索,Zipkin 具有可视化页面。
Zipkin 安装
如果您使用的是 Java 17或者更高版本,可以通过编译源码方式生成 zipkin-server-*exec.jar
bash
java -jar ./zipkin-server/target/zipkin-server-*exec.jar
Docker 安装方式
docker 方式一个命令即可,启动成功输入 http://{ip}:9411/,即可进入Web 页面
bash
docker run -d -p 9411:9411 openzipkin/zipkin
Spring Cloud Sleuth 简单Demo
Server 作为SpringBoot 启动类,同时提供了/hello http 接口。
Client 作为SpringBoot 启动类,提供/callServer接口,其内部通过restTemplate调用 Server/hello接口。
java
@EnableAutoConfiguration
@RestController
@Slf4j
public class Server {
private final Tracer tracer;
public Server(Tracer tracer) {
this.tracer = tracer;
}
@RequestMapping("/hello")
public String hello() {
log.info("Server hello is called ");
// Span currentSpan = tracer.currentSpan();
// if (currentSpan != null) {
// currentSpan.tag("custom-tag", "value");
// currentSpan.annotate("Custom Event");
// }
return new Date().toString();
}
public static void main(String[] args) {
SpringApplication.run(Server.class,
"--spring.application.name=server",
"--server.port=9000"
);
}
}
@EnableAutoConfiguration
@RestController
@Slf4j
public class Client {
@Autowired
RestTemplate restTemplate;
String backendBaseUrl = "http://localhost:9000";
@RequestMapping("/callServer") public String callServer() {
log.info("callServer {}",new Date());
return restTemplate.getForObject(backendBaseUrl + "/hello", String.class);
}
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Client.class,
"--spring.application.name=client",
"--server.port=8081"
);
}
}
logging.level.org.springframework.web=DEBUG spring.sleuth.traceId128=true spring.sleuth.sampler.probability=1.0 # Adds trace and span IDs to logs (when a trace is in progress) logging.pattern.level=[%X{traceId}/%X{spanId}] %-5p [%t] %C{2} - %m%n spring.application.name=sleuth-service spring.zipkin.base-url=http://localhost:9411
效果图
zipkin 控制台可以看到相关日志,我们自己打印的日志并没有TraceId 相关信息
业务日志增加TraceId 信息
很自然想到的方式通过API方式获取Trace ID相关信息,但这种相对比繁琐,业务代码有一定入侵,其实我们可以配置logback-spring.xml 控制日志格式,配置后再看看效果,我们自己的业务日志也包含Trace ID 等信息了。
API 获取链路信息简单例子
Span currentSpan = tracer.currentSpan();
System.out.println(currentSpan.context().traceIdString());
System.out.println(currentSpan.context().spanIdString());
logback-spring.xml
xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - traceId=%X{traceId:-}, spanId=%X{spanId:-} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>