利用Jaeger打造云原生架构下分布式追踪系统
01
为什么选择Jaeger
优点
-
Jaeger由Uber开源并被云原生基金会(CNCF)纳入孵化项目,背后有大厂和强大的组织支持,项目目前开发活跃;
-
原生支持 OpenTracing 标准(可以认为是OpenTracing协议的参考实现),支持多种主流语言,可以复用大量的 OpenTracing 组件;
-
丰富的采样率设置支持;
-
高扩展,易伸缩,没有单点故障,可以随着业务方便扩容;
-
多种存储后端支持;
-
提供现代的 Web UI,可以支持大量数据的呈现;
-
支持云原生的部署方式,非常容易部署在 Kubernetes 集群中;
-
可观察性,所有组件均默认可暴露 Prometheus metrics,日志默认使用结构化的日志到标准输出。
缺点
-
接入过程有一定的侵入性;
-
相比与上篇介绍的 Apache SkyWalking 、CAT,Jaeger更专注于链路追踪(Tracing),日志和指标功能支持比较有限;
-
本身缺少监控和报警机制,需要结合第三方工具来实现,比如配合Grafana 和 Prometheus实现。文章后面会给出简单的示例。
02
实现原理
1.Jaeger架构图解
图片来源: Jaeger Architecture
2.Jaeger组件
-
客户端库实现了OpenTarcing API。可以手动也可以通过已经集成OpenTracing 的框架工具实现应用的分布式追踪,像Flask、Dropwizard、gRPC等都已经有现成的集成工具库;
-
每当接受到新的请求,就会创建 span 并关联上下文信息(trace id、span id 和 baggage)。只有 id 和 baggage 会随请求向下传递,而所有组成 span 的其他信息,如操作名称、日志等,不会在同一个trace 的span间传递。通过采样得到的 span 会在后台异步发送到 Jaeger Agents 上;
-
需要注意的是虽然所有的traces都会创建,但是只有少部分会被采样,采样到的trace会被标记并用于后续的处理和存储。默认情况下,Jaeger client 的采样率是 0.1%,也就是千分之一,并且可以从 Agent上取回采样率设置;
-
Agent 是一个网络守护进程,监听通过 UDP 发送过来的 spans,并将其批量发送给 Collector。按设计 Agent 要作为基础设施被部署到所有主机节点。Agent 将 Collector 和客户端之间的路由与发现机制抽象了出来。后面会详细介绍Agent的部署模式;
-
Collector 从 Agents 接收 traces,并通过一个pipeline对其进行处理。目前的pipeline会检验traces、建立索引、执行转换并最终进行存储。Jaeger的存储系统是一个可插入的组件,当前支持 Cassandra、Elasticsearch 和 Kafka(测试环境可以支持纯内存存储);
-
Query 从存储中检索 traces 并通过 一个漂亮的 UI 界面进行展现,目前支持搜索、过滤、traces 对比、查看依赖调用关系图等功能。
3.关于采样率
分布式追踪系统本身也会造成一定的性能低损耗,如果完整记录每次请求,对于生产环境可能会有极大的性能损耗,一般需要进行采样设置。
当前支持四种采样率设置:
-
固定采样(sampler.type=const)sampler.param=1 全采样, sampler.param=0 不采样;
-
按百分比采样(sampler.type=probabilistic)sampler.param=0.1 则随机采十分之一的样本;
-
采样速度限制(sampler.type=ratelimiting)sampler.param=2.0 每秒采样两个traces;
-
动态获取采样率 (sampler.type=remote) 这个是默认配置,可以通过配置从 Agent 中获取采样率的动态设置。
自适应采样(Adaptive Sampling)也已经在开发计划中。
03
部署实践
1.在Kubernetes集群上部署Jaeger
Jaeger是为云原生环境下的分布式追踪而打造,Kubernetes 又是当前云原生服务编排事实上的标准,下面以示例的方式介绍如何在 Kubernetes集群上部署 Jaeger:
1# 克隆示例到本地
2git clone https://github.com/maguowei/distributed-tracing-system.git
3cd distributed-tracing-system
4
5# 这里我们选择Elasticsearch作为存储, 简单创建测试用的 Elasticsearch 服务
6kubectl create -f deployment/kubernetes/elasticsearch
7
8# 部署Jaeger全家桶(Agent, Collector, Query)
9kubectl create -f deployment/kubernetes/jaeger
10
11# 以NodePort 方式暴露 Query UI
12kubectl expose service jaeger-query --port 16686 --type NodePort --name jaeger-query-node-port
13
14# 找到暴露的端口号
15kubectl get service jaeger-query-node-port
16
17# 访问 http://127.0.0.1:${port}
当前Query 中可以看到是空的,我们运行 官方的 HotROD 微服务示例,生成一些数据:
1kubectl create -f deployment/kubernetes/example
2kubectl expose service jaeger-example-hotrod --port 8080 --type NodePort --name jaeger-example-hotrod-node-port
打开HotROD页面, 任意点击页面上的按钮,生成一些调用数据:
刷新Jaeger Query UI 页面,然后我们就可以看到生成的调用信息:
点开具体的一条Trace 可以看到详细的调用过程:
还可以看到图形化的调用关系链:
2.选择 DaemonSet 还是 Sidecar
Agent 官方目前有两种部署方案,一种是 DaemonSet 方式,一种是 Sidecar 方式。
按照官方的说法,Jaeger 中的 Agent 组件是作为 tracer 和 Collector 之间的 buffer, 所以 Agent 应该离 tracer 越近越好,通常应该是 tracer 的 localhost, 基于这样的假定,tracer 能够直接通过UDP发送span 到 Agent,达到最好的性能和可靠性之间的平衡。
这样的假定在裸机服务器上部署非常棒,但在当前流行的云环境和容器中,对于 Kubernetes 来说究竟什么是本地(localhost)呢?是服务运行所在的节点(node)还是 pod 本身呢?
DaemonSet 的 pod 运行在节点(node)级别,这样的pod如同每个节点上的守护进程,Kubernetes 确保每个节点有且只有一个 Agent pod运行, 如果以 DaemonSet 方式部署,则意味着这个 Agent 会接受节点上所有应用pods发送的数据,对于 Agent 来说所有的 pods 都是同等对待的。这样确实能够节省一些内存,但是一个 Agent 可能要服务同一个节点上的数百个 pods。
Sidecar 是在应用 pod 中增加其他服务,在Kubernetes 中服务是以 pod 为基本单位的,但是一个 pod 中可以包含多个容器, 这通常可以用来实现嵌入一些基础设施服务, 在 Sidecar 方式部署下,对于 Jaeger Agent 会作为 pod 中的一个容器和 tarcer 并存,由于运行在应用级别,不需要额外的权限,每一个应用都可以将数据发送到不同的 Collector 后端,这样能保证更好的服务扩展性。
总结来说,基于你的部署架构,如果是私有云环境,且信任 Kubernetes 集群上运行的应用,可能占用更少内存的 DaemonSet 会适合你。如果是公有云环境,或者希望获得多租户能力,Sidecar 可能更好一些,由于 Agent 服务当前没有任何安全认证手段,这种方式不需要在 pod 外暴露Agent服务,相比之下更加安全一些,尽管内存占用会稍多一些(每个 Agent 内存占用在20M以内)。
1)Agent 以 DaemonSet 模式部署
DaemonSet 方式部署会有一个问题,如何保证应用能够和自己所在节点的Agent通讯?
为解决通讯问题,Agent需要使用主机网络(hostNetwork), 应用中需要借用 Kubernetes Downward API 获取节点IP信息。
DaemonSet 模式部署 Agent:
1apiVersion: apps/v1
2kind: DaemonSet
3metadata:
4 name: jaeger-agent
5 labels:
6 app: jaeger-agent
7spec:
8 selector:
9 matchLabels:
10 app: jaeger-agent
11 template:
12 metadata:
13 labels:
14 app: jaeger-agent
15 spec:
16 containers:
17 - name: jaeger-agent
18 image: jaegertracing/jaeger-agent:1.12.0
19 env:
20 - name: REPORTER_GRPC_HOST_PORT
21 value: "jaeger-collector:14250"
22 resources: {}
23 hostNetwork: true
24 dnsPolicy: ClusterFirstWithHostNet
25 restartPolicy: Always
通过 Kubernetes Downward API 将节点的IP信息(status.hostIP) 以环境变量的形式注入到应用容器中:
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: myapp
5spec:
6 selector:
7 matchLabels:
8 app: myapp
9 template:
10 metadata:
11 labels:
12 app: myapp
13 spec:
14 containers:
15 - name: myapp
16 image: example/myapp:version
17 env:
18 - name: JAEGER_AGENT_HOST
19 valueFrom:
20 fieldRef:
21 fieldPath: status.hostIP
2)Agent以Sidecar模式部署
下面是以Sidecar模式运行的应用示例,官方也提供了自动注入Sidecar的机制,详细使用可以参考[12]:
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: myapp
5 labels:
6 app: myapp
7spec:
8 replicas: 1
9 selector:
10 matchLabels:
11 app: myapp
12 template:
13 metadata:
14 labels:
15 app: myapp
16 spec:
17 containers:
18 - name: myapp
19 image: example/myapp:version
20 - name: jaeger-agent
21 image: jaegertracing/jaeger-agent:1.12.0
22 env:
23 - name: REPORTER_GRPC_HOST_PORT
24 value: "jaeger-collector:14250"
这样Jaeger Agent将会监听 localhost:5775/localhost:6831/localhost:6832/localhost:5778 这些本地端口,通常你不需要再在client配置中指定连接的主机名或者端口信息,应为这都是默认值。
3.生成依赖调用关系图
Jaeger Query UI服务中的 dependencies 选项默认点开为空,需要运行 spark-dependencies 来生成依赖关系图。
spark-dependencies 是一个Spark job 可以通过聚合和分析存储中的 trace 数据,生成服务间的依赖关系图,并将依赖链接信息持久化到存储中。之后 Jaeger Query Dependencies 页面就可以显示服务之间的依赖关系。
1# 可以手动只执行一次
2kubectl run -it --rm jaeger-spark-dependencies --env=STORAGE=elasticsearch --env ES_NODES=http://jaeger-elasticsearch:9200 --env ES_NODES_WAN_ONLY=true --restart=Never --image=jaegertracing/spark-dependencies
3
4# 也可以创建 CronJob, 每天定点生成新的依赖图
5kubectl create -f deployment/kubernetes/spark-dependencies/jaeger-spark-dependencies-cronjob.yaml
04
应用示例
下面以Python Django项目为例在服务中集成 Jaeger。
安装必要的依赖:
1pip install jaeger-client
2pip install django_opentracing
Jaeger tracer 配置和初始化:
1from jaeger_client import Config
2from django.conf import settings
3
4
5def init_jaeger_tracer(service_name='your-app-name'):
6 config = Config(
7 config={
8 'sampler': {
9 'type': 'const',
10 'param': 1,
11 },
12 'local_agent': {
13 'reporting_host': settings.JAEGER_REPORTING_HOST,
14 'reporting_port': settings.JAEGER_REPORTING_PORT,
15 },
16 'logging': True,
17 },
18 service_name='django-example',
19 validate=True,
20 )
21 return config.initialize_tracer()
22
23
24# this call also sets opentracing.tracer
25jaeger_tracer = init_jaeger_tracer(service_name='example')
Django_opentracing配置, 在Django settings文件中增加以下配置:
1import django_opentracing
2
3...
4
5# 添加 django_opentracing.OpenTracingMiddleware
6MIDDLEWARE = [
7 'django_opentracing.OpenTracingMiddleware',
8 ... # other middleware classes
9]
10
11# OpenTracing settings
12
13OPENTRACING_SET_GLOBAL_TRACER = True
14
15# if not included, defaults to True.
16# has to come before OPENTRACING_TRACING setting because python...
17OPENTRACING_TRACE_ALL = True
18
19# defaults to []
20# only valid if OPENTRACING_TRACE_ALL == True
21OPENTRACING_TRACED_ATTRIBUTES = ['path', 'method']
22
23from example.service.jaeger import jaeger_tracer
24
25OPENTRACING_TRACER = django_opentracing.DjangoTracing(jaeger_tracer)
这样Django接收的每个请求都会生成一条单独的Trace,当前请求的path和method会以Span Tag的形式记录下来。
手动创建Span和记录调用信息等更详尽的使用方法,请参考官方使用文档。
05
监控和报警
当前Jaeger缺少自带的报警机制,但是由于存储可以使用Elasticsearch,配合Grafana就可以实现简单的报警监控。
Jaeger本身暴露了Prometheus 格式的metrics 信息, 配合 Grafana可以方便的监控 Jaeger本身的运行状态。
06
资源清理
演示完毕,最后做一下资源的清理和释放:
1kubectl delete -f deployment/kubernetes/spark-dependencies
2kubectl delete -f deployment/kubernetes/example
3kubectl delete -f deployment/kubernetes/jaeger
4kubectl delete -f deployment/kubernetes/elasticsearch
5kubectl delete service jaeger-example-hotrod-node-port
6kubectl delete service jaeger-query-node-port