应用程序通过 Envoy 代理和 Jaeger 进行分布式追踪(一)
Istio 支持通过 Envoy 代理进行分布式追踪,代理自动为其应用程序生成追踪 span,只需要应用程序转发适当的请求上下文即可。Istio 支持很多追踪系统,包括 Zipkin, Jaeger,Lightstep 和 Datadog,其中 Jaeger 目前已经成为 Istio 默认的分布式追踪工具。在我们的容器云平台中,我们采用了Istio + Jaeger的组合,为应用程序提供了低侵入性的链路追踪功能。本文将重点介绍如何通过Envoy代理和Jaeger实现应用程序的分布式追踪。
1、Envoy 分布式追踪 Jaeger 的实现原理
Envoy 原生支持 Jaeger,追踪所需 x-b3 开头的 Header 和 x-request-id 在不同的服务之间由业务逻辑进行传递,并由 Envoy 上报给 Jaeger,最终 Jaeger 生成完整的追踪信息。
为了将各种追踪 span 整合在一起以获得完整的追踪图,应用程序必须在传入和传出请求之间传播追踪上下文信息。特别是,Istio 依赖于应用程序传播 b3 追踪 Header 以及由 Envoy 生成的请求 ID,即应用程序服务请求时需携带这些 Header。这些 Header 包括:
- x-request-id: 这是一个唯一的请求标识符,用于标识单个请求。它有助于追踪请求,但不涉及分布式跟踪。在Istio中,这通常是由Envoy代理生成的,以便在服务之间传递请求标识。
- x-b3-traceid: 追踪ID,是一个在整个请求链中唯一的标识符。它用于标识一个请求在分布式系统中的路径。
- x-b3-spanId: 跨度ID,用于标识单个操作或跨度。每个服务处理请求时都会创建一个新的SpanId。它有助于将请求的不同部分关联起来。
- x-b3-parentspanid: 父跨度ID,指示当前操作的父操作的SpanId。这有助于构建操作之间的层次关系,以便更好地理解请求的调用链。
- x-b3-sampled:采样标志,用于指示是否对请求进行采样,以决定是否在分布式跟踪系统中记录该请求的信息。如果采样标志为 "1",则对该请求进行采样,如果为 "0",则不采样。
- x-b3-flags:标志位,用于标识请求的特定属性。在一些系统中,可能会使用这个标志位来传递额外的信息。
如果请求中没有 B3 HTTP Header,Istio Sidecar 代理(Envoy) 会自动生成初始化的 Headers。
在 Istio 中,Envoy 和 Jaeger 的关系如下:
上图中 Front Envoy 指的是第一个接收到请求的 Envoy Sidecar,它会负责创建 Root Span 并追加到请求 Header 内,请求到达不同的服务时,Envoy Sidecar 会将追踪信息进行上报。
2、应用程序通过 Envoy 代理和 Jaeger 进行分布式追踪
2.1 查看通过 Envoy 代理的应用程序会被自动生成初始化哪些 Headers
在第一章介绍 Envoy 分布式追踪 Jaeger 的实现原理时,我们知道 Envoy 原生支持 Jaeger,追踪所需 x-b3 开头的 Header 和 x-request-id 在不同的服务之间由业务逻辑进行传递,并由 Envoy 上报给 Jaeger,最终 Jaeger 生成完整的追踪信息。如果请求中没有 B3 HTTP Header,Istio Sidecar 代理(Envoy) 会自动生成初始化的 Headers。在这一小节,我们将看一下通过 Envoy 代理的应用程序会被自动生成初始化哪些 Headers。
(1) 构建个简单镜像能够打印 HTTP 请求信息
以下是一个示例,使用 Go 语言来创建一个简单的 HTTP 服务器,并在其中打印请求信息:
1)创建一个名为 main.go 的 Go 源代码文件,内容如下:
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // 打印请求信息 fmt.Println("Request Method:", r.Method) fmt.Println("Request URL:", r.URL.String()) fmt.Println("Request Headers:") for header, value := range r.Header { fmt.Printf(" %s: %v\n", header, value) } fmt.Println("Request Body:", r.Body) // 返回响应 w.WriteHeader(http.StatusOK) w.Write([]byte("Hello, World!")) }) log.Fatal(http.ListenAndServe(":80", nil)) }
2)创建一个名为 Dockerfile 的文件,内容如下:
# 使用 Golang 官方的 Golang 镜像作为基础镜像 FROM golang:1.17 # 设置工作目录 WORKDIR /app # 将 main.go 文件复制到容器中的 /app 目录下 COPY ./main.go . # 编译 Go 代码 RUN go build -o http_request_printer main.go # 在容器启动时运行 Go 应用 CMD ["./http_request_printer"]
3)使用以下命令构建 Docker 镜像:
docker build -t http_request_printer .
4)构建完成后,可以使用以下命令运行容器:
docker run -p 8080:80 http_request_printer
注意 1:如果宿主机暴露 18080 端口,不要用谷歌浏览器或者谷歌浏览器里面的 Postman 插件进行测试,因为谷歌浏览器默认会禁用 18080 端口。
5)修改镜像 tag ,将镜像推送到镜像仓库,此步骤比较简单,忽略命令。
6)通过 Postman 测试访问此容器:
7)通过容器日志查看请求信息:
可以看到除了自定义 Header 外,谷歌浏览器里面的 Postman 插件帮我们加了一些 Header 信息,但是可以很明确的看到这些 Header 都和链路追踪没有关系。
(2) 验证通过 Envoy 代理的应用程序会被自动生成初始化哪些 Headers
1)定义 http_request_printer 服务声明式配置文件
# http_request_printer_deploy_svc.yaml apiVersion: apps/v1 kind: Deployment metadata: namespace: tracing labels: version: v1 app: http-request-printer name: http-request-printer-v1 spec: replicas: 1 selector: matchLabels: version: v1 app: http-request-printer template: metadata: labels: version: v1 app: http-request-printer spec: containers: - name: container-rr19ea imagePullPolicy: IfNotPresent image: '10.20.32.201:80/library/http_request_printer' ports: - name: http-80 protocol: TCP containerPort: 80 servicePort: 80 serviceAccount: default affinity: {} initContainers: [] volumes: [] imagePullSecrets: - name: harbor strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 25% maxSurge: 25% --- apiVersion: v1 kind: Service metadata: namespace: tracing labels: version: v1 app: http-request-printer name: http-request-printer spec: sessionAffinity: None selector: app: http-request-printer template: metadata: labels: version: v1 app: http-request-printer ports: - name: http-80 protocol: TCP port: 80 targetPort: 80 type: NodePort
2)在 Kubernetes 集群中部署 http_request_printer 服务
kubectl apply -f http_request_printer_deploy_svc.yaml
3)查看服务访问信息,并通过 Postman 访问此服务
[root@master1 ~]# kubectl get svc -n=tracing NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE http-request-printer NodePort 10.234.131.36 <none> 80:32513/TCP 74s
4)通过容器日志查看请求信息:
可以看到除了自定义 Header 外,谷歌浏览器里面的 Postman 插件帮我们加了一些 Header 信息,但是可以很明确的看到这些 Header 都和链路追踪没有关系。
5)修改应用deployment配置让注入边车
#修改Pod模板标签,添加边车注入标签 sidecar.istio.io/inject: 'true' labels: app: http-request-printer sidecar.istio.io/inject: 'true' version: v1
6)Pod重启后,确定容器已注入边车
7)注入边车后,再通过 Postman 访问此服务
查看容器日志可以看到多了5个和链路追踪相关的 Header 数据。
由此证明,如果请求中没有 B3 HTTP Header,Istio Sidecar 代理(Envoy) 会自动生成初始化的 Headers。
8)可以看到 x-b3-sampled 值为1,代表 Jaeger 会记录该请求的信息
3、总结
Envoy 原生支持 Jaeger,追踪所需 x-b3 开头的 Header 和 x-request-id 在不同的服务之间由业务逻辑进行传递,并由 Envoy 上报给 Jaeger,最终 Jaeger 生成完整的追踪信息。
为了将各种追踪 span 整合在一起以获得完整的追踪图,应用程序必须在传入和传出请求之间传播追踪上下文信息。特别是,Istio 依赖于应用程序传播 b3 追踪 Header 以及由 Envoy 生成的请求 ID,即应用程序服务请求时需携带这些 Header。
如果请求中没有 B3 HTTP Header,Istio Sidecar 代理(Envoy) 会自动生成初始化的 Headers。
参考:分布式追踪