基于 Docker 简单搭建 Jaeger 的示例,此例子运行在虚拟机中,虚拟机的 IP 为 192.168.99.100

Jaeger 搭建

创建数据库 Cassandra

使用的 docker-compose 文件如下:

1
2
3
4
5
6
7
8
9
version: "3"
services:
cassandra:
container_name: jaeger_db_cassandra
image: cassandra:3
volumes:
- /var/lib/cassandra
ports:
- 9042:9042

使用 Jaeger 提供的镜像为 Cassandra 初始化数据库

1
docker run --link jaeger_db_cassandra:cassandra --net cassandra_default --rm -ti jaegertracing/jaeger-cassandra-schema

因为 Cassandra 用了 Docker-Compose 启动,所以初始化时要用 --net 参数连接到 Cassandra 所在的网络

启动 Jaeger 的 Query 、 Collector 和 Agent 服务

使用的 docker-compose 文件如下:

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
version: "3"
services:
query:
container_name: jaeger_query
image: jaegertracing/jaeger-query:1.11.0
environment:
- SPAN_STORAGE_TYPE=cassandra
- CASSANDRA_KEYSPACE=jaeger_v1_dc1
- CASSANDRA_SERVERS=192.168.99.100
ports:
- 16686:16686/tcp
collector:
container_name: jaeger_collector
image: jaegertracing/jaeger-collector:1.11.0
environment:
- SPAN_STORAGE_TYPE=cassandra
- CASSANDRA_KEYSPACE=jaeger_v1_dc1
- CASSANDRA_SERVERS=192.168.99.100
ports:
- 9411:9411/tcp
- 14267:14267/tcp
- 14268:14268/tcp
agent:
container_name: jaeger_agent
image: jaegertracing/jaeger-agent:1.11.0
environment:
- COLLECTOR_HOST_PORT=192.168.99.100:14267
ports:
- 5775:5775/udp
- 6831-6832:6831-6832/udp
- 5778:5778/tcp
depends_on:
- collector

每个组件可配置的参数可运行对应的镜像 + -h 参数查看,如:

1
docker run --rm -ti jaegertracing/jaeger-collector -h

用 Docker-Compose 运行 Jaeger 时,参数除了配置成环境变量,也可以配置成启动参数的形式,对应的环境变量的名称就是启动的参数去掉 -- ,字母大写,’.‘ 转换成 ‘_‘,如:

1
2
3
4
5
6
7
8
version: "3"
services:
query:
container_name: jaeger_query
image: jaegertracing/jaeger-query:1.11.0
ports:
- 16686:16686/tcp
command: "--cassandra.keyspace jaeger_v1_dc1 --cassandra.servers 192.168.99.100"

示例代码

首先构造一个项目,作为前端,用户访问前端的地址时,前端会调用一次微服务,并返回页面给用户
用 IDEA 新建项目,选择 Spring Initializr ,后面的依赖中选择 Web 就行

build.gradle 中引入 Jaeger Client 和发起 Http 调用的 okhttp ,如下所示:

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
plugins {
id 'org.springframework.boot' version '2.1.4.RELEASE'
id 'java'
}

apply plugin: 'io.spring.dependency-management'

bootJar {
baseName = 'frontend'
}

repositories {
mavenCentral()
}

sourceCompatibility = '11'

ext {
set('springCloudVersion', 'Greenwich.SR1')
}

dependencies {
implementation 'io.jaegertracing:jaeger-client:0.34.0'
implementation 'com.squareup.okhttp3:okhttp:3.4.2'

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

注册 Tracer 的 Bean ,为了方便这里直接放在框架启动类 Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SpringBootApplication
public class Application {

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

@Bean
public Tracer tracer() {
SamplerConfiguration samplerConfig = SamplerConfiguration.fromEnv()
.withType(ConstSampler.TYPE)
.withParam(1);

ReporterConfiguration reporterConfig = ReporterConfiguration.fromEnv()
.withLogSpans(true);

Configuration config = new Configuration("frontend-demo")
.withSampler(samplerConfig)
.withReporter(reporterConfig);

return config.getTracer();
}
}

控制器中调用别的服务时,将其包装为 Span ,为了能构造调用的上下级关系,把本次调用的 Span 的上下文注入(Inject)到 Tracer 中,如下所示:

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
@Controller
public class FrontendController {

@Autowired
private Tracer tracer;

@RequestMapping("/")
public String index(Model model) throws IOException, InterruptedException {
Scope span = tracer.buildSpan("frontend-index").startActive(true);

String response = callExternalService("eureka-hello-service");

span.close();

model.addAttribute("data", response);
return "index";
}

private String callExternalService(String name) throws IOException, InterruptedException {
String url = "http://192.168.99.100:21001/greeting";

Request.Builder requestBuilder = new Request.Builder().url(url);

tracer.inject(tracer.activeSpan().context(), Format.Builtin.HTTP_HEADERS, new RequestBuilderCarrier(requestBuilder));

Request request = requestBuilder.build();

OkHttpClient client = new OkHttpClient();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
}

其中,注入 Span 上下文时用到的 RequestBuilderCarrier 可自定义,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import io.opentracing.propagation.TextMap;
import okhttp3.Request;

import java.util.Iterator;
import java.util.Map;

public class RequestBuilderCarrier implements TextMap {
private final Request.Builder requestBuilder;

public RequestBuilderCarrier(Request.Builder requestBuilder) {
this.requestBuilder = requestBuilder;
}

@Override
public Iterator<Map.Entry<String, String>> iterator() {
throw new UnsupportedOperationException("carrier is writer-only");
}

@Override
public void put(String key, String value) {
requestBuilder.addHeader(key, value);
}
}

同上,新建一个 Spring Boot 项目,作为被调用的微服务,Jaeger 配置相同,控制器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class GreetingController {

private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();

@Autowired
private Tracer tracer;

@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name,
@RequestHeader HttpHeaders headers)
throws IOException, InterruptedException {
SpanContext spanContext = tracer.extract(Format.Builtin.HTTP_HEADERS,
new TextMapExtractAdapter(headers.toSingleValueMap()));
Span span = tracer.buildSpan("test").asChildOf(spanContext).start();

span.finish();

return new Greeting(counter.incrementAndGet(),
String.format(template, name));
}
}

启动示例代码

首先,在两个项目中分别运行 gradle build 生成 Jar 文件,这里是 frontend.jar 和 ms-service.jar ,把它们跟docker-compose.yml文件放一起,docker-compose.yml 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: "3"
services:
frontend:
container_name: frontend_1
image: adoptopenjdk/openjdk11:jdk-11.0.2.9
volumes:
- ./frontend.jar:/frontend.jar
command: java -jar /frontend.jar
ports:
- 21000:8080
environment:
- JAEGER_AGENT_HOST=192.168.99.100
msservice:
container_name: ms_service_1
image: adoptopenjdk/openjdk11:jdk-11.0.2.9
volumes:
- ./ms-service.jar:/ms-service.jar
command: java -jar /ms-service.jar
ports:
- 21001:8080
environment:
- JAEGER_AGENT_HOST=192.168.99.100

注意代码跟 docker-compose.yml 中的地址、端口的对应关系
用 Docker-Compose 启动项目,在浏览器中访问前端地址 192.168.99.100:21000,获取到返回的页面后,访问 Jaeger Query 的地址 192.168.99.100:16686,左侧栏选择前端服务,点击 Find Traces,即可看到调用链信息