译者 | 陈峻
审校 | 孙淑娟
无论您是否已经实现了微服务,您的系统往往会由反向代理、应用程序、以及数据库等多个组件组成。只要服务请求流经的组件数量越多,您对于监控的需求就越强烈。当然,监控只是状态跟踪的开始,您更需要一个能够横跨所有组件的聚合性视图,通过指标和日志两个维度,来实现可观察性。
1.W3C的规范
具有跟踪能力的解决方案通常能够通过异构技术栈,来规范化监控的标准格式。目前,市场上已经存在了几个不同的实现规范。而在大多数情况下,正如下面著名的XKCD漫画所描述的那样,一个新的规范需求往往会导致另一个额外规范的实施。
在此,我向你介绍一个新的、市场正在遵守的W3C规范--Trace Context。该规范定义了标准的HTTP标头和一种数值的格式,来支持分布式跟踪场景的上下文信息。毕竟,上下文信息不但能够唯一地标识分布式系统中的各种请求,而且定义了一种添加和传播提供者特定信息的方法。而Trace Context规范恰好标准化了上下文信息在服务之间的发送和修改方式。其中也包含了两个如下图所示的关键性概念:
- 跟踪(Trace)需要遵循跨越多个组件的请求路径。
- 跨越(Span)被绑定到了单个组件上,并通过父子关系,链接到另一个跨越。
目前Trace Context已经有了多种实现,而其中一种便是OpenTelemetry。
2.OpenTelemetry可作为黄金标准
OpenTelemetry是一种工具、API和SDK的集合,适用于多种语言。用户可以使用它来检测、生成、收集和导出指标、日志和跟踪等遥测类型的数据,以便分析软件的性能和行为。
OpenTelemetry是一个由CNCF管理的项目。在它之前已有如下两个项目:
- OpenTracing,顾名思义是专注于跟踪的
- OpenCensus,其目标是管理指标和跟踪
通过合并两个项目的日志功能,OpenTelemetry目前提供了一组专注于可观察性的层面。该层面具有如下特征:
- 通过多种语言检测API
- 用不同的语言规范化实现
- 提供诸如收集器等基础设施组件
- 提供诸如W3C Trace Context的互操作性格式
值得注意的是,OpenTelemetry虽然是Trace Context的一种实现,但是其功能更为广泛。Trace Context将自身限制在HTTP场景中,而OpenTelemetry则可以跨越到Kafka等非Web组件上。
3.用例
下面,让我们以某个电商网站为例,进行深入探讨。假设该电商网站是围绕微服务设计的,其中包含了管理产品的catalog和处理产品价格的pricing,这两个微服务。如下图所示,catalog是一个用Kotlin编写的Spring Boot应用,而pricing一个Python Flask应用。
每个微服务都可以通过REST API被访问到,并且受到API网关的保护。当用户访问该应用时,主页会从后台获取所有产品、及其价格的信息,予以呈现。而跟踪则可以让我们跨过微服务或数据库网关,去跟踪某个请求的路径。
4.网关处的跟踪
我们将使用Apache APISIX在入口点(也就是网关)处生成跟踪ID。此处的Apache APISIX提供了诸如:负载均衡、动态上游、金丝雀发布、断路器、身份验证、可观察性等,丰富的流量管理功能。同时,基于插件架构的Apache APISIX,通过提供OpenTelemetry插件,来根据OpenTelemetry的规范产生跟踪数据。不过,该插件仅支持基于HTTP的二进制编码--OLTP。
下面,让我们来配置opentelemetry插件:
#1:在独立模式下运行Apache APISIX,以便应用易于被理解。
#2:将opentelemetry配置为全局插件。
#3:设置服务的名称。它将成为出现在跟踪显示组件中的名称。
#4:将跟踪发送到jaeger服务处。我们将在下文中详细讨论。
为了跟踪每一条路由,我们需要通过如下代码,将插件设置为全局插件,而无需向每条路由添加插件:
#1:由于跟踪本身也会对性能产生影响,而且追踪的内容越多,影响也就越大,因此,我们全面仔细平衡性能影响与可观察性的收益。不过,在本例中,我们仍然希望能够跟踪每个请求。
5.收集、存储和显示跟踪
虽然Trace Context是一种W3C规范,而且OpenTelemetry是事实上的标准,但是目前市场上仍存在许多收集、存储和显示跟踪的解决方案。例如,Elastic技术栈虽然可以处理各种存储和显示,但是用户必须依靠其他产品进行收集;而Jaeger和Zipkin则能够通过一个完整的套件,全面实现收集、存储和显示跟踪。
早于OpenTelemetry的Jaeger和Zipkin虽然有着各自不同的跟踪传输格式,但是它们都能够与OpenTelemetry的格式相集成。鉴于Jaeger能够提供一个一体化的Docker镜像,而且每个功能都有其对应的组件,同时它们被嵌入在同一个镜像中,以方便配置。因此,我在此选用Jaeger。其镜像的相关端口分配如下:
端口 | 协议 | 组件 | 功能 |
16686 | HTTP | 查询 | 服务前端 |
4317 | HTTP | 收集器 | 如果启用的话,接受通过gRPC的OpenTelemetry协议(OTLP) |
4318 | HTTP | 收集器 | 如果启用的话,接受通过HTTP的OpenTelemetry协议 |
其Docker Compose的代码为:
#1:使用all-in-one的镜像。
#2:启用OpenTelemetry格式的收集器(非常重要)。
#3:暴露UI端口。
至此,我们已经完成了基础设施的构建,下面我们将专注于在应用中启用跟踪。
6.Flask应用的跟踪
pricing服务是一个简单的Flask应用程序。它提供了从数据库中获取单个产品与价格的端点。下面是对应的代码:
#1:端点
#2:路由需要产品的ID。
#3:使用SQLAlchemy从数据库中获取数据。
#4:在此,我们会随机产生价格。当然,真实定价引擎并非如此。
值得注意的是,为了观察跟踪,我们在此采用的是低效的、每次仅调用并获取单一价格的方式。而在现实生活中,路由应该能够接受多个产品的ID,并在一个“请求-响应”中获取所有相关的价格。
我们可以用自动和手动两种方式来检测应用。由于手动需要付出开发时间,而自动既省力又快速,因此我建议您从自动开始,如需添加手动的方式。
首先,我们需要添加几个Python包:
- opentelemetry-distro[otlp]==0.33b0
- opentelemetry-instrumentation
- opentelemetry-instrumentation-flask
接着,我们需要配置如下参数:
左右滑动查看完整代码
#1:将跟踪发送给Jaeger。
#2:设置服务的名称。它将成为出现在跟踪显示组件中的名称。
#3:在此,我们暂时忽略日志和指标。
然后,我们并不使用标准的flask run命令,而是将其进行如下包装:
至此,我们已经从方法调用和Flask路由中,收集到了跨越。
下面,我们以手动的方式,按需添加额外的跨度:
#1:使用配置的标签和属性添加一个额外的跨度。
7.Spring Boot应用的跟踪
catalog服务是用Kotlin开发的Reactive Spring Boot应用。它提供了如下两个端点:
- 获取单个产品
- 获取所有产品
两者都是先查看产品数据库,再查询上述pricing服务的价格。
而对于Python而言,我们同样可以利用自动和手动两种检测方法。在此,让我们先从唾手可得的自动化开始。在JVM上,我们通过一个代理来实现:
与Python一样,它为每个方法的调用和HTTP的入口点创建了跨越。同时,它还会检测JDBC的调用。在本例的Reactive栈中,我们使用的是R2DBC。因此,我们需要配置如下默认行为:
#1:将跟踪发送给Jaeger。
#2:设置服务的名称。它将成为出现在跟踪显示组件中的名称。
#3:在此,我们暂时忽略日志和指标。
而对于Python而言,我们直接添加手动检测。当然,目前有两个选项可被采用:程序化和基于注释。除非我们引入Spring Cloud Sleuth,否则前者会略显复杂。下面,让我们来添加额外的依赖性:
其对应的注释性代码为:
#1:使用配置的标签添加一个额外的跨越。
#2:将参数用作属性,将键(key)设置为id,其对应的值则设置为参数的运行时数值。
8.输出结果
下面,我们通过如下命令来查看该示例的结果:
通过如下Jaeger UI,我们可以找到两条跟踪(每次调用一条):
我们也可以深入研究单个跟踪的跨越:
值得注意的是,我们还可以在没有前文那张UML图表的情况下,推断出其数据流图。此类数据流图很好地显示了组件的内部调用。而且,每个跨越都包含了由自动与手动检测添加进来的属性:
9.小结
综上所述,我通过简单的示例,展示了如何使用OpenTelemetry,跨过微服务或数据库网关,实现对某个请求路径的端对端跟踪。
虽然在现实世界中,跟踪可能会涉及与HTTP无关的组件,例如Kafka和消息队列等,但是大多数系统仍然会以某种方式去依赖HTTP。而且,跨组件地跟踪HTTP请求,是实现系统可观察性的良好开端。
原文链接:https://dzone.com/articles/end-to-end-tracing-with-opentelemetry