基于解决不同行业、业务应用的可扩展性、可用性等一系列问题,由此而生的微服务架构得到了各大厂商的、组织以及个人的青睐,随之而来便广泛应用于各种行业场景应用中。然而,随着时间的推移,越来越多的问题慢慢地呈现在大众的视野中。
其中,最为核心的问题莫过于微服务分布式性质导致的运行问题,以及带来的 2 个至关重要的挑战:
1、Monitoring,监控,如何能够全方位监控所有服务及其所涉及的相关指标。
2、Tracing,追踪,如何能够立体化追踪所有请求并识别我们应用服务中链路调用的瓶颈?
在本文中,我们将介绍如何将 Jaeger 被分类的跟踪集成到 Spring Boot MicroServices 中。在解析之前,我们先来了解下 Jaeger 链路追踪工作流原理,具体如如下参考示意图所示:
基于 Jaeger 组件架构原理,我们可以看到:在分布式系统中处理,当一个跟踪完成后,通过 Jaeger-Agent 将数据推送到 Jaeger-Collector。此时,Jaeger-Collector 负责处理四面八方推送来的跟踪信息,然后存储到后端系统库中,例如:可以存储到 ES、数据库等。然后,用户可以借助 Jaeger-UI 图形界面观测到这些被分析出来的跟踪信息。
关于数据采样率,在实际的业务场景中,链路追踪系统本身也会造成一定的性能低损耗,如果完整记录每次请求,对于生产环境可能会带来极大的性能损耗,因此,我们需要依据当前现状进行采样策略配置。截止目前,当前可支持5种采样率设置,具体如下:
1、固定采样(sampler.type=const)sampler.param=1 全采样, sampler.param=0 不采样
2、按百分比采样(sampler.type=probabilistic)sampler.param=0.1 则随机采十分之一的样本
3、采样速度限制(sampler.type=ratelimiting)sampler.param=2.0 每秒采样两个traces
4、动态获取采样率 (sampler.type=remote) 此策略为默认配置,可以通过配置从 Agent 中获取采样率的动态设置
5、自适应采样(Adaptive Sampling)开发计划中
在实际的业务场景中,为了能够追溯某一请求运行轨迹,通常,在理想情况下,我们需要对整个链路拓扑进行全方位追踪,以便能够在业务出现异常时能够快速响应、快速处理。因此,无论是基于 VM 的 Spring Cloud 微服务还是基于 Container ,其链路追踪体系基本的模型参考示意图如下所示:
在本文中,我们以 “Demo” 的形式对基于 Jaeger 的分布式链路追踪系统工程进行简要描述。基于上述的模型参考示意图,我们开始进行相关组件的部署。
为了能够尽可能详尽地解析 Jaeger 组件的基础原理,我们将先以最简单、明了的方式进行 Jaeger 组件部署。基于 Jaeger 的“all in one” 来进行镜像的构建、启动。具体如下所示:
[administrator@JavaLangOutOfMemory ~ ]% docker run -d --name jaeger \ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ -p 14250:14250 \ -p 9411:9411 \ jaegertracing/all-in-one:latest
在本文中,有关端口释义如下所示:
组件 |
端口 |
协议 | 描述 |
Agent |
6831 | UDP | 应用程序向代理发送跟踪的端口,接受 Jaeger.thrift而不是 Compact thrift协议 |
Agent | 6832 | UDP | 通过二进制thrift协议接受 Jaeger.thrift ,需要某些不支持压缩的客户端库 |
Agent |
5775 | UDP |
接收兼容zipkin的协议数据 |
Agent | 5778 | HTTP |
大数据流量下不建议使用 |
... |
... | ... | ... |
组件 |
端口 |
协议 |
描述 |
Collector |
14250 |
TCP |
Agent 发送 Proto 格式数据 |
Collector |
14267 |
TCP |
Agent 发送 Jaeger.thrift格式数据 |
Collector | 14268 |
HTTP |
从客户端接受 Jaeger.thrift |
Collector |
14269 |
HTTP |
健康检查 |
组件 |
端口 |
协议 |
描述 |
Query |
16686 |
HTTP |
HTTP 查询服务于 Jaeger UI |
Query |
16687 |
HTTP |
健康检查 |
... | ... |
... |
... |
自 1.17 版本(https://www.jaegertracing.io/docs/1.23/operator/ #当前版本),开始,我们还可以基于 Operator 的方式进行部署,并且其支持如下几种业务方式:
1、All-In-One Strategy
2、Production Strategy
3、Streaming Strategy
Jaeger Operator:Jaeger Operator for Kubernetes 简化了在 Kubernetes 上的部署和运行 Jaeger。Jaeger Operator 是 Kubernetes Qperator 的实现。从技术层面上讲,Qperator 是打包,部署和管理 Kubernetes 应用程序的一种方法。Jaeger Operator 版本跟踪 Jaeger 组件(查询,收集器,代理)的一种版本。具体部署步骤如下所示:
[administrator@JavaLangOutOfMemory ~ ]% kubectl create namespace jaeger
[administrator@JavaLangOutOfMemory ~ ]% kubectl create -n jaeger -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/crds/jaegertracing.io_jaegers_crd.yaml [administrator@JavaLangOutOfMemory ~ ]% kubectl create -n jaeger -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/service_account.yaml [administrator@JavaLangOutOfMemory ~ ]% kubectl create -n jaeger -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role.yaml [administrator@JavaLangOutOfMemory ~ ]% kubectl create -n jaeger -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role_binding.yaml [administrator@JavaLangOutOfMemory ~ ]% kubectl create -n jaeger -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/operator.yaml
此时,查看其状态,具体命令如下所示:
[administrator@JavaLangOutOfMemory ~ ]% kubectl get all -n jaeger
然后,进行 Jaeger 实例的创建,创建 jaeger.yaml 文件,配置 ES 集群及资源限制,具体如下所示:Deployment 以及所涉及 demo-prod-collector 容器的 CPU 和内存使用大小。如下示例文件,定义了最大数量可以起 10 个 Pod。
apiVersion: jaegertracing.io/v1 kind: Jaeger metadata: name: demo-prod spec: strategy: production storage: type: elasticsearch options: es: server-urls: http://10.172.10.1:9200 index-prefix: collector: maxReplicas: 10 resources: limits: cpu: 500m memory: 512Mi
[administrator@JavaLangOutOfMemory ~ ]% kubectl apply -f jaeger.yaml -n jaeger jaeger.jaegertracing.io/demo-prod created
若实际的业务场景中,如果流量过大,我们可以借助接入 Kafka 集群以减轻 ES 存储库压力,故此,修改后的 jaeger.yaml 文件如下所示:
apiVersion: jaegertracing.io/v1 kind: Jaeger metadata: name: demo-streaming spec: strategy: streaming collector: options: kafka: producer: topic: jaeger-spans brokers: demo-cluster-kafka-brokers.kafka:9092 #修改为kafka地址 ingester: options: kafka: consumer: topic: jaeger-spans brokers: demo-cluster-kafka-brokers.kafka:9092 #修改为kafka地址 ingester: deadlockInterval: 5s storage: type: elasticsearch options: es: server-urls: http://elasticsearch:9200 #修改为ES地址
针对 Agent 的部署,Agent 官方目前有两种部署方案,一种是基于 DaemonSet 方式,一种是基于 Sidecar 方式。依据官方所述,Jaeger 中的 Agent 组件是作为 Tracer 和 Collector 之间的 buffer, 所以 Agent 应该离 Tracer 越近越好,通常应该是 Tracer 的 Localhost, 基于这样的假定,Tracer 能够直接通过UDP发送 span 到 Agent,达到最好的性能和可靠性之间的平衡。
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、基于 DaemonSet 模式部署
apiVersion: apps/v1 kind: DaemonSet metadata: name: jaeger-agent labels: app: jaeger-agent spec: selector: matchLabels: app: jaeger-agent template: metadata: labels: app: jaeger-agent spec: containers: - name: jaeger-agent image: jaegertracing/jaeger-agent:1.12.0 env: - name: REPORTER_GRPC_HOST_PORT value: "jaeger-collector:14250" resources: {} hostNetwork: true dnsPolicy: ClusterFirstWithHostNet restartPolicy: Always
通过 Kubernetes Downward API 将节点的 IP 信息(status.hostIP) 以环境变量的形式注入到应用容器中。
2、基于 Sidecar 模式部署
apiVersion: apps/v1 kind: Deployment metadata: name: myapp labels: app: myapp spec: replicas: 1 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: example/myapp:version - name: jaeger-agent image: jaegertracing/jaeger-agent:1.12.0 env: - name: REPORTER_GRPC_HOST_PORT value: "jaeger-collector:14250"
至此,Jaeger 服务部署 OK。剩余组件的部署可参考官网。接下来,我们来看一下 Jaeger 接入 Traefik 组件的相关配置。默认情况下,Traefik 使用 Jaeger 来最为追踪系统的后端实现.,具体配置如下所示:
[administrator@JavaLangOutOfMemory ~ ]% cat traefik.toml [tracing] [tracing.jaeger] # 开启jaeger的追踪支持 samplingServerURL = "http://localhost:5778/demo" # 指定jaeger-agent的http采样地址 samplingType = "const" # 指定采样类型[const(const|probabilistic|rateLimiting)] samplingParam = 1.0 # 采样参数的值[1.0(const:0|1,probabilistic:0-1,rateLimiting:每秒的span数)] localAgentHostPort = "127.0.0.1:6831" # 本地agent主机和端口(会发送到jaeger-agent) gen128Bit = true # 生成128位的traceId,兼容OpenCensus propagation = "jaeger" # 设置数据传输的header类型[jaeger(jaeger|b3兼容OpenZipkin)] traceContextHeaderName = "demo-trace-id" # 跟踪上下文的header,用于传输跟踪上下文的http头名 [tracing.jaeger.collector] # 指定jaeger的collector服务 endpoint = "http://127.0.0.1:14268/api/traces?format=jaeger.thrift" user = "demo-user" # 向collector提交时的http认证用户[""] password = "demo-password" # 向collector提交时的http认证密码[""] # cli 配置 --tracing.jaeger=true --tracing.jaeger.samplingServerURL=http://localhost:5778/demo --tracing.jaeger.samplingType=const --tracing.jaeger.samplingParam=1.0 --tracing.jaeger.localAgentHostPort=127.0.0.1:6831 --tracing.jaeger.gen128Bit --tracing.jaeger.propagation=jaeger --tracing.jaeger.traceContextHeaderName=uber-trace-id --tracing.jaeger.collector.endpoint=http://127.0.0.1:14268/api/traces?format=jaeger.thrift --tracing.jaeger.collector.user=demo-user --tracing.jaeger.collector.password=demo-password
针对 Spring Boot 微服务,主要涉及以下步骤,具体如下所示:
1、整合 Jaeger ,引入 Maven 依赖
<dependency> <groupId>io.opentracing.contrib</groupId> <artifactId>opentracing-spring-jaeger-web-starter</artifactId> <version>3.3.1</version> </dependency>
2、连接属性配置
spring.application.name=demo opentracing.jaeger.enabled=true opentracing.jaeger.log-spans=false # opentracing.jaeger.enable128-bit-traces=true
3、其他参数配置,诸如,使用 JaegerAutoConfiguration 进行自动配置,以及 Log back 日志配置文件等,以便能够有效对服务请求链路进行全方位追踪。基于日志配置文件,以及结合官网给出的参考,主要针对自定义参数 traceId、spanId、sampled,当然这些参数也可以在 new MDCScopeManager.Builder() 时指定。具体可参考如下配置所示:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="INFO"> <Appenders> <Console name="console" target="SYSTEM_OUT"> <PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg% traceId=%X{traceId} spanId=%X{spanId} sampled=%X{sampled}%n" /> </Console> </Appenders> <Loggers> <Root level="debug" additivity="false"> <AppenderRef ref="console" /> </Root> </Loggers> </Configuration>
基于上述的配置,即可完成 Spring Boot 微服务的部署(Order Service、Payment Service、Account Service、Customer Service以及其他服务),相关服务的部署及源码暂不在本文中描述,后续将呈现。至此,在整个网络架构拓扑中,接入层 Traefik 和 服务层 Spring Boot 已完成 Jaeger 分布式链路追踪系统的接入,具体生成的相关依赖图如下所示:
此时,我们也可以看到各个服务之间的调用依赖以及接口请求的日志情况,具体如下所示:
针对服务层下游的组件(缓存层、基础中间件层、数据存储层等等)接入,将在后续的文章中进行各自单独解析。
综上所述,基于云原生生态领域的链路追踪系统 Jaeger ,在实际的业务场景中对于识别、定位及分析我们应用网络拓扑结构中服务间的链路调用的瓶颈其作用是不言而喻的,具有十分重要的参考意义。基于其所具备的“问题识别”、“信息追溯”等特征,使得我们在梳理服务之间的复杂依赖以及疑难问题分析面前能够迎刃而解。