原文作者:Elijah Zupancic of F5 和 Jason Schmidt of F5
原文链接:现代应用参考架构之 OpenTelemetry 集成进展报告
转载来源:NGINX 官方网站
去年秋天我们在 Sprint 2.0 上介绍 NGINX 现代应用参考架构 (MARA) 项目时,就曾强调过这不是一个随随便便的架构,我们的初衷是打造一款“稳定可靠、经过测试且可以部署到在 Kubernetes 环境中运行的实时生产应用”的解决方案。对于这样一个项目来说,可观测性工具可以说是必不可少。MARA 团队的所有成员都曾亲身体验过,缺乏状态和性能洞察会让应用开发和交付变得多么困难。我们很快就达成了共识,即 MARA 必须添加可以在生产环境中进行调试和跟踪的工具。
MARA 的另一项指导原则是首选开源解决方案。本文描述了我们对多功能开源可观测性工具的追求是如何使我们将目光转向 OpenTelemetry 的,然后详细介绍了其中的利弊权衡和设计决策,以及采用了哪些技术将 OpenTelemetry 与使用 Python、Java 和 NGINX 构建的微服务应用相集成。
我们希望我们的经验可以帮助您避开可能的陷阱,并加快您对 OpenTelemetry 的采用。请注意,本文是一份具有时效性的进展报告 —— 我们讨论的技术预计将在一年内成熟。此外,尽管我们指出了某些项目当前的不足之处,但我们仍然对所有已经完成的开源工作深表感激,并期待它们未来取得更好的进展。
我们的应用
我们选择了 Bank of Sirius 作为集成可观测性解决方案的应用。Bank of Sirius 是我们从 Google 提供的 Bank of Anthos 示例应用引出的分支,是一个具有微服务架构的 Web 应用,可以通过基础架构即代码进行部署。虽然我们可以通过很多种方式来改进此应用的性能和可靠性,但它已经足够成熟,可以被合理地认为是一种“棕地”应用。因此,我们认为这是一个展示如何将 OpenTelemetry 集成到应用中的好例子,因为理论上分布式跟踪可以生成有关应用架构缺点的宝贵洞察。
我们是怎么选择了 OpenTelemetry
我们选择 OpenTelemetry 的道路相当曲折,经历了几个阶段。
创建功能清单
在评估可用的开源可观测性工具之前,我们确定了哪些方面需要关注。根据过去的经验,我们列出了以下清单。
- 日志记录 —— 顾名思义,这意味着从应用中生成经典的、以换行符分隔的消息集;Bank of Sirius 应用会以 Bunyan 格式构建日志
- 分布式跟踪 —— 整个应用中每个组件的Timing和元数据,例如供应商提供的应用性能管理 (APM)
- 指标 —— 在一段时间内捕获并以时间序列数据的形式绘制的测量值/li>
- 异常/错误聚合和通知 —— 一个聚合了最常见的异常和错误的集合,该集合必须是可搜索的,以便我们可以确定哪些应用错误是最常见的错误
- 健康检查 —— 发送给 service 的定期探针,用于确定它们是否在应用中正常运行
- 运行时状态自检 —— 一组仅对管理员可见的 API,可以返回有关应用运行时状态的信息
- 堆转储/核心转储 —— service运行时状态的综合快照;考虑到我们的目的,很重要的一点就是看在需要时或在 service 崩溃时获取这些堆转储的难易程度
对照功能清单比较工具功能
当然,我们并不指望一个开源工具或一种方法就能搞定所有功能,但至少它为我们提供了一个比较可用工具的理想依据。我们查阅了每个工具的文档,确定了各个工具可支持七项功能清单中的哪些功能。下表对我们的发现进行了总结。
技术 | 日志记录 | 分布式跟踪 | 指标 | 错误聚合 | 健康检查 | 运行时自检 | 堆/核心转储 |
---|---|---|---|---|---|---|---|
ELK + Elastic APM | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
Grafana | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
Graylog | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
Jaeger | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
OpenCensus | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
OpenTelemetry | Beta | ✅ | ✅ | ✅ | ✅* | ❌ | ❌ |
Prometheus | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
StatsD | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
Zipkin | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
*作为一项扩展功能
做完这张表格后,我们突然意识到一个问题。各个工具的功能和预期用途差别很大,我们根本不能将它们归为一类,这样的比较可以说是驴唇不对马嘴!
举例来说,ELK(Elasticsearch-Logstash-Kibana,再加上 Filebeat)和 Zipkin 的作用具有根本性的不同,两相比较只会让人摸不着头脑。遗憾的是,为了响应用户请求,有些工具无疑还会添加一些次要的功能,从而与其他工具产生功能上的重叠,这就是臭名昭著的“任务蔓延”问题。从表面上看,ELK 主攻日志存储和可视化,Zipkin 擅长分布式跟踪。但如果您稍微深入研究一下 Elastic 产品组合,您很快就会发现其中的 Elastic APM 也支持分布式跟踪,甚至兼容 Jaeger。
除了任务蔓延的问题外,许多工具还可以相互集成,从而产生各种不同的收集器、聚合器、仪表板等功能组合。有些技术相互兼容,有些则不兼容。
执行定性调查
综上所述,这个比较表格无法帮助我们足够准确地做出选择。考虑到一个项目的价值观与我们的价值观越相似,我们就越有可能在一段时间内保持兼容,我们决定对每个项目的目标、指导原则和可能的未来方向进行定性调查。访问 OpenCensus 页面时,我们一眼就发现了这段话。
OpenCensus 和 OpenTracing 合并形成了 OpenTelemetry,以作为 OpenCensus 和 OpenTracing 的下一个主版本。OpenTelemetry 将向后兼容现有的 OpenCensus 集成,我们将在未来两年内继续为现有 OpenCensus 库打安全补丁。
这条信息对我们来说很关键。我们知道我们无法保证我们的选择一定会经得起未来考验,但我们至少要知道未来它是否有坚实的社区支持。有了这些信息,我们就可以将 OpenCensus 从候选名单中剔除了。使用 Jaeger 可能也不是一个好主意,因为它是现已正式弃用的 OpenTracing 项目的参考实现 — 大部分新贡献将落在 OpenTelemetry 上。
然后我们查阅了 OpenTelemetry Collector 的网页。
OpenTelemetry Collector 针对如何接收、处理和导出遥测数据提供了的实现,并且不受供应商的限制。此外,它无需运行、操作和维护多个代理/收集器,即可支持将开源遥测数据格式(例如 Jaeger、Prometheus 等)发送到多个开源或商用后台系统。
OpenTelemetry Collector 充当聚合器,允许我们使用不同的后端来混搭不同的可观测性收集和检测方法。简单来说,我们的应用可以使用 Zipkin 收集跟踪信息、使用 Prometheus 获取指标,这些信息都可以被发送到可配置的后端,然后我们再使用 Grafana 对其进行可视化。这种设计的其他一些排列组合也是可行的,因此我们可以多尝试几种方法,看看究竟哪些技术适合我们的用例。
选定 OpenTelemetry 集成作为前进方向
我们最终将目光落在了 OpenTelemetry Collector 上,因为理论上它允许我们切换不同的可观测性技术。尽管该项目相对不那么成熟,但我们决定大胆一试,使用仅具有开源集成的 OpenTelemetry——毕竟东西好不好,用了才知道。
但是,OpenTelemetry Collector 还是缺少一些功能,我们必须添加其他技术来填补这些缺口。以下小节总结了我们的选择及其背后的原因。
实施日志记录
在选择可观测性工具时,日志记录这一要素看似简单,实则并不好抉择。说它简单,是因为您只需从容器中获取日志输出即可;说它复杂,是因为您需要决定将数据存储在哪里、如何将其传输到该存储库、如何进行索引才能使其发挥效用,以及这些数据需要保留多长时间。只有支持根据足够多的不同标准(以满足不同搜索者的需求)轻松进行搜索,日志文件才能发挥效用。
我们研究了 OpenTelemetry Collector 的日志记录支持,在撰写本文时它还处于 Beta 测试阶段。我们决定短期内使用 ELK 进行日志记录,同时继续调查其他选择。
在没有更好的选择之前,我们默认使用 Elasticsearch 工具,以便将部署分为ingest、coordinating、master 和 data 节点。为了便于部署,我们使用了 Bitnami 图表。为了传输数据,我们将 Filebeat 部署到 Kubernetes DaemonSet 中。通过部署 Kibana 并利用预加载的索引、搜索、可视化和仪表盘,我们轻松添加了搜索功能。
很快我们便发现,尽管这个解决方案可行,但其默认配置非常耗费资源,因此很难在 K3S 或 Microk8s 等资源占用较小的环境中运行。虽然我们通过调整每个组件的副本数量解决了上述问题,但却出现了一些故障,这些故障可能是由于资源耗尽或数据量过多所致。
我们并未因此气馁,而是将借机对采用不同配置的日志记录系统进行基准测试,并研究其他选项,如 Grafana Loki 和 Graylog。我们很可能会发现轻量级的日志记录解决方案无法提供某些用户需要的全套功能,而这些功能可能为资源密集型工具所提供。鉴于 MARA 的模块化特性,我们可能会为这些选项构建额外的模块,从而为用户提供更多的选择。
实施分布式跟踪
除了需要确定哪种工具可提供我们所需的跟踪功能之外,我们还需考虑解决方案的实施方式以及需要与之集成的技术。
首先,我们希望确保任何工具都不会影响应用本身的服务质量。我们都有过这样的经历:在导出日志时,系统性能每小时就会下降一次,因此我们不想再重蹈覆辙。在这方面,OpenTelemetry Collector 的架构引人注目,因为它支持您在每个主机上运行一个实例。每个收集器从主机上运行的所有不同应用(容器化或其他类型)中的客户端和代理中接收数据。收集器会整合,并(可能)会压缩这些数据,然后将其发送到存储后端。这听起来棒极了。
接下来,我们评估了应用中所用的不同编程语言和框架对 OpenTelemetry 的支持。此时,事情变得有点棘手。尽管我们只使用了两种编程语言和相关框架(如下表所示),但复杂程度之高令人咂舌。
语言 | 框架 | service数量 |
---|---|---|
Java | Spring Boot | 3 |
Python | Flask | 3 |
为了添加语言级跟踪,我们首先尝试了 OpenTelemetry 的自动检测代理,但却发现其输出数据混乱不堪。我们相信,随着自动检测库的日趋成熟,这种情况会有所改善。但目前我们不考虑 OpenTelemetry 代理,并决定将跟踪语句加入我们的代码。
在开始在代码中直接实现跟踪之前,我们首先连接了 OpenTelemetry Collector,以便将所有跟踪数据输出到本地运行的 Jaeger 实例,从而更轻松地从中查看输出结果。这样做有很大帮助,因为当我们了解如何全面集成 OpenTelemetry 时,便可使用跟踪数据的可视图像。例如,如果在调用从属服务时发现 HTTP 客户端库不含跟踪数据,我们可以立即将该问题添加到修复列表中。Jaeger 在单个跟踪中清晰呈现了所有不同跨度:
Python 分布式跟踪
在我们的 Python 代码中添加跟踪语句相对来说比较简单。我们添加了两个我们所有 service 都引用的 Python 源文件,并更新了各自的 requirements.txt 文件,以添加相关的 opentelemetry-instrumentation-* 依赖项。这意味着我们不仅可以对所有 Python 服务使用相同的跟踪配置,而且还能够将每个请求的跟踪 ID 添加到日志消息中,并将跟踪 ID 嵌入从属 service 的请求。
Java 分布式跟踪
接下来,我们将视线转向 Java 服务。在“绿地”项目中直接使用 OpenTelemetry Java 库相对简单 —— 您只需导入必要的库并直接使用跟踪 API 即可。但是,如果您也和我们一样使用 Spring,则需做出其他决定。
Spring 已有一个分布式跟踪 API —— Spring Cloud Sleuth。它为底层分布式跟踪实现提供了一个 façade,并具有以下功能(如文档中所述):
- 将跟踪和跨度 ID 添加到 Slf4J MDC,以便从日志聚合器中的给定跟踪或跨度中提取所有日志。
- 检测 Spring 应用的通用进出点(servlet filter、rest template、scheduled actions、message channels、feign client)。
- 如果 spring-cloud-sleuth-zipkin 可用,……,则通过 HTTP (生成并报告)兼容 Zipkin 格式的跟踪数据。默认情况下,它将这些数据发送到 localhost(端口 9411)上的 Zipkin 收集器服务。您可使用 spring.zipkin.baseUrl 配置服务的位置。
此外,该 API 还支持我们将跟踪数据添加到 @Scheduled 注解的任务。
换句话说,我们只使用 Spring Cloud Sleuth 便可从一开始就获得 HTTP 服务端点级的跟踪数据 —— 这堪称一大优势。由于我们的项目使用了 Spring,因此我们决定全面拥抱该框架并利用其所提供的功能。但在使用 Maven 将这一切连接在一起时,我们发现了一些问题:
- Spring Cloud Sleuth Autoconfigure 模块仍然处于里程碑版本。
- Spring Cloud Sleuth Autoconfigure 模块依赖于过时的内测版opentelemetry-instrumentation-api。该库目前没有最新的非内测 1.x 版本。
- 鉴于 Spring Cloud Sleuth 为里程碑版本编码其依赖项引用的方式,该项目必须从 Spring Snapshot 存储库中拉取父项目对象模型 (POM)spring-cloud-build。
我们的 Maven 项目定义因此变得有些复杂,因为我们必须从 Spring 存储库和 Maven Central 中拉取 Maven(这充分表明 Spring Cloud 很早便提供了 OpenTelemetry 支持)。尽管如此,我们仍持续推进,创建了一个通用遥测模块,以使用 Spring Cloud Sleuth 和 OpenTelemetry 配置分布式跟踪,并开发了多种遥测相关的辅助功能和扩展功能。
在通用遥测模块中,我们通过提供以下特性扩展了 Spring Cloud Sleuth 和 OpenTelemetry 库的跟踪功能:
- 支持 Spring 的自动配置类,为项目设置跟踪和扩展功能,并加载其他跟踪资源属性。
- NoOp 接口实现,这样我们可以将 NoOp 实例注入所有 Spring Cloud Sleuth 接口,以便在启动时禁用跟踪。
- 跟踪命名拦截器,用于规范跟踪名称。
- 通过 slf4j 和 Spring Cloud Sleuth 跟踪输出错误的错误处理程序。
- 跟踪属性的增强实现,它将附加信息编码到每个发出的跟踪中,包括 service 的版本、service 实例的 ID、机器 ID、pod 名称、容器 ID、容器名称和命名空间名称。
- 跟踪语句检查器,它将跟踪 ID 注入 Hibernate 发出的 SQL 语句之前的注释中。显然,这项工作现在可以使用 SQLCommenter 完成,但我们尚未进行迁移。
此外,依托于 Apache HTTP 客户端,我们还实现了兼容 Spring 的 HTTP 客户端,因为我们希望在 service 之间进行 HTTP 调用时获得更多的指标和更高的可定制性。在此实现中,当调用从属服务时,跟踪和跨度标识符将作为 HTTP 请求头传入,以包含在跟踪输出中。此外,这一实现提供了由 OpenTelemetry 聚合的 HTTP 连接池指标。
总而言之,使用 Spring Cloud Sleuth 和 OpenTelemetry 进行跟踪是一段艰难的历程,但我们相信一切都值得。希望本项目以及本文能够为做出这一选择的人们照亮前路。
NGINX 分布式跟踪
为了在请求的整个生命周期中获取连接所有 service 的跟踪信息,我们需要将 OpenTelemetry 集成到 NGINX 中。为此,我们使用了 OpenTelemetry NGINX 模块(仍处于beta测试阶段)。考虑到可能很难获得适用于所有 NGINX 版本的模块的工作二进制文件,因此我们创建了一个容器镜像的 GitHub 代码仓库,其中包含不受支持的 NGINX 模块。我们运行夜间构建,并通过易于从中导入的 Docker 镜像分发模块二进制文件。
我们尚未将此流程整合到 MARA 项目的 NGINX Ingress Controller 的构建流程中,但计划尽快实施。
实施指标收集
完成 OpenTelemetry 跟踪集成后,接下来我们将重点放在指标上。当时,我们基于 Python 的应用没有现成指标,于是我们决定暂时推迟添加相关指标。对于 Java 应用,原始的 Bank of Anthos 源代码(结合使用 Micrometer与 Google Cloud 的 Stackdriver)为指标提供了支持。但我们从 Bank of Sirius 中删除了该代码,原因是它不支持配置指标后端。尽管如此,指标钩子的存在说明需要进行适当的指标集成。
为了制定一个实用的可配置解决方案,我们首先研究了 OpenTelemetry Java 库和 Micrometer 中的指标支持。我们搜索了这两种技术之间的比较,许多搜索结果都列举了 OpenTelemetry 在 JVM 中用作指标 API 的缺陷,尽管在撰写本文时 OpenTelemetry 指标仍处于内测阶段。Micrometer 是一个成熟的 JVM 指标门面层,类似于 slf4j,提供了一个通用 API 包装器,对接可配置的指标实现,而非自身指标实现。有趣的是,它是 Spring 的默认指标 API。
这时,我们对以下实际情况进行了权衡:
- OpenTelemetry Collector 可以使用几乎任何来源的指标,包括 Prometheus、StatsD和原生OpenTelemetry Protocol (OTLP)
- Micrometer 支持大量的指标后端,包括 Prometheus 和 StatsD
- 面向 JVM 的 OpenTelemetry Metrics SDK 支持通过 OTLP 发送指标
经过几次试验后,我们确定,最实用的方法是搭配使用 Micrometer Façade 和 Prometheus 支持实现,并配置 OpenTelemetry Collector 从而可以使用 Prometheus API 从应用中提取指标。许多文章都曾介绍过,OpenTelemetry 中缺少指标类型可能会导致一些问题,但我们的用例无需这些指标类型,因此可以在这点上让步。
我们发现 OpenTelemetry Collector 有趣的一点是:即使我们已将其配置为通过 OTLP 接收跟踪数据和通过 Prometheus API 接收指标,它仍然可以配置为使用 OTLP 或任何其他支持协议将这两种类型的数据发送到外部数据接收器。这有助于我们轻松地使用 LightStep 来试用我们的应用。
总体而言,使用 Java 编写指标非常简单,因为我们按照 Micrometer API 标准进行编写,后者有大量示例和教程可供参考。对于指标和跟踪而言,最困难之处可能是恰好在面向 telemetry-common 指标的 pom.xml 文件中获取 Maven 依赖关系图。
实施 error 聚合
OpenTelemetry 项目本身的任务中不包含 error 聚合,而且它所提供的 error 标记实现也不像 Sentry 或 Honeybadger.io 等解决方案那样简洁。尽管如此,我们还是决定在短期内使用 OpenTelemetry 进行 error 聚合,而非添加其他工具。借助 Jaeger 之类的工具,我们可以搜索 error=true,以找到所有带有 error 条件的跟踪。这至少能够让我们了解常见问题之所在。未来,我们可能会考虑添加 Sentry 集成。
实施健康检查和运行时自检
在我们应用的上下文中,通过健康检查,Kubernetes 能够知道 service 是否健康或是否已完成启动阶段。如果 service 不健康,则可将 Kubernetes 配置为终止或重启实例。因为我们发现文档资料的欠缺,我们决定不在我们的应用中使用 OpenTelemetry 健康检查。
而在 JVM 服务中,我们使用了名为 Spring Boot Actuator 的 Spring 项目,它不仅提供健康检查端点,而且还提供运行时自检和堆转储端点。在 Python 服务中,我们使用 Flask Management Endpoints 模块,该模块提供了 Spring Boot Actuator 功能的子集。目前,它只提供可定制的应用信息和健康检查。
Spring Boot Actuator 可接入 JVM 和 Spring,以提供自检、监控和健康检查端点。此外,它还提供了一个框架,用于将自定义信息添加到其端点上呈现的默认数据中。端点可提供对缓存状态、运行时环境、数据库迁移、健康检查、可定制应用信息、指标、周期作业、HTTP 会话状态和线程转储等要素的运行时自检。
Spring Boot Actuator 实现的健康检查端点采用了模块化配置,因此 service 的健康状况将包括多个单独的检查(分成“存活”、“就绪”等类别)。此外,它还提供了显示所有检查模块的完整健康检查。
信息端点在 JSON 文档中被定义为单个高级 JSON 对象和一系列分级密钥和值。通常,文档会指定 service 名称和版本、架构、主机名、操作系统信息、进程 ID、可执行文件名称以及有关主机的详细信息,例如机器 ID 或唯一 service ID。
实施堆转储和核心转储
您可能还记得上述“对照功能清单比较工具功能”部分的表格,其中没有一个工具支持运行时自检或堆转储/核心转储。但作为我们的底层框架,Spring 提供了对二者的支持 —— 尽管将可观测性功能添加到应用中需要花一些功夫。如上一节所述,在运行时自检中,我们结合使用了 Python 模块与 Spring Boot Actuator。
同样,在堆转储中,我们使用了 Spring Boot Actuator 提供的线程转储端点来实现所需功能的子集。我们无法按需获得核心转储,也无法以理想的细粒度级别获得 JVM 的堆转储,但我们可以轻松地获得一些所需的功能。然而,Python 服务的核心转储将需要开展大量额外工作,这有待在后期进行。
总结
在反复尝试和比较之后,我们最终选择使用以下技术来实现 MARA 的可观测性(在下文中,OTEL 代表 OpenTelemetry):
- 日志记录 (适用于所有容器) – Filebeat → Elasticsearch / Kibana
分布式跟踪
- Java – Spring Cloud Sleuth → 用于 OTEL 的 Spring Cloud Sleuth 导出器 → OTEL Collector → 可插式导出器,如 Jaeger、Lightstep、Splunk 等。
- Python – OTEL Python 库 → OTEL Collector → 可插式存储
- NGINX 和 NGINX Plus (尚不支持 NGINX Ingress Controller) – NGINX OTEL 模块 → OTEL Collector → 可插式导出器
指标收集
- Java – Micrometer(通过 Spring) → Prometheus 导出器 → OTEL Collector
- Python – 尚未实现
- Python WSGI – GUnicorn StatsD → Prometheus (通过 StatsD/ServiceMonitor)
- NGINX – Prometheus 端点 → Prometheus (通过 ServiceMonitor)
- 错误聚合 – OTEL 分布式跟踪 → 可插式导出器 → 导出器的搜索功能,用于查找标明 error 的跟踪
健康检查
- Java – Spring Boot Actuator → Kubernetes
- Python – Flask Management Endpoints 模块 → Kubernetes
运行时自检
- Java – Spring Boot Actuator
- Python – Flask Management Endpoints 模块
堆/核心存储
- Java – Spring Boot Actuator 支持线程转储
- Python – 尚不支持
此实现只是当前进展情况,必将随着技术的发展而演进。不久之后,我们将通过广泛的负载测试来运行应用。我们希望全面了解可观测性方法的不足之处并添加更多可观测性功能。
欢迎亲自试用现代应用参考架构和示例应用 (Bank of Sirius)。如果您对帮助我们改进服务有任何想法,欢迎您在我们的 GitHub 仓库中建言献策!
更多资源
想要更及时全面地获取 NGINX 相关的技术干货、互动问答、系列课程、活动资源?
请前往 NGINX 开源社区: