Sleuth 入门教程 - 中级篇

大纲

Spring Cloud 与 SkyWalking

SkyWalking 介绍

SkyWalking 的概述

SkyWalking 创建于 2015 年,用于提供分布式追踪功能。从 5.x 开始,项目进化为一个完成功能的 APM 系统,被用于追踪、监控和诊断分布式系统,特别是使用微服务架构、云原生或容器技术。更多介绍可参考:SkyWalking 官网、SkyWalking GitHub 项目。

SkyWalking 的功能

SkyWalking 提供的主要功能如下:

  • 分布式追踪和上下文传输
  • 应用、实例、服务性能指标分析
  • 根源分析
  • 应用拓扑分析
  • 应用和服务依赖分析
  • 慢服务检测
  • 性能优化

SkyWalking 的特性

SkyWalking 主要特性如下:

  • 多语言探针或者类库
    • Java 自动探针,追踪和监控程序时,不需要修改源码
    • 社区提供的语言探针:.NET Core、Node.js
  • 多种后端存储
    • 支持 Elastic Search、H2
    • 支持 OpenTrancing:Java 自动探针和 OpenTracing API 协同工作。
  • 轻量级、完善的后端聚合和分析功能
  • 现代化 WebUI
  • 日志集成
  • 应用、实例和服务的告警
  • 支持接受其他跟踪器的数据格式:
    • Zipkin JSON、Thrift、Protobuf v1 和 v2 格式,由 OpenZipkin 库提供支持
    • Jaeger 采用 Zipkin Thrift 或 JSON v1/v2 格式

SkyWalking 整体架构

SkyWalking 的整体架构主要由四部分组成 Collector、Agent、Web、Storage,具体如下图所示。从上到下是应用级别的接入,可以使用 SDK 形式的接入,也使用非入侵性的 Agent 形式接入,Agent 负载将数据转化成 SkyWalking Trace 数据协议,通过 HTTP 或者是 gRPC 发送到 Collector,然后 Collector 对收集到的数据进行分析和聚合,最后存储到 ElasticSearch 或者 H2 中,一般情况下 H2 只用于测试,右边的 UI 是通过 HTTP + GrahQL 进行数据获取展示,大体的流程就是这样。

SkyWalking 实战案例

本节将演示 Spring Cloud 与 SkyWalking 的实战案例,该案例使用了两个核心服务(Consumer Service、Provider Service)、Zuul 网关、Eureka 作为注册中心。案例的调用流程是这样的,首先通过访问 Zuul 网关,然后再将请求转发到 Consumer Service,接着 Consumer Service 通过 Feign 远程调用 Provider Service,最后返回响应结果。具体的调用流程图如下所示:

1. 版本说明

在本案例中,使用各组件的版本如下所示:

组件版本
Spring Boot2.0.3
Spring CloudFinchley.RELEASE
ElasticSearch5.6.10
SkyWalking5.0.0-GA

2. 安装与启动 SkyWalking

3. 创建 Maven 父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>

<!-- 利用传递依赖,公共部分 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<!-- 管理依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<!--注意:这里需要添加以下配置,否则可能会有各种依赖问题 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

4. 创建 Eureka 工程

创建 Eureka Server 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-netflix-eureka-server

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>

创建 Eureka Server 的启动主类,这里添加相应注解,作为程序的入口:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

public static void main(String[] args){
SpringApplication.run(EurekaServerApplication.class, args);
}
}

添加 Eureka Server 需要的 application.yml 配置文件到工程中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8090

eureka:
instance:
hostname: 127.0.0.1
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
waitTimeInMsWhenSyncEmpty: 0
enableSelfPreservation: false

5. 创建 Zuul 工程

创建 Zuul 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-netflix-zuul。由于需要将服务注册到 Eureka Server,工程下的 pom.xml 文件还需要引入 spring-cloud-starter-netflix-eureka-client。

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>

创建 Zuul 的启动主类,添加 @EnableDiscoveryClient 和 @EnableZuulProxy 注解,作为程序的入口:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ZuulApplication {

public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}

}

提示

在主启动类上面的 @EnableDiscoveryClient 和 @EnableZuulProxy 注解,主要用于开启服务发现和 Zuul 代理转发的功能。

添加 Zuul 需要的 application.yml 配置文件到工程中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
server:
port: 7070

spring:
application:
name: zuul-service

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

zuul:
routes:
service-consumer:
path: /client/**
serviceId: consumer-service

ribbon:
eureka:
enabled: true
ReadTimeout: 30000
ConnectTimeout: 30000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1
OkToRetryOnAllOperations: false

hystrix:
threadpool:
default:
coreSize: 1000 # 并发执行的最大线程数,默认 10
maxQueueSize: 1000 # BlockingQueue 的最大队列数
queueSizeRejectionThreshold: 500 # 即使 maxQueueSize 没有达到,达到 queueSizeRejectionThreshold 该值后,请求也会被拒绝
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 120001

提示

在上述配置内容中,添加了 Ribbon 和 Hystrix 相关配置是为了在首次请求时不会出现超时现象。

6. 创建 Provider 工程

为了测试 Feign 的 Web 服务客户端的功能,必须要有一个服务提供者。创建 Provider 的 Maven 工程后,由于需要将服务注册到 Eureka Server,工程下的 pom.xml 文件需要引入 spring-cloud-starter-netflix-eureka-client

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

创建 Provider 的启动主类,添加注解 @EnableDiscoveryClient,将服务注册到 Eureka Server:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}

}

在 application.yml 文件中指定服务名称(provider-service)、注册中心地址与端口号:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 9090

spring:
application:
name: provider-service

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

创建用于测试的 Controller 类:

1
2
3
4
5
6
7
8
9
@RestController
public class ProviderController {

@RequestMapping(value = "/getSendInfo", method = RequestMethod.GET)
public String getSendInfo(@RequestParam("serviceName") String serviceName) {
return serviceName + " ---> " + "Provider-Service";
}

}

7. 创建 Consumer 工程

创建 Consumer 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-openfeign。由于需要从 Eureka Server 获取服务列表,即作为 Eureka 客户端,还需要引入 spring-cloud-starter-netflix-eureka-client。

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>

创建启动主类,添加注解 @EnableFeignClients、@EnableDiscoveryClient

1
2
3
4
5
6
7
8
9
10
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

public static void main(String[] args){
SpringApplication.run(ConsumerApplication.class, args);
}

}

创建服务接口类,用于通过 Feign 调用 Provider 服务:

1
2
3
4
5
6
7
@FeignClient(name = "provider-service")
public interface SkyFeignService {

@RequestMapping(value = "/getSendInfo", method = RequestMethod.GET)
String getSendInfo(@RequestParam("serviceName") String serviceName);

}

创建用于测试的 Controller 类,使用多种方式调用 Provider 服务的那个自定义 API:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class ConsumerController {

@Autowired
private SkyFeignService providerFeignService;

@RequestMapping(value = "/getInfo", method = RequestMethod.GET)
public String getInfo() {
return providerFeignService.getSendInfo("Consumer-Service");
}

}

在 application.yml 文件中配置端口号、注册中心地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 6060

spring:
application:
name: consumer-service

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

8. 使用 Agent 启动服务和监控

在 官网 下载 SkyWalking 的 5.0.0-GA 版本,解压后得到里面的 agent 目录(如下),该目录是探针相关的。

1
2
3
4
5
6
7
8
9
10
11
apache-skywalking-apm-incubating
├── agent
├── bin
├── collector-libs
├── config
├── DISCLAIMER
├── LICENSE
├── licenses
├── NOTICE
├── README.txt
└── webapp

手动创建四个目录(如下),分别为 service-eureka、service-zuul、service-consumer、service-provider,主要用于存放被监控的 Jar 和 agent 目录,每个应用单独使用一个对应的 agent 目录进行启动。

1
2
3
4
5
6
7
8
9
10
11
12
├── service-consumer
│   ├── agent
│   └── consumer-service-1.0-SNAPSHOT.jar
├── service-eureka
│   ├── agent
│   └── eureka-service-1.0-SNAPSHOT.jar
├── service-provider
│   ├── agent
│   └── provider-service-1.0-SNAPSHOT.jar
└── service-zuul
├── agent
└── zuul-service-1.0-SNAPSHOT.jar

其中的 agent 目录是之前下载 SkyWalking 并解压后得到的,分别修改 agent/config 目录里面的 agent.config 文件,只需要修改 agent.application_ code 这一项配置,这个代表应用。每个配置文件对应改为:

1
2
3
4
agent.application_code=service-eureka
agent.application_code=service-zuul
agent.application_code=service-provider
agent.application_code=service-consumer

而服务应用是上面开发好的,通过 Maven 命令 maven package 将项目打包后得到可执行的 Jar。最后,通过下面的命令按顺序启动要监控的服务,具体如下:

1
2
3
4
5
6
7
java -javaagent:/usr/local/skywalking/service-eureka/agent/skywalking-agent.jar -jar /usr/local/skywalking/service-eureka/eureka-service-1.0-SNAPSHOT.jar

java -javaagent:/usr/local/skywalking/service-zuul/agent/skywalking-agent.jar -jar /usr/local/skywalking/service-zuul/zuul-service-1.0-SNAPSHOT.jar

java -javaagent:/usr/local/skywalking/service-provider/agent/skywalking-agent.jar -jar /usr/local/skywalking/service-provider/provider-service-1.0-SNAPSHOT.jar

java -javaagent:/usr/local/skywalking/service-consumer/agent/skywalking-agent.jar -jar /usr/local/skywalking/service-consumer/consumer-service-1.0-SNAPSHOT.jar

启动命令从上到下依次执行即可,所有应用启动完成后,通过 http://localhost:8080 可以访问 SkyWalking 的管理界面,默认登录的用户名 / 密码是 admin / admin:

通过 Postman 等 HTTP 工具调用 http://127.0.0.1:7070/client/getInfo 接口后,此时观察 SkyWalking 首页最下面会出现一个调用过程:

更详细的调用信息可以切换顶部左侧菜单中的 Applicaiotn、Service、Topology、Trace 和 Alarm 等标签项,首先看一下 Application 展示的内容:

接下来看一下 Service 展示的内容:

接下来看一下 Toplogy 展示的内容(整个拓扑图):

提示

  • 刷新 Toplogy 页面时,可能会先看到一个 service-eureka 的节点。重复刷新可能会出现 serivce-consumer 指向 service-provider 的请求,再请求一次,再刷新可能会出现 user --> service-zuul --> service-consumer --> service-provider 的调用,为什么会出现这样的现象呢?
  • 这里出现延迟的原因和请求调用顺序有关系,请求调用是顺序开始,但完成却是倒序完成的。在每个应用中完成请求处理后,会发送监控信息到 Collector,然后 Collector 才能依次输出采集的结果,所以就出现这种延迟显示的情况。

9. 下载案例代码

  • 完整的案例代码可以从 这里 下载得到。

Spring Cloud 与 Pinpoint

Pinpoint 介绍

Pinpoint 的概述

Pinpoint 是一个分析大规模分布式系统的平台,并提供处理大量跟踪数据的解决方案。它自 2012 年 7 月开发,并千 2015 年 1 月 9 日作为开源项目推出。Pinpoint 是由韩国人编写的著名的 APM (Application Performance Management) 系统。

Pinpoint 的特性

  • Pinpoint 的特点

    • 分布式事务跟踪,跟踪跨分布式应用的消息。
    • 自动检测应用拓扑,帮助开发者搞清楚应用的架构。
    • 水平扩展以便支持大规模服务器集群。
    • 提供代码级别的可见性以便轻松定位失败点和瓶颈。
    • 使用字节码增强技术,添加新功能而无须修改代码。
  • Pinpoint 的优势

    • 非入侵式:使用字节码增强技术,添加新功能而无须修改代码。
    • 资源消耗:对性能的影响最小(资源使用量增加)

Pinpoint 的兼容性

下面对 Pinpoint 涉及的组件和存储方面的中间件进行版本对比。

Pinpoint 的核心模块

Pinpoint 主要包含以下 4 个架构模块:

  • HBase(用于存储数据)。
  • PinpointCollector(部署在 Web 容器上)。
  • Pinpoint Web(部署在 Web 容器上)。
  • Pinpoint Agent(附加到需要分析的 Java 应用程序)。

为了更好地说明 Pinpoint 中架构的模块,需要看详细的架构图(如下)。大体的流程是这样的:首先通过 Agent 收集调用应用的数据,将数据发送给 Collector,Collector 负责处理和分析数据,最后将数据存储到 HBase 中,可以通过 Pinpoint Web UI 查看已经分析好的调用分析数据。

Pinpoint 的数据结构

在 Pinpoint 中,数据结构的核心由 Span、Trace 和 TraceId 组成。

  • Span:RPC(远程过程调用)跟踪的基本单位。它表示 RPC 到达时处理的工作,包含跟踪数据。为确保代码级别的可见性,Span 将子项标记为 SpanEvent,作为数据结构。每个 Span 包含一个 TraceId。
  • Trace:一系列跨度;它由相关的 RPC(Spans)组成。同一个跟踪中的跨距共享相同的 TransactionId。Trace 通过 SpanIds 和 ParentSpanIds 排序为分层树状结构。
  • TraceId:由 TransactionId、SpanId 和 ParentSpanId 组成的密钥集合。TransactionId 表示消息 ID,SpanId 和 ParentSpanId 都表示 RPC 的父子调用关系。
    • TransactionId(TxId):来自单个事务的分布式系统发送 / 接收的消息的 ID;它必须在整个服务器组中是全局唯一的。
    • SpanId:接收 RPC 消息时处理的作业的 ID;它是在 RPC 到达服务器节点时生成的。
    • ParentSpanId(pSpanId):生成 RPC 的父 Span 的 SpanId。如果节点是事务的起始点,则不会有父跨度(对于这些情况,使用值 -1 来表示跨度是事务的根跨度。

提示

上图能够比较直观地说明这些 ID 结构直接的关系,同时可以很好地描述一个 Trace 的过程。

Pinpoint 与 Zipkin 对比

对比点 ZipkinPinpoint 说明
技术点√Zipkin 依赖于 Spring 框架的 API 支持,监控内容和范围有限
Pinpoint 使用字节码注入技术,监控更为深入和广泛,比如可以看到调用了几次 Redis、哪几个微服务、MySQL 以及执行的 SQL 等,可以非常方便地追踪问题
接入成本√Zipkin 需要对应用进行简单改造,包括 Sleuth 的引入和配置
Pinpoint 只需要在服务器打入探针,对业务没有任何改动
扩展性√Zipkin 使用了 Spring Cloud 常用的 RESTful 协议,可以方便地进行扩展
Pinpoint 现在使用 gRPC 协议,也可以使用 Thrift 传输协议,出于传输数据量和性能的考虑
兼容性√Zipkin 对 Spring Cloud 的兼容性更好,也得益于上面说的扩展性
产品√相比较而言,Pinpoint 可以说是一款更成熟的产品
性能√由于采集的数据范围不同,即使 Pinpoint 采用了高性能的传输协议,但是 Zipkin 的性能依然更高

Pinpoint 实战案例

由于篇幅有限,详细的实战案例代码可参考《重新定义 Spring Cloud 实战》的 第 16.7 小节(389 页)。

参考资料