一分钟构建高可用的分布式追踪系统-Jaeger

关键技术:

  • Kubernetes: 分布式的容器编排与调度系统,当前公认的云原生技术基础设施
  • Jaeger: 开源端到端的分布式追踪系统
  • Helm: Kubernetes 中的包管理器

背景

Kubernetes 已经成为整个云原生公认的基础设施了,随着互联网业务的快速发展,越来越多的业务会在一开始就选择使用云原生的方式进行开发和交付,而 Helm 就是整个过程中用来交付环境的工具,就好比在过去,我们会将各个业务打包成 tar 包或者 RPM 包,然后交付到不同环境进行部署。

而云原生应用天然的分布式和微服务特性会使得我们在业务运行过程中很难洞察到业务问题,对于 SRE 来讲,就无从谈起可用性和稳定性问题了。因此,对于云原生系统而言,一个可靠的分布式应用监控以及链路追踪系统就显得尤为重要了。

当然,类似的分布式应用监控和追踪系统也有很多,比如 SkyWalingZipkinCatPinpoint

而工作中使用到了 Jaeger 以及对 Golang 语言相对而言比较熟悉,从而有了本篇文章。

技术点介绍

对于 Kubernetes 而言,不用多说,想必技术同学应该都知道了,下面主要介绍 Helm 和 Jaeger ,毕竟这俩才是快速构建高可用的分布式追踪系统的关键。

Helm

Helm 为团队提供了在 Kubernetes 内部创建、安装和管理应用程序时需要协作的工具,有点类似于 Ubuntu 中的 APT 或 CentOS 中的 YUM

对于操作者而言,主要有如下功能:

  • 查找要安装和使用的预打包软件(Chart)
  • 轻松创建和托管自己的软件包
  • 将软件包安装到任何 K8s 集群中
  • 查询集群以查看已安装和正在运行的程序包
  • 更新、删除、回滚或查看已安装软件包的历史记录

核心组件:

  • helm: Helm 是一个命令行下的客户端工具。主要用于 Kubernetes 应用程序 Chart 的创建、打包、发布以及创建和管理本地和远程的 Chart 仓库
  • Chart: Helm 的软件包,采用 TAR 格式。类似于 APT 的 DEB 包或者 YUM 的 RPM 包,其包含了一组定义 Kubernetes 资源相关的 YAML 文件。
  • Repoistory: Helm 的软件仓库,Repository 本质上是一个 Web 服务器,该服务器保存了一系列的 Chart 软件包以供用户下载,并且提供了一个该 Repository 的 Chart 包的清单文件以供查询。Helm 可以同时管理多个不同的 Repository。
  • Release: 使用 helm install 命令在 Kubernetes 集群中部署的 Chart 称为 Release。可以理解为 Helm 使用 Chart 包部署的一个应用实例。

Jaeger

Jaeger 受 Dapper 和 OpenZipkin 的启发,是由 Uber Technologies 发布的开源分布式追踪系统。它用于监控和排查基于微服务的分布式系统问题,包括:

  • 分布式上下文传播
  • 分布式事务监控
  • 根因分析
  • 服务依赖关系分析
  • 性能/延迟优化

Jaeger 架构:

直接存储架构

使用Kafka作为中间缓冲区架构

整体上讲,一个基础的 Jaeger 追踪系统包含下面几个部分:

  • jaeger-query: 用于客户端查询和检索组件,并包含了一个基础的UI
  • jaeger-collector: 接收来自 jaeger-agent 的 trace 数据,并通过处理管道来执行。当前的处理管道包含验证 trace 数据,创建索引,执行数据转换以及将数据存储到对应的后端
  • jaeger-agent: 一个网络守护进程,侦听通过 UDP 发送的 spans ,它对其进行批处理并发送给收集器。它被设计为作为基础设施组件部署到所有主机。代理将收集器的路由和发现从客户机抽象出来
  • backend-storage: 用于指标数据存储的可插拔式后端存储,支持 Cassandra, Elasticsearch and Kafka
  • ingester: 可选组件,用于从 kafka 中消费数据并写入到可直接读取的 Cassandra 或 Elasticsearch 存储中

注意: trace 和 span 是链路追踪系统中的专业属于,span 就表示具体的一个工作单元,并且包含了该单元的元数据比如具体是什么操作,执行的时间等等,而 trace 是通过整个系统的数据路径,可以被认为是将 Spans 组成一个有向无环图(DAG)

而对于一个多组件系统来说,也存在多个网络端口用于数据的传输,下图即为各个组件的端口用途:

jaeger-ports

Jaeger 环境快速初始化

注意:

  • 当前选用es作为后端存储,必须版本是6 (因为es-go 的sdk 是强依赖版本的,jaeger 最新版本只支持es6)
  • es采用外置实实例,因为有状态服务在k8s集群中暂时不好维护数据状态,便于生产环境直接替换独立的 es 集群实例

安装helm v3 以及基础使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ wget https://get.helm.sh/helm-v3.1.2-linux-amd64.tar.gz
$ tar -zxvf helm-v3.1.2-linux-amd64.tar.gz
$ cd linux-amd64
$ mv helm /usr/local/bin/
$ helm --help

# 添加helm 仓库
$ helm repo add stable https://kubernetes-charts.storage.googleapis.com/
$ helm repo add bitnami https://charts.bitnami.com/bitnami
# 查看仓库列表
$ helm repo list

# chart 语法检查
$ helm lint chart

# yaml 模版渲染
$ helm template chart

# chart 部署(创建服务的release)
$ helm install

下载Jaeger Helm Chart

 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
# git clone https://gitee.com/BGBiao/sample-jaeger.git

# 目录结构

sh-4.2# cd sample-jaeger/
sh-4.2# ls
Chart.lock  charts  Chart.yaml	es  files  readme.md  templates  values.yaml
sh-4.2# tree -L 2 .
.
|-- Chart.lock
|-- charts
|-- Chart.yaml
|-- es
|   `-- elasticsearch.yml
|-- readme.md
|-- templates
|   |-- agent-deploy.yaml
|   |-- agent-svc.yaml
|   |-- collector-deploy.yaml
|   |-- collector-svc.yaml
|   |-- configmap.yaml
|   |-- elasticsearch-secret.yaml
|   |-- _helpers.tpl
|   |-- hotrod-deploy.yaml
|   |-- hotrod-ingress.yaml
|   |-- hotrod-svc.yaml
|   |-- query-deploy.yaml
|   `-- query-svc.yaml
`-- values.yaml

准备 ElasticSearch 后端存储

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 使用docker 方式准备一个外置的es 实例
$ docker run --name jaeger-es  -itd -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -v $PWD/es/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml elasticsearch:6.4.3

$ curl localhost:9200
{"error":{"root_cause":[{"type":"security_exception","reason":"missing authentication token for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"security\" charset=\"UTF-8\""}}],"type":"security_exception","reason":"missing authentication token for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"security\" charset=\"UTF-8\""}},"status":401}

# 因为es 开启了认证,需要先设置密码访问
sh-4.2# docker exec -it jaeger-es-tmp bash
./bin/elasticsearch-setup-passwords
Sets the passwords for reserved users

Commands
--------
auto - Uses randomly generated passwords
interactive - Uses passwords entered by a user

Non-option arguments:
command

Option         Description
------         -----------
-h, --help     show help
-s, --silent   show minimal output
-v, --verbose  show verbose output
ERROR: Missing command

# 设置elasticstack 所有的密码
[root@b90822f2c26b elasticsearch]# ./bin/elasticsearch-setup-passwords interactive
Initiating the setup of passwords for reserved users elastic,kibana,logstash_system,beats_system.
You will be prompted to enter passwords as the process progresses.
Please confirm that you would like to continue [y/N]y


Enter password for [elastic]:
Reenter password for [elastic]:
Enter password for [kibana]:
Reenter password for [kibana]:
Enter password for [logstash_system]:
Reenter password for [logstash_system]:
Enter password for [beats_system]:
Reenter password for [beats_system]:
Changed password for user [kibana]
Changed password for user [logstash_system]
Changed password for user [beats_system]
Changed password for user [elastic]

# 使用密码访问es实例
$ curl -u 'elastic:xxxxxxx' http://localhost:9200
{
  "name" : "1xdrvFl",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "CO76XmP0Qbexysev4Al2MQ",
  "version" : {
    "number" : "6.4.3",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "fe40335",
    "build_date" : "2018-10-30T23:17:19.084789Z",
    "build_snapshot" : false,
    "lucene_version" : "7.4.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

修改 Chart 配置并安装 Release

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# 修改helm chart 中的es地址 storage.elasticsearch.host/storage.elasticsearch.password
#  部署整个jaeger 服务
sh-4.2# ls
Chart.lock  charts  Chart.yaml	es  files  readme.md  templates  values.yaml
sh-4.2# helm install -n sample-jaeger jaeger .
Error: create: failed to create: namespaces "sample-jaeger" not found
sh-4.2#


sh-4.2# kubectl  create ns sample-jaeger
namespace/sample-jaeger created
sh-4.2# helm install -n sample-jaeger jaeger .
NAME: jaeger
LAST DEPLOYED: Sun May 23 23:42:58 2021
NAMESPACE: sample-jaeger
STATUS: deployed
REVISION: 1
TEST SUITE: None
sh-4.2# helm list -n sample-jaeger
NAME  	NAMESPACE    	REVISION	UPDATED                               	STATUS  	CHART              	APP VERSION
jaeger	sample-jaeger	1       	2021-05-23 23:42:58.75504002 +0800 CST	deployed	sample-jaeger-0.1.0	1.16.0


sh-4.2# kubectl  get all -n sample-jaeger
NAME                                                       READY   STATUS    RESTARTS   AGE
pod/jaeger-agent-579587cb85-rzd46                          1/1     Running   0          27s
pod/jaeger-collector-57c57bc788-682sn                      1/1     Running   0          27s
pod/jaeger-query-85d86d44d-dhj7g                           1/1     Running   0          27s
pod/jaeger-sample-jaeger-jaeger-example-7bf65bd75b-mlxhv   1/1     Running   0          27s

NAME                                          TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)                                                          AGE
service/jaeger-agent                          NodePort   172.16.127.123   <none>        6831:31139/UDP,5775:31355/TCP,6832:30937/TCP,5778:32360/TCP      27s
service/jaeger-agent-backup                   NodePort   172.16.125.116   <none>        6831:30036/UDP,5775:31138/TCP,6832:31179/TCP,5778:30568/TCP      27s
service/jaeger-collector                      NodePort   172.16.127.93    <none>        14250:30708/TCP,14268:32354/TCP,9411:32312/TCP,14267:30986/TCP   27s
service/jaeger-collector-backup               NodePort   172.16.125.137   <none>        14250:30477/TCP,14268:32513/TCP,9411:30679/TCP,14267:31237/TCP   27s
service/jaeger-query                          NodePort   172.16.124.251   <none>        16686:30843/TCP,16687:30530/TCP                                  27s
service/jaeger-query-backup                   NodePort   172.16.125.214   <none>        16686:30273/TCP,16687:30858/TCP                                  27s
service/jaeger-sample-jaeger-jaeger-example   NodePort   172.16.125.35    <none>        80:31255/TCP                                                     27s

NAME                                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/jaeger-agent                          1/1     1            1           27s
deployment.apps/jaeger-collector                      1/1     1            1           27s
deployment.apps/jaeger-query                          1/1     1            1           27s
deployment.apps/jaeger-sample-jaeger-jaeger-example   1/1     1            1           27s

NAME                                                             DESIRED   CURRENT   READY   AGE
replicaset.apps/jaeger-agent-579587cb85                          1         1         1       27s
replicaset.apps/jaeger-collector-57c57bc788                      1         1         1       27s
replicaset.apps/jaeger-query-85d86d44d                           1         1         1       27s
replicaset.apps/jaeger-sample-jaeger-jaeger-example-7bf65bd75b   1         1         1       27s


# 这里我们测试下jaeger-query 的服务
sh-4.2# curl 172.16.124.251:16686 -I
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Date: Sun, 23 May 2021 15:44:39 GMT

# 此时发现我们核心的几个服务都没有问题(agent,collector,query)
# 接下里就可以直接访问我们的jaeger 的服务页面了
# 因为我们的service 采用了nodePort 类型,可以直接访问主机的ip:port

Jaeger 系统使用

jaeger-首页

注意: 默认我们没有应用的数据采集,因此看不到任何链路相关的数据,其实可以看到,我们上面也部署了一个测试程序jaeger-example,它是一个简单但却完整的程序,贯穿了前后端,数据库缓存等全流程的逻辑,可以让我们清楚的查看整个调用链路。

这个时候我们直接打开jaeger-example 对应的地址即可。 node:31255

jaeger-horod

可以看到整个服务提供了一个全局视角,从用户的客户端id进入请求开始,来模拟全流程的服务请求流转,接下来就可以随意点击几个服务模块,让服务主动上报一些数据给jaeger

jaeger-hotROD-serving

模拟一些用户的请求后,我们就可以查看到服务的一些指标了

choose-some-filter-find-trace

可以看到刚才的模拟请求中,总共产生了5个Traces,也就分别对应了我们在 hotROD 服务中的5次请求

然后,点进去任何一条链路,都可以查看到整个调用过程的耗时情况

tracing-timeline

tracing-troubshooting

从调用链路的方法调用,也能很快查看到调用超时以及异常模块

然后右上角可以选择 trace 数据的方式,比如可以看到调用关系,这样就很方便我们来查看一个完整服务的各个调用关系

trace-graph

到这里,我们就可以看到,我们使用 helm 快速部署了一个分布式的 jaeger 集群

当然啦,hotROD 服务仅仅是测试服务,验证我们的jaeger 监控可用,之后我们需要将它销毁以释放相关的资源

修改Helm Chart更新服务

注意: 由于我们的 服务都是通过在chart 中的yaml定义的,因此服务的管控只需要改yaml就行。

修改values.yaml 文件中的jaegerExample.enabled: false 后,执行如下命令

 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
# 制定配置文件,对指定的release 进行更新
sh-4.2# helm  upgrade -n sample-jaeger -f ./values.yaml jaeger .
Release "jaeger" has been upgraded. Happy Helming!
NAME: jaeger
LAST DEPLOYED: Mon May 24 00:18:46 2021
NAMESPACE: sample-jaeger
STATUS: deployed
REVISION: 2
TEST SUITE: None

# 可以看到,REVISION 已经更新到2
## 并且我们的jaeger-example 服务已经下线
sh-4.2# helm  list -n sample-jaeger
NAME  	NAMESPACE    	REVISION	UPDATED                                	STATUS  	CHART              	APP VERSION
jaeger	sample-jaeger	2       	2021-05-24 00:18:46.865941544 +0800 CST	deployed	sample-jaeger-0.1.0	1.16.0

sh-4.2# kubectl get all -n sample-jaeger
NAME                                    READY   STATUS    RESTARTS   AGE
pod/jaeger-agent-579587cb85-rzd46       1/1     Running   0          38m
pod/jaeger-collector-57c57bc788-682sn   1/1     Running   0          38m
pod/jaeger-query-85d86d44d-dhj7g        1/1     Running   0          38m

NAME                              TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)                                                          AGE
service/jaeger-agent              NodePort   172.16.127.123   <none>        6831:31139/UDP,5775:31355/TCP,6832:30937/TCP,5778:32360/TCP      38m
service/jaeger-agent-backup       NodePort   172.16.125.116   <none>        6831:30036/UDP,5775:31138/TCP,6832:31179/TCP,5778:30568/TCP      38m
service/jaeger-collector          NodePort   172.16.127.93    <none>        14250:30708/TCP,14268:32354/TCP,9411:32312/TCP,14267:30986/TCP   38m
service/jaeger-collector-backup   NodePort   172.16.125.137   <none>        14250:30477/TCP,14268:32513/TCP,9411:30679/TCP,14267:31237/TCP   38m
service/jaeger-query              NodePort   172.16.124.251   <none>        16686:30843/TCP,16687:30530/TCP                                  38m
service/jaeger-query-backup       NodePort   172.16.125.214   <none>        16686:30273/TCP,16687:30858/TCP                                  38m

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/jaeger-agent       1/1     1            1           38m
deployment.apps/jaeger-collector   1/1     1            1           38m
deployment.apps/jaeger-query       1/1     1            1           38m

NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/jaeger-agent-579587cb85       1         1         1       38m
replicaset.apps/jaeger-collector-57c57bc788   1         1         1       38m
replicaset.apps/jaeger-query-85d86d44d        1         1         1       38m



如此,我们就使用helm 来很优雅的管理了我们服务的部署以及实例的管理。

最后,我们有没有发现,直接使用helm 可能会有几个问题:

    1. 命名空间没办法声明,且不能自动创建
    1. 不支持像kubectl apply命令,来直接更新对应资源的配置
    1. 无法做到环境的区分以及chart版本控制

其实,在社区里已经有相关的工具来解决这些问题了,就是 helmfile ,它可以通过helmfile.yaml 文件来帮助用户管理和维护众多 helm chart,提高部署的可观测性和可重复性,区分环境,免去各种 --set 参数的困扰,感兴趣的同学可以去试一试。

公众号