Jaeger 受Dapper和OpenZipkin的启发, 是Uber Technologies以开源形式发布的分布式跟踪系统。它用于监控和故障排除基于微服务的分布式系统, 包括:
- 分布式上下文传播
- 分布式事务监控
- 根本原因分析
- 服务依赖分析
- 性能/延迟优化
Uber 发表了一篇博客文章, 在 Uber 上演进分布式跟踪, 其中解释了 Jaeger 中做出架构选择的历史和原因。Jaeger 的创建者Yuri Shkuro还出版了一本书Mastering Distributed Tracing, 深入介绍了 Jaeger 设计和操作的许多方面, 以及一般的分布式跟踪。
0. 什么是分布式跟踪和 OpenTracing
分布式跟踪是一种基于微服务架构的监控和分析系统的技术, 由 X-Trace、Google’s Dapper和Twitter’s Zipkin 等系统推广。其基础是分布式上下文传播的概念, 它涉及将某些元数据与进入系统的每个请求相关联, 并在请求执行扇出到其他微服务时跨线程和进程边界传播该元数据。如果我们为每个入站请求分配一个唯一 ID 并将其作为分布式上下文的一部分携带, 那么我们可以将来自多个线程和多个进程的各种分析数据拼接成一个“跟踪”, 该“跟踪”代表我们系统对请求的执行.
分布式跟踪需要使用分析挂钩和上下文传播机制对应用程序代码(或其使用的框架)进行检测。2015 年 10 月, 一个新的社区诞生了OpenTracing API, 这是一个开放的、供应商中立的、与语言无关的分布式跟踪标准。你可以阅读本·西格尔曼关于OpenTracing的文章 the motivations and design principles behind OpenTracing, 了解更多关于它的信息。
1. 特征
1.1 OpenTracing兼容的数据模型和检测库
Jaeger 后端、Web UI 和检测库从头开始设计, 以支持OpenTracing 标准(Go, Java, Node, Python, C++ and C#)。
- 通过跨度引用将迹线表示为有向无环图(不仅仅是树)
- 支持强类型跨度标签和结构化日志
- 通过baggage支持通用的分布式上下文传播机制
1.2 对每个服务/端点的概率使用一致的前期抽样
1.3 多个存储后端
Jaeger 支持两种流行的开源 NoSQL 数据库作为跟踪存储后端: Cassandra 和 Elasticsearch。还有使用Badger的嵌入式数据库支持。使用其他数据库的社区实验正在进行中, 例如 ScyllaDB、InfluxDB、Amazon DynamoDB。Jaeger 还附带了一个简单的内存存储, 用于测试设置。
1.4 拓扑图
Jaeger UI 支持两种类型的服务图: 系统架构和深度依赖图
1.4.1 系统架构
架构中观察到的所有服务的“经典”服务依赖图。该图仅表示服务之间的一跳依赖关系, 类似于从服务网格生成的遥测中可以得到的。例如, 一个图A - B - C
意味着有一些跟踪包含A和B之间的网络调用, 以及一些跟踪包含B和C之间的调用。但是, 这并不意味着任何痕迹都包含完整的链A - B - C
, 即我们不能说A依赖于C.
此图的节点粒度仅为服务, 而不是服务端点。
系统架构图可以从内存存储中即时构建, 或者在使用分布式存储时使用 Spark 或 Flink 作业.
1.4.2 深度依赖图
也称为“传递依赖图”, 其中链A -> B -> C
表示A对C. 单个图表需要“焦点”服务(以粉红色显示)并且仅显示通过该服务的路径。通常, 这种类型的图并不代表系统的完整架构, 除非有一个服务连接到所有东西, 例如 API 网关, 并且它被选为焦点服务。
该图的节点粒度可以在服务和服务端点之间更改。在后一种模式下, 同一服务中的不同端点将显示为单独的节点, 例如A::op1和A::op2。
此时传递图只能从搜索结果中的迹线构造。将来会有一个 Flink 作业, 通过聚合所有跟踪来计算图形。
1.5 自适应采样
1.6 采集后数据处理管道(即将推出)
1.7 其他
1.7.1 高可扩展性
Jaeger 后端设计为没有单点故障, 并且可以根据业务需求进行扩展。例如, Uber 的任何给定 Jaeger 安装通常每天处理数十亿个 span。
1.7.2 现代网络用户界面
Jaeger Web UI 使用流行的开源框架(如 React)在 Javascript 中实现。v1.0 中发布了多项性能改进, 以允许 UI 有效地处理大量数据并显示具有数万个跨度的跟踪(例如, 我们尝试了具有 80,000 个跨度的跟踪)。
1.7.3 云原生部署
Jaeger 后端作为 Docker 镜像的集合分发。二进制文件支持各种配置方法, 包括命令行选项、环境变量和多种格式(yaml、toml 等)的配置文件。Kubernetes 模板 和Helm 图表协助部署到 Kubernetes 集群。
1.7.4 可观察性
默认情况下, 所有 Jaeger 后端组件都会公开Prometheus指标(也支持其他指标后端)。使用结构化日志库zap将日志写入标准输出。
1.7.5 安全
Jaeger 的第三方安全审计可在https://github.com/jaegertracing/security-audits获得。有关 Jaeger 中可用安全机制的摘要, 请参阅问题 #1718。
1.7.6 向后兼容 Zipkin
尽管我们建议使用 OpenTelemetry 检测应用程序, 但如果您的组织已经投资使用 Zipkin 库进行检测, 则您不必重写所有代码。Jaeger 通过 HTTP 接受 Zipkin 格式(Thrift 或 JSON v1/v2)的跨度, 提供与 Zipkin 的向后兼容性。从 Zipkin 后端切换只是将流量从 Zipkin 库路由到 Jaeger 后端的问题。
2. Jaeger Architecture
2.1 Jaeger 组件
Jaeger 可以部署为一体式二进制文件, 其中所有 Jaeger 后端组件在单个进程中运行, 也可以部署为可扩展的分布式系统, 如下所述。有两个主要的部署选项:
- 收集器直接写入存储。
- 收集器正在写入 Kafka 作为初步缓冲区。
2.1.1 Jaeger 客户端库
Jaeger 客户端是OpenTracing API的特定语言实现。它们可用于手动检测应用程序以进行分布式跟踪, 也可以使用已经与 OpenTracing 集成的各种现有开源框架(例如 Flask、Dropwizard、gRPC 等)来检测应用程序。
检测服务在接收新请求时创建跨度并将上下文信息(跟踪id、跨度id 和行李)附加到传出请求。只有 id 和 bag 会随请求传播; 所有其他分析数据, 如操作名称、时间、标签和日志, 都不会传播。相反, 它在后台异步传输到 Jaeger 后端。
2.1.2 Jaeger代理
Jaeger Agent是一个网络守护程序, 它侦听通过 UDP 发送的跨度, 并将其批处理并发送给收集器。它旨在作为基础架构组件部署到所有主机。代理将收集器的路由和发现从客户端抽象出来。
2.1.3 Collector
Jaeger Collector从 Jaeger Agent接收跟踪并通过处理管道运行它们。目前, 我们的管道验证跟踪、索引它们、执行任何转换并最终存储它们。
2.2 采样 Sampling
2.2.1 客户端采样配置
Jaeger 库支持以下采样器:
- Constant(sampler.type=const) 采样器总是对所有轨迹做出相同的决定。它要么对所有轨迹进行采样(sampler.param=1), 要么不采样(sampler.param=0)。
- Probabilistic(sampler.type=probabilistic) 采样器做出随机采样决策, 采样概率等于sampler.param属性值。例如, sampler.param=0.1将采样大约十分之一的迹线。
- Rate Limiting(sampler.type=ratelimiting) 采样器使用漏桶速率限制器来确保以某个恒定速率对轨迹进行采样。例如, 当sampler.param=2.0它以每秒 2 次跟踪的速率对请求进行采样时。
- Remote(sampler.type=remote, 也是默认值)采样器向 Jaeger 代理咨询要在当前服务中使用的适当采样策略。这允许从 Jaeger 后端的中央配置控制服务中的采样策略, 甚至可以动态控制(请参阅自适应采样)。
2.2.2 采集器采样配置
如果您的客户端配置为使用远程采样, 则可以通过收集器集中控制采样率。在远程采样设置中, 将向 Jaeger 客户端提供一个描述端点及其采样概率的 json 文档。该文档可以通过两种不同的方式生成: 从文件中定期加载或基于流量动态加载。文档生成的方法由环境变量控制SAMPLING_CONFIG_TYPE, 可以设置为file(默认)或adaptive.
文件采样
可以使用--sampling.strategies-file
指向包含要提供给 Jaeger 客户端的采样策略的文件的选项来实例化收集器。该选项的值可以包含 JSON 文件的路径, 如果其内容发生更改, 该文件将自动重新加载, 或者包含将定期检索文件的 HTTP URL, 重新加载频率由--sampling.strategies-reload-interval
选项控制。
如果未提供配置, 则收集器将返回所有服务的默认概率抽样策略, 概率为 0.001 (0.1%)。
{
"service_strategies": [
{
"service": "foo",
"type": "probabilistic",
"param": 0.8,
"operation_strategies": [
{
"operation": "op1",
"type": "probabilistic",
"param": 0.2
},
{
"operation": "op2",
"type": "probabilistic",
"param": 0.4
}
]
},
{
"service": "bar",
"type": "ratelimiting",
"param": 5
}
],
"default_strategy": {
"type": "probabilistic",
"param": 0.5,
"operation_strategies": [
{
"operation": "/health",
"type": "probabilistic",
"param": 0.0
},
{
"operation": "/metrics",
"type": "probabilistic",
"param": 0.0
}
]
}
}
2.2.3 自适应采样 Adaptive Sampling
自 Jaeger v1.27 起, 自适应采样在 Jaeger 收集器中工作, 通过观察从服务接收的跨度并重新计算每个服务/端点组合的采样概率, 以确保收集的跟踪量匹配–sampling.target-samples-per-second。当检测到新服务或端点时, 首先对其进行采样, –sampling.initial-sampling-probability直到收集到足够的数据来计算适合通过端点的流量的速率。
自适应采样需要一个存储后端来存储观察到的流量数据和计算的概率。目前memory(用于一体式部署)并cassandra支持作为采样存储后端。我们正在寻求帮助以实现对其他后端的支持(跟踪问题)。
3. OpenTelemetry、OpenTracing 以及 OpenCensus
3.1 OpenTracing
OpenTracing制定了一套平台无关、厂商无关的Trace协议, 使得开发人员能够方便的添加或更换分布式追踪系统的实现。在2016年11月的时候CNCF技术委员会投票接受OpenTracing作为Hosted项目, 这是CNCF的第三个项目, 第一个是Kubernetes, 第二个是Prometheus, 可见CNCF对OpenTracing背后可观察性的重视。比如大名鼎鼎的Zipkin、Jaeger都遵循OpenTracing协议。
3.2 OpenCensus
OpenCensus的发起者是谷歌, 也就是最早提出Tracing概念的公司, 而OpenCensus也就是Google Dapper的社区版。OpenCensus和OpenTracing最大的不同在于除了Tracing外, 它还把Metrics也包括进来, 这样也可以在OpenCensus上做基础的指标监控; 还一点不同是OpenCensus并不是单纯的规范制定, 他还把包括数据采集的Agent、Collector一股脑都搞了。OpenCensus也有众多的追随者, 最近最大的新闻就是微软也宣布加入, OpenCensus可谓是如虎添翼。
3.3 OpenTelemetry
OpenTelemetry合并了OpenTracing和OpenCensus项目, 提供了一组API和库来标准化遥测数据的采集和传输。OpenTelemetry提供了一个安全, 厂商中立的工具, 这样就可以按照需要将数据发往不同的后端。OpenTelemetry项目由如下组件构成:
- 推动在所有项目中使用一致的规范
- 基于规范的, 包含接口和实现的APIs
- 不同语言的SDK(APIs的实现), 如 Java, Python, Go, Erlang等
- Exporters: 可以将数据发往一个选择的后端
- Collectors: 厂商中立的实现, 用于处理和导出遥测数据
3.3.1 Opentelemetry 术语
Traces: 记录经过分布式系统的请求活动, 一个trace是spans的有向无环图
Spans: 一个trace中表示一个命名的, 基于时间的操作。Spans嵌套形成trace树。每个trace包含一个根span, 描述了端到端的延迟, 其子操作也可能拥有一个或多个子spans。
Metrics: 在运行时捕获的关于服务的原始度量数据。Opentelemetry定义的metric instruments。Observer支持通过异步API来采集数据, 每个采集间隔采集一个数据。
Context: 一个span包含一个span context, 它是一个全局唯一的标识, 表示每个span所属的唯一的请求, 以及跨服务边界转移trace信息所需的数据。OpenTelemetry 也支持correlation context, 它可以包含用户定义的属性。correlation context不是必要的, 组件可以选择不携带和存储该信息。
Context propagation: 表示在不同的服务之间传递上下文信息, 通常通过HTTP首部。 Context propagation是Opentelemetry系统的关键功能之一。除了tracing之外, 还有一些有趣的用法, 如, 执行A/B测试。OpenTelemetry支持通过多个协议的Context propagation来避免可能发生的问题, 但需要注意的是, 在自己的应用中最好使用单一的方法。
3.3.2 OpenTelemetry的好处
通过将OpenTracing 和OpenCensus 合并为一个开放的标准, OpenTelemetry提供了如下便利:
- 选择简单: 不必在两个标准之间进行选择, OpenTelemetry可以同时兼容 OpenTracing和OpenCensus。
- 跨平台: OpenTelemetry 支持各种语言和后端。它代表了一种厂商中立的方式, 可以在不改变现有工具的情况下捕获并将遥测数据传输到后端。
- 简化可观测性: 正如OpenTelemetry所说的"高质量的观测下要求高质量的遥测"。希望看到更多的厂商转向OpenTelemetry, 因为它更方便, 且仅需测试单一标准。
4. Demo(OpenTelemetry)
4.1 通用函数
func tracerProvider(url string, serv ServInfo) (*tracesdk.TracerProvider, error) {
exp, err := jaegerimp.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
if err != nil {
return nil, err
}
tp := tracesdk.NewTracerProvider(
tracesdk.WithBatcher(exp),
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serv.Name),
attribute.String("environment", "demo"),
attribute.Int64("ID", serv.ID),
)),
)
return tp, nil
}
func requestService(ctx context.Context, servName, url string) ([]byte, error) {
tr := otel.Tracer(servName)
ctx2, span := tr.Start(ctx, servName)
span.SetAttributes(attribute.Key("testset").String("value"))
defer span.End()
resp, err := otelhttp.Get(ctx2, url)
if err != nil {
onError(span, err)
return nil, err
}
defer resp.Body.Close()
resBody, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(resBody))
return resBody, nil
}
func onError(span trace.Span, err error) {
// span.SetTag(string(ext.Error), true)
// span.LogKV(otlog.Error(err))
span.SetAttributes(attribute.Key("error").String(err.Error()))
log.Print(err)
}
tp, err := tracerProvider("http://192.168.137.3:14268/api/traces", serv)
if err != nil {
panic(err)
}
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
4.2 服务处理的封装
func serviceA(serv string) {
mux := http.NewServeMux()
handler := http.HandlerFunc(handlerServeA)
wrappedHandler := otelhttp.NewHandler(handler, "handlerServeA")
mux.Handle("/ping", wrappedHandler)
log.Println("Sever On:", serv)
log.Fatal(http.ListenAndServe(serv, mux))
}
4.3 微服务A、B和C
func handlerServeA(w http.ResponseWriter, r *http.Request) {
url := fmt.Sprintf("http://%s/ping", jaegerServeList["B"].Host)
data, err := requestService(r.Context(), "requestServiceB", url)
if err != nil {
log.Println("requestServiceB error:", err)
return
}
log.Println("requestServiceB successed.")
w.Write(data)
}
func handlerServeB(w http.ResponseWriter, r *http.Request) {
url := fmt.Sprintf("http://%s/ping", jaegerServeList["C"].Host)
data, err := requestService(r.Context(), "requestServiceC", url)
if err != nil {
log.Println("requestServiceC error:", err)
w.Write([]byte("requestServiceC error"))
return
}
log.Println("requestServiceC successed.")
w.Write(data)
}
func handlerServeC(w http.ResponseWriter, r *http.Request) {
log.Print("Received /ping request")
t := time.Now()
ts := t.Format("Mon Jan _2 15:04:05 2006")
io.WriteString(w, fmt.Sprintf("The time is %s", ts))
}
4.4 演示效果
4.5 Pyhton Demo (OpenTelemetry)
from opentelemetry import trace
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.wsgi import collect_request_attributes
from opentelemetry.propagate import extract
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from dotenv import load_dotenv
import os
load_dotenv()
trace.set_tracer_provider(
TracerProvider(
resource=Resource.create({SERVICE_NAME: "service-python"})
)
)
tracer = trace.get_tracer(__name__)
# create a JaegerExporter
jaeger_exporter = JaegerExporter(
# configure agent
agent_host_name= os.getenv('AGENT_HOST_NAME'),
agent_port= int(os.getenv('AGENT_PORT')),
)
span_processor = BatchSpanProcessor(jaeger_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
from flask import Flask,request,jsonify
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()
tracer = trace.get_tracer(__name__)
@app.route("/ping")
def hello_world():
with tracer.start_as_current_span(
"server_request",
context=extract(request.headers),
kind=trace.SpanKind.SERVER,
attributes=collect_request_attributes(request.environ),
) as span:
return "Hello, World"