系统可观测性(5)OpenTelemetry基础使用

发布于:2025-03-15 ⋅ 阅读:(18) ⋅ 点赞:(0)

系统可观测性(5)OpenTelemetry基础概念


Author: Once Day Date: 2025年3月12日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…

漫漫长路,有人对你微笑过嘛…

本文档翻译整理自《OpenTelemetry Docs》,仅用于学习和交流。

全系列文章可参考专栏: 十年代码训练_Once-Day的博客-CSDN博客

参考文章:


1 OpenTelemetry项目

OpenTelemetry是一个开源的、独立于供应商的遥测数据收集和管理框架,旨在帮助实现系统的可观测性。它为创建和管理追踪、指标和日志等遥测数据提供了一套标准化的工具和API

OpenTelemetry是由OpenTracing和OpenCensus这两个项目合并而成的,目标是成为可观测性领域的标准。

OpenTelemetry与供应商和工具无关,可以与多种开源和商业的可观测性后端兼容,如Jaeger、Prometheus等,避免了供应商锁定。OpenTelemetry关注遥测数据的生成、收集、管理和输出,让用户能够轻松地监测应用程序或系统,而无需考虑其语言、基础设施或运行时环境。遥测数据的存储和可视化则交由其他工具处理。

OpenTelemetry主要由以下几部分组成:

  • 一套规范,定义了所有组件的标准。
  • 一个标准协议,定义了遥测数据的格式。
  • 语义约定,定义了常见遥测数据类型的标准命名方案。
  • API,定义了如何生成遥测数据。
  • SDK,实现了规范、API和遥测数据的输出。
  • 工具生态系统,为常见库和框架提供了检测功能。
  • 自动检测组件,无需更改代码即可生成遥测数据。
  • OpenTelemetry Collector,一个接收、处理和输出遥测数据的代理。

OpenTelemetry的一大特点是高度可扩展。例如,可以为Collector添加自定义数据源的接收器,为SDK加载自定义检测库,为特定用例定制SDK或Collector发行版,为不支持OpenTelemetry协议(OTLP)的自定义后端创建新的输出器,以及为非标准上下文传播格式创建自定义传播器等。虽然大多数用户可能不需要扩展OpenTelemetry,但该项目在几乎每一层都提供了扩展的可能性。

OpenTelemetry是云原生计算基金会(CNCF)的一个项目,由OpenTracing和OpenCensus两个先前的项目合并而成。这两个项目都旨在解决代码检测和将遥测数据发送到可观测性后端方面缺乏标准的问题。由于任何一个项目都无法独立完全解决这个问题,它们合并成立了OpenTelemetry,结合了各自的优势,提供了一个统一的解决方案。

2 可观测性概念

可观测性允许你通过向系统提出问题来从外部了解系统,而无需知道其内部工作原理。此外,它还可以让你轻松地对新问题(即"未知的未知")进行故障排除和处理。它还可以帮助你回答"为什么会发生这种情况?"这个问题。

要对系统提出这些问题,应用程序必须经过适当的检测。也就是说,应用程序代码必须发出诸如跟踪、指标和日志等信号。当开发人员不需要添加更多检测来排查问题时,就说明应用程序已经得到了适当的检测,因为他们已经拥有了所需的所有信息。

OpenTelemetry是一种机制,通过它可以对应用程序代码进行检测,以帮助使系统可观测。

遥测(Telemetry)是指系统及其行为发出的数据。数据可以以跟踪、指标和日志的形式出现

可靠性回答了这个问题:“服务是否在做用户期望它做的事情?”,一个系统可能100%的时间都在运行,但是如果当用户点击"添加到购物车"将一双黑色鞋子添加到购物车时,系统并不总是添加黑色鞋子,那么该系统可能就是不可靠的。

指标是一段时间内关于基础设施或应用程序的数值数据的聚合。例如:系统错误率、CPU利用率和给定服务的请求率。

SLI(服务级别指标,Service Level Indicator)代表对服务行为的一种衡量。好的SLI从用户的角度衡量你的服务。SLI的一个示例可以是网页加载的速度。

SLO(服务级别目标,Service Level Objective)代表了向组织/其他团队传达可靠性的方式。这是通过将一个或多个SLI与业务价值相关联来实现的。

分布式跟踪可以让你观察请求在复杂的分布式系统中如何传播。分布式跟踪提高了应用程序或系统健康状况的可见性,并允许你调试难以在本地重现的行为。对于通常具有不确定性问题或太复杂而无法在本地重现的分布式系统来说,它是必不可少的。

要理解分布式跟踪,需要了解其每个组件的作用:日志、Span和跟踪。

(1)日志是由服务或其他组件发出的带有时间戳的消息。与跟踪不同,它们不一定与任何特定的用户请求或事务相关联。你几乎可以在软件的任何地方找到日志。在过去,开发人员和运维人员都非常依赖日志来帮助他们理解系统行为。

I, [2021-02-23T13:26:23.505892 #22473]  INFO -- : [6459ffe1-ea53-4044-aaa3-bf902868f730] Started GET "/" for ::1 at 2021-02-23 13:26:23 -0800

日志不足以用于跟踪代码执行,因为它们通常缺乏上下文信息,例如它们从何处被调用。

当日志作为Span的一部分或与跟踪和Span关联时,它们会变得更加有用。

(2)Span代表一个工作单元或操作。Span跟踪请求所执行的特定操作,描绘出在执行该操作期间发生的情况。

Span包含名称、时间相关数据、结构化日志消息和其他元数据(即属性),以提供有关其跟踪的操作的信息。

Span属性是附加到Span的元数据,以下表格包含Span属性的示例:

Key Value
http.request.method “GET”
network.protocol.version “1.1”
url.path “/webshop/articles/4”
url.query “?s=1”
server.address “example.com”
server.port 8080
url.scheme “https”
http.route “/webshop/articles/:article_id”
http.response.status_code 200
client.address “192.0.2.4”
client.socket.address “192.0.2.5” (客户端通过代理)
user_agent.original “Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0”

(3)分布式跟踪(distributed trace),通常称为跟踪,记录请求(由应用程序或最终用户发出)在多服务架构(如微服务和无服务器应用程序)中传播时所采取的路径

一个跟踪由一个或多个Span组成。第一个Span代表根Span。每个根Span代表从开始到结束的请求。父Span下的子Span提供了请求过程中发生的更深入的上下文(或构成请求的步骤)。

如果没有跟踪,在分布式系统中查找性能问题的根本原因可能会很有挑战性。通过将请求在分布式系统中的流动过程分解,跟踪使调试和理解分布式系统变得不那么令人生畏。

许多可观测性后端将跟踪可视化为如下所示的瀑布图:

在这里插入图片描述

瀑布图显示了根Span与其子Span之间的父子关系。当一个Span封装另一个Span时,这也表示嵌套关系。

3 上下文传播(Context propagation)

通过上下文传播,信号之间可以相互关联,无论它们在何处生成。虽然上下文传播并不局限于追踪,但它能让追踪在跨越进程和网络边界、任意分布的服务之间,构建关于系统的因果关系信息。

要理解上下文传播,需要分别理解两个概念:上下文和传播。

(1)上下文(Context):上下文是一个对象,它包含了发送和接收服务(或执行单元)将一个信号与另一个信号相关联所需的信息。

例如,如果服务 A 调用服务 B,那么服务 A 中 ID 在上下文中的一个跨度(span)将作为服务 B 中创建的下一个跨度的父跨度。上下文中的追踪 ID(trace ID)也将用于服务 B 中创建的下一个跨度,这意味着该跨度与服务 A 中的跨度属于同一条追踪链。

(2)传播(propagation):传播是在服务和进程之间传递上下文的机制。它对上下文对象进行序列化或反序列化,并提供从一个服务传播到另一个服务的相关信息。

传播通常由检测库(instrumentation libraries)处理,对用户来说是透明的。如果你需要手动传播上下文,可以使用传播器 API(Propagators API)。

OpenTelemetry 维护了几个官方传播器。默认的传播器使用 W3C 追踪上下文规范(W3C TraceContext specification)指定的头部信息。

4 追踪(Traces)

追踪能让我们全面了解向应用程序发出请求时发生的情况。无论你的应用程序是一个连接单一数据库的单体应用,还是一个复杂的服务网格,追踪对于理解请求在应用程序中所经过的完整 “路径” 都至关重要。

让我们通过三个工作单元(用跨度表示)来探究这一点:

  • “hello” 跨度
{
    "name": "hello",
    "context": {
        "trace_id": "5b8aa5a2d2c872e8321cf37308d69df2",
        "span_id": "051581bf3cb55c13"
    },
    "parent_id": null,
    "start_time": "2022-04-29T18:52:58.114201Z",
    "end_time": "2022-04-29T18:52:58.114687Z",
    "attributes": {
        "http.route": "some_route1"
    },
    "events": [
        {
            "name": "Guten Tag!",
            "timestamp": "2022-04-29T18:52:58.114561Z",
            "attributes": {
                "event_attributes": 1
            }
        }
    ]
}

这是根跨度,表示整个操作的开始和结束。注意,它有一个trace_id字段来指明追踪,但没有parent_id。通过这一点就能判断它是根跨度。

  • “hello - greetings” 跨度
{
    "name": "hello-greetings",
    "context": {
        "trace_id": "5b8aa5a2d2c872e8321cf37308d69df2",
        "span_id": "5fb397be34d26b51"
    },
    "parent_id": "051581bf3cb55c13",
    "start_time": "2022-04-29T18:52:58.114304Z",
    "end_time": "2022-04-29T22:52:58.114561Z",
    "attributes": {
        "http.route": "some_route2"
    },
    "events": [
        {
            "name": "hey there!",
            "timestamp": "2022-04-29T18:52:58.114561Z",
            "attributes": {
                "event_attributes": 1
            }
        },
        {
            "name": "bye now!",
            "timestamp": "2022-04-29T18:52:58.114585Z",
            "attributes": {
                "event_attributes": 1
            }
        }
    ]
}

这个跨度封装了特定任务,比如打招呼,它的父跨度是 “hello” 跨度。注意,它与根跨度共享相同的trace_id,表明它是同一条追踪的一部分。此外,它的parent_id与 “hello” 跨度的span_id相匹配。

  • “hello - salutations” 跨度
{
    "name": "hello-salutations",
    "context": {
        "trace_id": "5b8aa5a2d2c872e8321cf37308d69df2",
        "span_id": "93564f51e1abe1c2"
    },
    "parent_id": "051581bf3cb55c13",
    "start_time": "2022-04-29T18:52:58.114492Z",
    "end_time": "2022-04-29T18:52:58.114631Z",
    "attributes": {
        "http.route": "some_route3"
    },
    "events": [
        {
            "name": "hey there!",
            "timestamp": "2022-04-29T18:52:58.114561Z",
            "attributes": {
                "event_attributes": 1
            }
        }
    ]
}

这个跨度表示此次追踪中的第三个操作,和前一个跨度一样,它是 “hello” 跨度的子跨度。这也使得它和 “hello - greetings” 跨度互为兄弟跨度。

这三个 JSON 块都共享相同的trace_idparent_id字段代表了层级关系。这就构成了一条追踪!

每个跨度看起来都像结构化日志。某种程度上确实如此!一种理解追踪的方式是,追踪是一系列包含上下文、关联关系、层级结构等信息的结构化日志集合。不过,这些 “结构化日志” 可能来自不同的进程、服务、虚拟机、数据中心等等。这就是追踪能够呈现任何系统端到端视图的原因。

为了理解 OpenTelemetry 中的追踪机制,让我们来看一下在为代码添加检测时涉及的组件列表。

追踪器提供者(Tracer Provider):追踪器提供者(有时称为TracerProvider)是Tracer的工厂。在大多数应用程序中,追踪器提供者只会初始化一次,其生命周期与应用程序的生命周期一致。追踪器提供者的初始化还包括资源(Resource)和导出器(Exporter)的初始化。这通常是使用 OpenTelemetry 进行追踪的第一步。在一些语言的 SDK 中,已经为你初始化好了全局追踪器提供者。

追踪器(Tracer):追踪器创建跨度,这些跨度包含有关特定操作(比如服务中的一个请求)的更多信息。追踪器由追踪器提供者创建。

追踪导出器(Trace Exporters):追踪导出器将追踪数据发送给使用者。这个使用者可以是用于调试和开发阶段的标准输出、OpenTelemetry 收集器,或者你选择的任何开源或厂商后端。

上下文传播(Context Propagation):上下文传播是实现分布式追踪的核心概念。通过上下文传播,跨度之间可以相互关联,并组装成一条追踪,无论跨度在何处生成。有关此主题的更多信息,请参阅上下文传播概念页面。

跨度(Spans):跨度代表一个工作单元或操作,是追踪的基本构建块。在 OpenTelemetry 中,跨度包含以下信息:

  • 名称
  • 父跨度 ID(根跨度为空)
  • 开始和结束时间戳
  • 跨度上下文
  • 属性
  • 跨度事件
  • 跨度链接
  • 跨度状态
{
    "name": "/v1/sys/health",
    "context": {
        "trace_id": "7bba9f33312b3dbb8b2c2c62bb7abe2d",
        "span_id": "086e83747d0e381e"
    },
    "parent_id": "",
    "start_time": "2021-10-22 16:04:01.209458162 +0000 UTC",
    "end_time": "2021-10-22 16:04:01.209514132 +0000 UTC",
    "status_code": "STATUS_CODE_OK",
    "status_message": "",
    "attributes": {
        "net.transport": "IP.TCP",
        "net.peer.ip": "172.17.0.1",
        "net.peer.port": "51820",
        "net.host.ip": "10.177.2.152",
        "net.host.port": "26040",
        "http.method": "GET",
        "http.target": "/v1/sys/health",
        "http.server_name": "mortar-gateway",
        "http.route": "/v1/sys/health",
        "http.user_agent": "Consul Health Check",
        "http.scheme": "http",
        "http.host": "10.177.2.152:26040",
        "http.flavor": "1.1"
    },
    "events": [
        {
            "name": "",
            "message": "OK",
            "timestamp": "2021-10-22 16:04:01.209512872 +0000 UTC"
        }
    ]
}

跨度可以嵌套,这由父跨度 ID 的存在暗示:子跨度代表子操作。这使得跨度能够更准确地记录应用程序中完成的工作。

跨度上下文(Span Context):跨度上下文是每个跨度上的不可变对象,包含以下内容:

  • 代表该跨度所属追踪的追踪 ID。
  • 该跨度的跨度 ID。
  • 追踪标志(Trace Flags),一种二进制编码,包含有关追踪的信息。
  • 追踪状态(Trace State),一个键值对列表,可以携带特定于供应商的追踪信息。

跨度上下文是跨度的一部分,会与分布式上下文(Distributed Context)和行李(Baggage)一起进行序列化和传播。

由于跨度上下文包含追踪 ID,因此在创建跨度链接时会用到它。

属性(Attributes):属性是键值对,包含元数据,可用于注释跨度,以携带有关其正在跟踪的操作的信息。

例如,如果一个跨度跟踪电子商务系统中向用户购物车添加商品的操作,可以捕获用户 ID、要添加到购物车的商品 ID 以及购物车 ID。

可以在跨度创建期间或之后添加属性。建议在跨度创建时添加属性,以便 SDK 采样能够获取这些属性。如果必须在跨度创建后添加值,可以使用该值更新跨度。

属性具有以下规则,每种语言的 SDK 都会实现:

  • 键必须是非空字符串值。
  • 值必须是非空字符串、布尔值、浮点值、整数值,或者是这些值的数组。

此外,还有语义属性(Semantic Attributes),这是常见操作中通常存在的元数据的命名约定。尽可能使用语义属性命名有助于在不同系统中对常见类型的元数据进行标准化。

跨度事件(Span Events):跨度事件可以看作是跨度上的结构化日志消息(或注释),通常用于表示跨度持续时间内某个有意义的时间点。

例如,考虑网页浏览器中的两种场景:

  • 跟踪页面加载。
  • 表示页面何时变为可交互状态。

第一种场景最好使用跨度,因为它是一个有开始和结束的操作。

第二种场景最好使用跨度事件,因为它代表一个有意义的时间点。

何时使用跨度事件与跨度属性:由于跨度事件也包含属性,因此何时使用事件而不是属性并不总是一目了然。在做决定时,可以考虑特定时间戳是否有意义。

例如,当使用跨度跟踪一个操作并且该操作完成时,可能希望将操作数据添加到遥测数据中。

如果操作完成的时间戳有意义或相关,则将数据附加到跨度事件中。

如果时间戳没有意义,则将数据作为跨度属性附加。

跨度链接(Span Links):跨度链接用于将一个跨度与一个或多个跨度关联起来,暗示它们之间的因果关系。例如,假设有一个分布式系统,其中一些操作由一条追踪进行跟踪。

作为对其中某些操作的响应,会将一个额外操作排入队列等待执行,但它的执行是异步的。我们也可以用一条追踪来跟踪后续操作。

我们希望将后续操作的追踪与第一个追踪关联起来,但我们无法预测后续操作何时开始。我们需要关联这两条追踪,这时就可以使用跨度链接。

你可以将第一条追踪的最后一个跨度链接到第二条追踪的第一个跨度。这样,它们就具有了因果关联。

跨度链接是可选的,但它是将追踪跨度相互关联的有效方式。

跨度状态(Span Status):每个跨度都有一个状态。可能的值有三种:

  • Unset
  • Error
  • Ok

默认值是Unset。状态为Unset的跨度表示它所跟踪的操作已成功完成且没有错误。

当跨度状态为Error时,意味着它所跟踪的操作发生了错误。例如,这可能是由于服务器处理请求时出现 HTTP 500 错误。

当跨度状态为Ok时,意味着应用程序开发人员明确将该跨度标记为无错误。虽然这可能不太直观,但当已知跨度已无错误完成时,并不需要将跨度状态设置为Ok,因为Unset已经涵盖了这种情况。Ok的作用是表示用户明确设置的、关于跨度状态的 “最终确定”。在开发人员希望对跨度状态只有 “成功” 这一种解释的任何情况下,Ok都很有用。

再次强调:Unset表示跨度无错误完成。Ok表示开发人员明确将跨度标记为成功。在大多数情况下,无需明确将跨度标记为Ok

跨度类型(Span Kind):创建跨度时,它的类型可以是ClientServerInternalProducerConsumer中的一种。跨度类型为追踪后端提供了关于如何组装追踪的提示。根据 OpenTelemetry 规范,服务器跨度的父跨度通常是远程客户端跨度,客户端跨度的子跨度通常是服务器跨度。类似地,消费者跨度的父跨度始终是生产者跨度,生产者跨度的子跨度始终是消费者跨度。如果未指定跨度类型,则默认认为是内部跨度。

  • 客户端(Client):客户端跨度表示同步的传出远程调用,例如传出的 HTTP 请求或数据库调用。请注意,在此上下文中,“同步” 并非指async/await,而是指该调用不会被排入队列等待后续处理。
  • 服务器(Server):服务器跨度表示同步的传入远程调用,例如传入的 HTTP 请求或远程过程调用。
  • 内部(Internal):内部跨度表示不跨越进程边界的操作。比如对函数调用或 Express 中间件进行检测时,可能会使用内部跨度。
  • 生产者(Producer):生产者跨度表示创建一个可能在以后异步处理的任务。它可能是一个远程任务,比如插入作业队列中的任务,也可能是由事件监听器处理的本地任务。
  • 消费者(Consumer):消费者跨度表示处理由生产者创建的任务,可能在生产者跨度结束很长时间后才开始。
5. 指标(Metrics)

指标是在运行时对服务进行的测量。捕获测量值的时刻称为指标事件,它不仅包含测量值本身,还包括测量的时间以及相关元数据。

应用程序和请求指标是可用性和性能的重要指标。自定义指标可以深入了解可用性指标如何影响用户体验或业务。收集到的数据可用于在出现故障时发出警报,或在需求高峰期触发调度决策,自动扩展部署规模。

为了了解 OpenTelemetry 中指标的工作原理,让我们来看一下在为代码添加检测时涉及的组件列表。

度量器提供者(Meter Provider):度量器提供者(有时称为 MeterProvider)是度量器(Meter)的工厂。在大多数应用程序中,度量器提供者只会初始化一次,其生命周期与应用程序的生命周期一致。度量器提供者的初始化还包括资源(Resource)和导出器(Exporter)的初始化。这通常是使用 OpenTelemetry 进行度量的第一步。在一些语言的 SDK 中,已经为你初始化好了全局度量器提供者。

度量器(Meter):度量器创建指标工具,用于在运行时捕获有关服务的测量值。度量器由度量器提供者创建。

指标导出器(Metric Exporter):指标导出器将指标数据发送给使用者。这个使用者可以是开发期间用于调试的标准输出、OpenTelemetry 收集器,或者你选择的任何开源或厂商后端。

指标工具(Metric Instruments):在 OpenTelemetry 中,测量值由指标工具捕获。一个指标工具由以下部分定义:

  • 名称
  • 类型
  • 单位(可选)
  • 描述(可选)

名称、单位和描述由开发人员选择,或者对于请求和进程指标等常见指标,通过语义约定来定义。

指标工具类型有以下几种:

  • 计数器(Counter):一种随时间累积的值 —— 你可以把它想象成汽车的里程表,它只会增加。
  • 异步计数器(Asynchronous Counter):与计数器类似,但每次导出时收集一次。如果你无法获取连续的增量,只能获取聚合值,就可以使用它。
  • 上下计数器(UpDownCounter):一种随时间累积的值,但也可以减少。例如队列长度,它会随着队列中工作项数量的变化而增加或减少。
  • 异步上下计数器(Asynchronous UpDownCounter):与上下计数器类似,但每次导出时收集一次。如果你无法获取连续的变化,只能获取聚合值(例如当前队列大小),可以使用它。
  • 仪表盘(Gauge):在读取时测量当前值。例如汽车的燃油表。仪表盘是同步的。
  • 异步仪表盘(Asynchronous Gauge):与仪表盘类似,但每次导出时收集一次。如果你无法获取连续的变化,只能获取聚合值,可以使用它。
  • 直方图(Histogram):一种客户端对值的聚合,例如请求延迟。如果你对值的统计信息感兴趣,直方图是个不错的选择。例如:有多少请求的处理时间少于 1 秒?

聚合(Aggregation):除了指标工具,聚合的概念也很重要,需要理解。聚合是一种技术,它将大量测量值合并为关于在某个时间窗口内发生的指标事件的精确或估计统计信息。OTLP 协议传输此类聚合后的指标。OpenTelemetry API 为每个指标工具提供了默认聚合,可使用视图(Views)进行覆盖。OpenTelemetry 项目旨在提供可视化工具和遥测后端支持的默认聚合。

与请求追踪不同,请求追踪旨在捕获请求的生命周期并为请求的各个部分提供上下文,而指标旨在提供汇总的统计信息。指标的一些用例示例包括:

  • 按协议类型报告服务读取的总字节数。
  • 报告读取的总字节数和每个请求的字节数。
  • 报告系统调用的持续时间。
  • 报告请求大小以确定趋势。
  • 报告进程的 CPU 或内存使用情况。
  • 报告账户的平均余额值。
  • 报告当前正在处理的活动请求。

视图(Views):视图为 SDK 用户提供了自定义 SDK 输出指标的灵活性。你可以自定义要处理或忽略哪些指标工具。你还可以自定义聚合方式以及希望在指标中报告哪些属性。

6. 日志(Logs)

日志是带有时间戳的文本记录,可以是结构化的(推荐),也可以是非结构化的,并带有可选的元数据。在所有的遥测信号中,日志的历史最为悠久。大多数编程语言都有内置的日志记录功能,或者有知名且广泛使用的日志记录库。

OpenTelemetry 并未定义专门的 API 或 SDK 来创建日志。相反,OpenTelemetry 日志是已经从某个日志记录框架或基础设施组件中获得的现有日志。OpenTelemetry 的软件开发工具包(SDK)和自动检测功能利用多个组件,将日志与追踪自动关联起来。

OpenTelemetry 对日志的支持旨在与现有的日志完全兼容,它提供了为这些日志添加额外上下文的功能,以及一个通用工具包,用于将来自许多不同来源的日志解析并处理成一种通用格式。

OpenTelemetry 收集器提供了多个处理日志的工具:

  • 多个接收器,用于解析来自特定、已知日志数据源的日志。
  • 文件日志接收器(filelogreceiver),它可以从任何文件中读取日志,并提供从不同格式解析日志的功能,或者使用正则表达式进行解析。
  • 像转换处理器(transformprocessor)这样的处理器,它允许解析嵌套数据、展平嵌套结构、添加 / 删除 / 更新值等等。
  • 导出器,它允许以非 OpenTelemetry 格式发出日志数据。

采用 OpenTelemetry 的第一步通常是部署一个收集器,将其作为通用的日志记录代理。

在应用程序中,OpenTelemetry 日志是使用任何日志记录库或内置的日志记录功能创建的。当添加自动检测功能或激活 SDK 时,OpenTelemetry会自动将现有的日志与任何活动的追踪和跨度关联起来,并将日志主体附上它们的ID。换句话说,OpenTelemetry 会自动关联日志和追踪。

从技术上讲,OpenTelemetry 并不区分结构化日志和非结构化日志。可以将现有的任何日志用于 OpenTelemetry。然而,并非所有的日志格式都同样有用!特别推荐在生产环境的可观测性中使用结构化日志,因为它们易于大规模解析和分析。以下部分解释了结构化、非结构化和半结构化日志之间的区别。

结构化日志是一种文本格式遵循一致的、机器可读格式的日志。对于应用程序来说,最常见的格式之一是 JSON:

{
  "timestamp": "2024-08-04T12:34:56.789Z",
  "level": "INFO",
  "service": "user-authentication",
  "environment": "production",
  "message": "User login successful",
  "context": {
    "userId": "12345",
    "username": "johndoe",
    "ipAddress": "192.168.1.1",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
  },
  "transactionId": "abcd-efgh-ijkl-mnop",
  "duration": 200,
  "request": {
    "method": "POST",
    "url": "/api/v1/login",
    "headers": {
      "Content-Type": "application/json",
      "Accept": "application/json"
    },
    "body": {
      "username": "johndoe",
      "password": "******"
    }
  },
  "response": {
    "statusCode": 200,
    "body": {
      "success": true,
      "token": "jwt-token-here"
    }
  }
}

对于基础设施组件,通常使用通用日志格式(CLF):

127.0.0.1 - johndoe [04/Aug/2024:12:34:56 -0400] "POST /api/v1/login HTTP/1.1" 200 1234

将不同的结构化日志格式混合在一起的情况也很常见。例如,扩展日志格式(ELF)日志可以将 JSON 与 CLF 日志中以空格分隔的数据混合在一起。

192.168.1.1 - johndoe [04/Aug/2024:12:34:56 -0400] "POST /api/v1/login HTTP/1.1" 200 1234 "http://example.com" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36" {"transactionId": "abcd-efgh-ijkl-mnop", "responseTime": 150, "requestBody": {"username": "johndoe"}, "responseHeaders": {"Content-Type": "application/json"}}

为了充分利用这种日志,需要将 JSON 部分和与 ELF 相关的部分都解析成一种共享格式,以便在可观测性后端更轻松地进行分析。OpenTelemetry 收集器中的文件日志接收器包含了解析此类日志的标准化方法。

结构化日志是使用日志的首选方式。由于结构化日志以一致的格式发出,它们易于解析,这使得它们在 OpenTelemetry 收集器中更容易进行预处理,与其他数据关联,并最终在可观测性后端进行分析

非结构化日志是不遵循一致结构的日志。它们可能更便于人类阅读,并且通常在开发过程中使用。然而,在生产环境的可观测性中不建议使用非结构化日志,因为大规模解析和分析它们要困难得多。

[ERROR] 2024-08-04 12:45:23 - Failed to connect to database. Exception: java.sql.SQLException: Timeout expired. Attempted reconnect 3 times. Server: db.example.com, Port: 5432
System reboot initiated at 2024-08-04 03:00:00 by user: admin. Reason: Scheduled maintenance. Services stopped: web-server, database, cache. Estimated downtime: 15 minutes.
DEBUG - 2024-08-04 09:30:15 - User johndoe performed action: file_upload. Filename: report_Q3_2024.pdf, Size: 2.3 MB, Duration: 5.2 seconds. Result: Success

在生产环境中存储和分析非结构化日志是可行的,不过你可能需要做大量工作来解析它们或以其他方式进行预处理,使其能够被机器读取。例如,上述三条日志需要使用正则表达式来解析它们的时间戳,并需要自定义解析器来一致地提取日志消息的主体。对于日志记录后端来说,通常需要这样做才能知道如何按时间戳对日志进行排序和组织。虽然为了分析目的解析非结构化日志是可能的,但这可能比切换到结构化日志记录(例如通过应用程序中的标准日志记录框架)需要做更多的工作。

半结构化日志是一种使用一些自洽模式来区分数据,从而使其能够被机器读取的日志,但在不同系统之间,数据之间可能不会使用相同的格式和分隔符。

2024-08-04T12:45:23Z level=ERROR service=user-authentication userId=12345 action=login message="Failed login attempt" error="Invalid password" ipAddress=192.168.1.1 userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"

虽然半结构化日志能够被机器读取,但可能需要几种不同的解析器才能进行大规模分析。

以下概念和组件列表为 OpenTelemetry 的日志记录支持提供了动力。

Log Appender / Bridge 日志附加器/桥接器:作为应用程序开发人员,不应该直接调用日志桥接器 API,因为它是提供给日志记录库的作者用于构建日志附加器 / 桥接器的。相反,只需使用喜欢的日志记录库,并将其配置为使用一个能够将日志发送到 OpenTelemetry 日志记录导出器(LogRecordExporter)的日志附加器(或日志桥接器)。

Logger Provider 记录器提供程序:(有时称为 LoggerProvider)是记录器(Logger)的工厂。在大多数情况下,记录器提供者只会初始化一次,其生命周期与应用程序的生命周期一致。记录器提供者的初始化还包括资源和导出器的初始化。这是日志桥接器 API 的一部分,并且只有当你是日志记录库的作者时才应该使用。

Logger 记录器:记录器创建日志记录。记录器由记录器提供者创建。这是日志桥接器 API 的一部分,并且只有当你是日志记录库的作者时才应该使用。

Log Record Exporter 日志记录导出器:日志记录导出器将日志记录发送给使用者。这个使用者可以是用于调试和开发阶段的标准输出、OpenTelemetry 收集器,或者你选择的任何开源或厂商后端。

Log Record 日志记录:日志记录表示一个事件的记录。在 OpenTelemetry 中,一个日志记录包含两种类型的字段:

  • 具有特定类型和含义的命名顶级字段
  • 任意值和类型的资源和属性字段

顶级字段如下:

字段名称 描述
时间戳(Timestamp) 事件发生的时间。
观测时间戳(ObservedTimestamp) 观测到事件的时间。
追踪 ID(TraceId) 请求的追踪 ID。
跨度 ID(SpanId) 请求的跨度 ID。
追踪标志(TraceFlags) W3C 追踪标志。
严重程度文本(SeverityText) 严重程度文本(也称为日志级别)。
严重程度数值(SeverityNumber) 严重程度的数值。
主体(Body) 日志记录的主体内容。
资源(Resource) 描述日志的来源。
检测范围(InstrumentationScope) 描述发出日志的范围。
属性(Attributes) 关于事件的其他信息。
7. 关联数据(Baggage)

在 OpenTelemetry 中,关联数据是与上下文并存的上下文信息。关联数据是一个键值对存储,这意味着它允许你在传递上下文的同时,传播任何你希望传递的数据。关联数据使得你能够在不同服务和进程间传递数据,让这些数据在其他服务中添加到追踪、指标或日志中时可用。

例如,假设在请求开始时你有一个clientId,你希望这个 ID 在一条追踪中的所有跨度、另一个服务中的某些指标以及请求过程中的某些日志中都可用。由于追踪可能跨多个服务,你需要某种方法来传播这些数据,而无需在代码库中的多个地方复制clientId

通过使用上下文传播在这些服务间传递关联数据,clientId就可以添加到任何额外的跨度、指标或日志中。此外,检测工具会自动为你传播关联数据。

在这里插入图片描述

关联数据最适合用于将通常仅在请求开始时可用的信息传递到下游。例如,这可以包括账户标识、用户 ID、产品 ID 和源 IP 地址等。

使用关联数据传播这些信息,有助于在后端对遥测数据进行更深入的分析。例如,如果你在追踪数据库调用的跨度中包含用户 ID 等信息,就可以更轻松地回答诸如 “哪些用户遇到了最慢的数据库调用?” 这类问题。你还可以记录有关下游操作的信息,并在日志数据中包含相同的用户 ID。

在这里插入图片描述

敏感的关联数据项可能会被共享给非预期的资源,比如第三方 API。这是因为自动检测会在大多数服务的网络请求中包含关联数据。具体来说,关联数据和追踪上下文的其他部分会在 HTTP 头中发送,这使得任何检查网络流量的人都能看到这些数据。如果网络流量在你的网络内部受到限制,这种风险可能不适用,但要记住,下游服务可能会将关联数据传播到你的网络之外。

此外,没有内置的完整性检查机制来确保关联数据项属于你,因此在读取关联数据时要格外小心。

关于关联数据,需要注意的重要一点是,它是一个独立的键值对存储,在未明确添加的情况下,与跨度、指标或日志的属性没有关联。

要将关联数据项添加为属性,你需要显式地从关联数据中读取数据,并将其作为属性添加到跨度、指标或日志中。

由于关联数据的一个常见用例是在整个追踪过程中向跨度属性添加数据,因此几种编程语言都有 “关联数据跨度处理器”,可以在创建跨度时将关联数据中的数据作为属性添加。

8. 检测方式

一个系统要具备可观测性,就必须进行检测:也就是说,系统组件的代码必须发出诸如追踪、指标和日志等信号。

使用 OpenTelemetry,主要可以通过两种方式对代码进行检测:

  • 基于代码的解决方案:借助针对大多数语言的官方 API 和 SDK 来实现
  • 无代码解决方案。

基于代码的解决方案能让你从应用程序本身获取更深入的洞察和丰富的遥测数据。它们允许你使用 OpenTelemetry API 从应用程序生成遥测数据,这是对无代码解决方案所生成遥测数据的重要补充。

无代码解决方案非常适合入门阶段,或者当你无法修改需要获取遥测数据的应用程序时使用。它们能从你使用的库和 / 或应用程序运行的环境中提供丰富的遥测数据。换个角度看,它们提供了关于应用程序边缘发生情况的信息。

你可以同时使用这两种解决方案。

OpenTelemetry 提供的不仅仅是无代码和基于代码的遥测解决方案。以下这些也是 OpenTelemetry 的一部分:

  • 库可以将 OpenTelemetry API 作为依赖项,这对使用该库的应用程序不会产生影响,除非导入了 OpenTelemetry SDK。
  • 对于每种信号,你都有多种方法来创建、处理和导出它们。
  • 由于实现中内置了上下文传播功能,无论信号在何处生成,你都可以将它们关联起来。
  • 资源(Resources)和检测范围(Instrumentation Scopes)允许按照不同实体(如主机、操作系统或 K8s 集群)对信号进行分组。
  • 每个特定语言的 API 和 SDK 实现都遵循 OpenTelemetry 规范的要求和预期。
  • 语义约定(Semantic Conventions)提供了一种通用的命名模式,可用于在不同代码库和平台之间实现标准化。
  • 无代码:了解如何在无需编写代码的情况下为应用程序添加可观测性。
  • 基于代码:了解设置基于代码的检测的基本步骤。
  • 库:了解如何为你的库添加原生检测功能。
9. 组件

目前,OpenTelemetry 由几个主要组件构成:

  • 规范(Specification)
  • 收集器(Collector)
  • 特定语言的 API 和 SDK 实现(Language-specific API & SDK implementations)
  • 检测库(Instrumentation Libraries)
  • 导出器(Exporters)
  • 无代码检测(Zero-Code Instrumentation)
  • 资源探测器(Resource Detectors)
  • 跨服务传播器(Cross Service Propagators)
  • 采样器(Samplers)
  • Kubernetes 操作符(Kubernetes operator)
  • 函数即服务资源(Function as a Service assets)

OpenTelemetry 使你无需使用特定供应商的 SDK 和工具来生成和导出遥测数据。

Specification 规范:描述了所有实现的跨语言要求和预期。除了术语定义之外,该规范还定义了以下内容:

  • API:定义用于生成和关联追踪、指标以及日志数据的数据类型和操作。

  • SDK:定义特定语言实现 API 的要求。配置、数据处理和导出的相关概念也在此定义。

  • 数据:定义 OpenTelemetry 协议(OTLP)以及遥测后端可以支持的与供应商无关的语义约定。

  • 收集器:OpenTelemetry 收集器是一个与供应商无关的代理,它可以接收、处理和导出遥测数据。它支持以多种格式(例如 OTLP、Jaeger、Prometheus 以及许多商业 / 专有工具格式)接收遥测数据,并将数据发送到一个或多个后端。它还支持在数据导出之前对遥测数据进行处理和过滤。

  • 特定语言的 API 和 SDK 实现:OpenTelemetry 还有特定语言的 SDK,使你能够使用 OpenTelemetry API,以你选择的语言生成遥测数据,并将这些数据导出到首选的后端。这些 SDK 还允许你纳入针对常用库和框架的检测库,以便与应用程序中的手动检测相连接。

  • 检测库:OpenTelemetry 支持大量组件,这些组件能从受支持语言的流行库和框架中生成相关的遥测数据。例如,HTTP 库的入站和出站 HTTP 请求会生成关于这些请求的数据。

OpenTelemetry 的一个理想目标是,所有流行库默认都具备可观测性,这样就无需额外的依赖项。

导出器:将遥测数据发送到 OpenTelemetry 收集器,以确保数据正确导出。在生产环境中使用收集器是一种最佳实践。若要可视化遥测数据,可以将其导出到诸如 Jaeger、Zipkin、Prometheus 或特定供应商的后端。

在导出器中,OpenTelemetry 协议(OTLP)导出器在设计时考虑了 OpenTelemetry 数据模型,能够在不丢失任何信息的情况下发出 OTel 数据。此外,许多处理遥测数据的工具都支持 OTLP(如 Prometheus、Jaeger 以及大多数供应商工具),在你有需要时提供了高度的灵活性。

无代码检测:在适用的情况下,OpenTelemetry 的特定语言实现提供了一种无需触及源代码即可检测应用程序的方法。虽然底层机制因语言而异,但无代码检测会为你的应用程序添加 OpenTelemetry API 和 SDK 功能。此外,它可能会添加一组检测库和导出器依赖项。

资源探测器:资源通过资源属性表示生成遥测数据的实体。例如,在 Kubernetes 容器中运行并生成遥测数据的进程,会有 Pod 名称、命名空间,可能还有部署名称。你可以将所有这些属性包含在资源中。

OpenTelemetry 的特定语言实现可以从OTEL_RESOURCE_ATTRIBUTES环境变量中检测资源,并且能够检测许多常见实体的资源,如进程运行时、服务、主机或操作系统。

跨服务传播器:传播是在服务和进程之间传输数据的机制。虽然传播并不局限于追踪,但它能让追踪在跨越进程和网络边界、任意分布的服务之间构建关于系统的因果关系信息。

在绝大多数用例中,上下文传播是通过检测库实现的。如果需要,你也可以自行使用传播器对诸如跨度上下文和关联数据(baggage)等横切关注点进行序列化和反序列化。

采样器:采样是一个限制系统生成追踪数量的过程。OpenTelemetry 的每个特定语言实现都提供了多种头部采样器。

Kubernetes 操作符:OpenTelemetry 操作符是 Kubernetes 操作符的一种实现。该操作符管理 OpenTelemetry 收集器,并使用 OpenTelemetry 对工作负载进行自动检测。

函数即服务资源:OpenTelemetry 支持多种方法来监控不同云供应商提供的函数即服务(Function-as-a-Service)。OpenTelemetry 社区目前提供预构建的 Lambda 层,能够自动检测你的应用程序,同时也提供独立的收集器 Lambda 层选项,可在手动或自动检测应用程序时使用。

10. 采样

借助追踪技术,你可以观察请求在分布式系统中从一个服务转移到另一个服务的过程。追踪对于系统的高级别和深入分析都非常实用。

然而,如果绝大多数请求都成功完成,且延迟在可接受范围内且无错误,那么你并不需要 100% 的追踪数据来有效观察应用程序和系统。你只需要进行合理的采样。

在这里插入图片描述

在讨论采样时,使用一致的术语非常重要。一条追踪或一个跨度被视为 “已采样” 或 “未采样”:

  • 已采样:一条追踪或一个跨度会被处理和导出。因为它被采样器选中,作为总体的代表,所以被视为 “已采样”。
  • 未采样:一条追踪或一个跨度不会被处理或导出。因为它未被采样器选中,所以被视为 “未采样”。

有时,这些术语的定义会被混淆。你可能会听到有人说他们 “对数据进行采样排除”,或者将未处理或未导出的数据视为 “已采样”。这些说法都是错误的。

采样是在不损失可见性的前提下降低可观测性成本的最有效方法之一。虽然还有其他降低成本的方法,如过滤或聚合数据,但这些方法不符合代表性的概念,而代表性在对应用程序或系统行为进行深入分析时至关重要。

代表性是指一个较小的群体能够准确代表一个较大群体的原则。此外,代表性可以通过数学方法验证,这意味着你可以高度确信较小的数据样本能够准确代表较大的群体。

另外,生成的数据越多,获得具有代表性的样本所需的数据就越少。对于高流量系统而言,1% 或更低的采样率就能非常准确地代表其他 99% 的数据,这种情况很常见。

如果满足以下任何一个标准,可考虑进行采样:

  • 每秒生成 1000 条或更多的追踪数据。
  • 大部分追踪数据代表正常流量,数据变化不大。
  • 存在一些常见标准,如错误或高延迟,这些通常意味着出现了问题。
  • 除了错误和延迟之外,还有特定领域的标准可用于确定相关数据。
  • 可以描述一些常见规则,用于确定数据是应被采样还是丢弃。
  • 有办法区分不同的服务,以便对高流量和低流量服务进行不同的采样。
  • 能够将未采样的数据(以防万一)路由到低成本存储系统。

最后,考虑一下整体预算。如果可观测性预算有限,但有时间进行有效的采样,那么通常来说采样是值得的。

采样可能并不适合你。如果满足以下任何一个标准,你可能需要避免采样:

  • 生成的数据量极少(每秒几十条或更少的小追踪数据)。
  • 仅以聚合方式使用可观测性数据,因此可以预先聚合数据。
  • 受到某些情况的限制,如法规禁止丢弃数据(并且无法将未采样的数据路由到低成本存储)。

最后,考虑与采样相关的以下三种成本:

  • 有效采样数据的直接计算成本,如尾部采样代理的成本。
  • 随着涉及的应用程序、系统和数据增多,维护有效采样方法的间接工程成本。
  • 由于采样技术不当而遗漏关键信息的间接机会成本。

采样虽然能有效降低可观测性成本,但如果实施不当,可能会带来其他意想不到的成本。根据你的可观测性后端、数据的性质以及有效采样的尝试,为可观测性分配更多资源(无论是使用供应商服务还是自行托管计算资源)可能会更划算。

头部采样是一种尽早做出采样决策的采样技术。决定是否采样或丢弃一个跨度或一条追踪,不是通过检查整条追踪来做出的

例如,最常见的头部采样形式是一致概率采样,也称为确定性采样。在这种情况下,根据追踪 ID 和期望的采样追踪百分比来做出采样决策。这确保了整条追踪都被采样(不会遗漏跨度),并且采样率一致,比如所有追踪的 5%。

头部采样的优点包括:

  • 易于理解
  • 易于配置
  • 高效
  • 可以在追踪收集管道的任何阶段进行

头部采样的主要缺点是无法根据整条追踪中的数据做出采样决策。例如,仅靠头部采样无法确保所有包含错误的追踪都被采样。在这种情况以及许多其他情况下,你需要使用尾部采样。

尾部采样是指在考虑一条追踪中的所有或大部分跨度之后,再决定是否对该追踪进行采样。尾部采样使你能够根据追踪不同部分得出的特定标准来选择采样的追踪,而头部采样则没有这个选项。

可以使用尾部采样的一些示例包括:

  • 始终对包含错误的追踪进行采样。
  • 根据总体延迟对追踪进行采样。
  • 根据一条追踪中一个或多个跨度上特定属性的存在或值对追踪进行采样;例如,对来自新部署服务的追踪进行更多采样。
  • 根据特定标准对追踪应用不同的采样率,比如当追踪仅来自低流量服务时与来自高流量服务时采用不同采样率。

如你所见,尾部采样在数据采样方式上更加精细复杂。对于必须对遥测数据进行采样的大型系统,几乎总是需要使用尾部采样来平衡数据量和数据的有用性。

目前,尾部采样主要有三个缺点:

  • 尾部采样可能难以实施。根据你可用的采样技术,它并不总是 “设置好就不用管” 的事情。随着系统的变化,采样策略也需要改变。对于大型复杂的分布式系统,实施采样策略的规则也会很复杂。
  • 尾部采样可能难以操作。实施尾部采样的组件必须是有状态的系统,能够接受和存储大量数据。根据流量模式,这可能需要几十个甚至几百个计算节点,而且这些节点对资源的利用方式各不相同。此外,如果尾部采样器无法处理接收到的数据量,可能需要 “回退” 到计算量较小的采样技术。由于这些因素,监控尾部采样组件以确保它们拥有做出正确采样决策所需的资源至关重要。
  • 如今,尾部采样器往往是特定供应商的技术。如果你使用付费供应商提供的可观测性服务,你能使用的最有效的尾部采样选项可能会受到供应商提供内容的限制。

最后,对于某些系统,尾部采样可能会与头部采样结合使用。例如,一组产生极高流量追踪数据的服务,可能首先使用头部采样,仅对一小部分追踪进行采样,然后在遥测管道的后续阶段使用尾部采样,在将数据导出到后端之前做出更精细的采样决策。这样做通常是为了防止遥测管道过载。







Alt

Once Day

也信美人终作土,不堪幽梦太匆匆......

如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!

(。◕‿◕。)感谢您的阅读与支持~~~


网站公告

今日签到

点亮在社区的每一天
去签到