半野

什么是可观测性?

系统/应用的可观测性性,是指在不系统内部运作的情况下,从外部就可以理解一个系统的运行状态,排查和处理出现的问题。

当然,为了对系统实施可观测性,应用程序就需要进行适当的 插桩 (Instrumentation), 使得应用程序可以对外发出信号。 这些信号,就是在外部理解系统运行状况的重要依据。

可观测性的三大信号

可观测性的三大支柱是:Logs(日志) / Metrics(指标) / Traces(链路/链路追踪)。三者各有侧重,联合起来才能快速定位问题。

Traces

Trace(链路追踪) 能够观察到请求是如何在分布式系统中传播的,它提高了应用程序或系统健康状况的可见性,并让你能够调试那些难以在本地重现的行为。
对于分布式系统来说,分布式链路是必不可少的,因为这些系统通常存在不确定性问题,或者过于复杂而无法在本地重现。
在 W3C Trace Context 标准里,一个典型的 traceparent HTTP 头可能长这样:

1
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

拆开:

  • 00 → 版本号 (目前仅有 00)
  • 4bf92f3577b34da6a3ce929d0e0e4736 → Trace ID
  • 00f067aa0ba902b7 → Span ID
  • 01 → TraceFlags (Head Sampling,头部采样)

下面会一一介绍这些概念。

TraceId

链路追踪 id,是分布式追踪里用来标识一次完整请求链路的唯一 ID。它是全局唯一的,一次 TraceId 只对应一次端到端的请求。

在 OpenTelemetry 和 W3C Trace Context 标准中,TraceId 是一个 16 字节(128 位)的唯一标识符,通常以 32 个十六进制字符表示(不区分大小写)。

TraceFlags

TraceFlags 用来表示是在头部采样策略中,这个链路否被采样的的标识。

  • 01(二进制 00000001) → 本次 trace 被采样
  • 00(二进制 00000000) → 本次 trace 不采样

在 OpenTelemetry 和 W3C Trace Context 标准中,TraceFlags 是 1 个字节(8 位) 的值,通常跟 Trace ID、Span ID 一起出现在请求头里,帮助各个服务在调用链中保持一致的追踪决策。

Span

一个操作单位,含开始时间、结束时间、属性、事件。

多个 span 的集合,代表一次事务或请求,形成一个 Trace。

1280X1280

上午是一个链路示意图,图中的每个矩形代表一个 Span。
https://opentelemetry.io/docs/concepts/observability-primer/#distributed-traces

SpanId

Span 的 Id,唯一标识一次具体操作(span)。

在 OpenTelemetry 和 W3C Trace Context 标准中,SpanId 是一个 8 字节(64 位) 的唯一标识符,通常以 16 个十六进制字符表示,例如:00f067aa0ba902b7

Attribute/Tag

给 span 添加的键值对数据,例如:

  • http.method=POST
  • http.status_code=200

Event

Span 内部的时序事件,用来记录一个瞬时发生的事件(Span 具有开始和结束两个操作,而 Event 只有单一的时间点)。

比如可以用来记录:“db.query.start”、“db.query.end”、“cache.miss”等等。如果不想开始新的 span,又想留下一些“日志”性质的上下文,就可以用 event。

Span 状态

每个 Span 都有一个状态。三个可能的值是:

  • Unset:默认值,表示它跟踪的操作已成功完成,没有错误。
  • Error:表示它跟踪的操作中发生了一些错误。
  • Ok:意味着应用开发人员已将该 Span 显式标记为无错误。

当已知 Span 已完成且没有错误时,不需要将 Span 状态设置为 Ok,因为 Unset 涵盖了这一点。Ok 的作用是代表对开发者明确设置的 Span 的状态的一个明确的“最终决定”,比如可能原来 Span 的状态是 Error,可以将其覆盖为 Ok。在大多数情况下,没有必要明确地将跨度标记为 Ok

Span 类型

当创建一个 Span 时,它可以是以下几种类型:

  • Client:表示出站远程调用,比如 HTTP Client 的调出。
  • Server:表示远程入站调用,例如 HTTP 的调入。
  • Internal:默认值。表示不跨越进程边界的操作。
  • Producer:表示一个异步处理的操作
  • Consumer:表示是对 Producer 创建的操作的执行/处理

根据 OpenTelemetry 规范:

  • Server Span,父级通常是远程 Client Span
  • Client Span,子级通常是 Server Span
  • Consumer Span,父级始终是 Producer Span
  • Producer Span,子级始终是消费者

Span Context

Span context 是每个 Span 上的一个不可变对象,包含以下内容:

  • 该 Span 所属的 Trace 的 Trace ID
  • 该 Span 的 Span ID
  • Trace Flags
  • Trace State,一个可以携带特定供应商追踪信息的键值对列表

Baggage

Baggage 是一个键值对存储,可以在 Span 之间传递的上下文信息。Baggage 允许你在同一个链路中的服务和进程之间传递数据,从而可以将其添加到这些服务中的链路、指标或日志中。

Baggage 键值对会通过 W3C Baggage 标准(baggage HTTP 头)传递给下游。下游服务接到请求后,可以读取 Baggage 值,或继续追加修改再传下去。

Baggage 最适合用于将请求入口处的元数据的信息,传递到后续处理流程或其他系统中。例如来源 IP,来源系统等。但是,注意不要在 Baggage 中存放敏感信息,因为这些数据可能会传递到第三方不可信的系统中。

采样 (Sampling)

头部采样

请求进入系统的最开始(根 Span 创建时) 就决定这条 Trace 是否被采样。常见以下几种:

  • 全采样
    全部采集。

  • 全不采样
    全部不采集。

  • 固定比例采样
    例如根据 TraceId 的 Hash 值,10% 请求被采样。

  • 按条件采样
    根据请求的 URL、用户、租户、请求来源等判断是否采样。
    根 Span 确认是否采样后,将这个标识放在 TraceFlags 中,并传递到下游服务。下游服务不再重新决定采样,只是遵循上游的决定

尾采样

尾部采样是在整个 Trace 结束后才决定是否保留这条 Trace。

  • 系统先收集整个 Trace 的所有 Span(可能涉及多个服务)
  • 等 Trace 结束后,根据收集到的完整信息(包括结果状态、错误、耗时等)来做采样决策
  • 只有被选中的 Trace 会被最终导出到后端(Jaeger、Zipkin 等)

常见策略:

  • 按错误采样:只保留包含错误的 Trace。
  • 按耗时采样:只保留响应时间超过某个阈值的 Trace。
  • 混合策略:错误全保留,耗材超过 2 秒的圈保留,其他正常请求按 1% 抽样。
  • 染色采样:对特定的请求添加染色标记,对该请求进行强制采样,可以认为是混合策略的一种。

TraceProvider & Tracer

这两个概念是在实际代码工程中会用到的,其中 TracerProviderTracer 的工厂,用来创建 Tracer。每个 Tracer 一般对应一个库、模块或服务(通过名字区分)。在大多数应用程序中,TracerProvider 只需被初始化一次,其生命周期与应用程序的生命周期相匹配。

TracerProvider 的初始化也包括 Resource 和 Exporter 的初始化,它是使用 OpenTelemetry 进行追踪的典型第一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Tracer OtelTracer = GlobalOpenTelemetry.getTracerProvider().get("my-service", "1.0.0")

# 等价于上面的形式
# Tracer OtelTracer = GlobalOpenTelemetry.getTracer("my-service", "1.0.0")

// Span span = Span.current();
Span span = OtelTracer
.spanBuilder("okhttp-request")
.startSpan();
SpanContext spanContext = span.getSpanContext();
String traceId = spanContext.getTraceId()

span.setAttribute("http.route", "/api/login");
span.addEvent("cache.miss");
try {
// do something
span.setStatus(StatusCode.OK);
} catch (Exception e){
span.setStatus(StatusCode.ERROR);
span.addEvent("exception");
} finally {
span.end();
}

Logs

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

OpenTelemetry 并没有专门定义日志打印的 API 或者 SDK,OpenTelemetry Logs 目标是直接使用现有的日志框架,并实现它们与 Trace 关联

Metrics

Metrics(指标) 是一类观测信号,用于度量系统随时间的数值变化,例如:CPU 使用率、内存大小、接口的请求数、接口的延迟等。

和 Trace、Log 不同的是,Metric 可以是一段时间内的 Trace 的聚合统计值。例如接口的延迟,我们一般是计算 t0~t1 一段时间内所有请求的耗时,然后计算 p99;

Metric 也可以是一段时间内某个 Trace 对应的瞬时值。例如接口的请求数,通常我们在 t0 时刻得到一个请求数量 n1,在 t1 时刻得到第二个请求数 n2(假如 t1t2 之间不只有一个请求),那么对于在 t1t2 中的 Trace,我们并不能准确知道其对应的请求数量是多少。(其实,这可以可以看为是 t1~t2 Trace 请求数的聚合统计)

Meter & Meter Provider

Meter 和 Metric 长的很像,但是其实是两个概念,Meter 是用来创建和管理 Metric 的工厂,而 Meter Provider 是用来创建和管理 Meter 的工厂:

Metric

Metric 由以下部分定义:

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

一个示例如下:

1
2
3
# HELP http_requests_total A counter metric that represents the cumulative number of HTTP requests received by the application
# TYPE http_requests_total counter
http_requests_total{path="/api/login", status="200", otel_scope_name="io.opentelemetry.spring-webmvc-6.0"} 3.0

http_requests_total 是指标名称;类型是 counter。path、status 等称之为标签,这是必须的两部分。

标签在不同的库中有不同的叫法,比如 tag、label,在 Java OpenTelemetry 的 SDK 中,使用 attribute 来表示(本文使用 label 叫法)。如果使用 OpenTelemetry,默认会有个 ==otel_scope_name== 的 label。otel_scope_name 使用的是 meter name(instrumentation scope name),而 meter name 则一般使用的是库/包/应用程序名。

基本 Metric Type / Instrument Kind

前面讲到 Metric 必须要有类型,常见的类型有 3 种:

  • Counter:==单调递增==(只能增或重置),用于计数事件,如 http_requests_totalorders_created_total。Prometheus 风格通常以 _total 结尾。
  • Gauge:表示瞬时值,==可增可减==,例如当前在线连接数、队列深度、内存使用量。
  • Histogram:把观测值放进预定义的桶(buckets),==常用于延迟分==布(例如 http_request_duration_seconds_bucket{le="0.1"})。Histogram 在 Prometheus 中可以计算分位数(通过 histogram_quantile() )并有 sumcount 指标。

高基数问题

新手在使用 metric 的时候,一不小心就会掉入到高基数的坑中。

什么是高基数

如果一个 label 的所有值不可枚举(例如 traceId),或者值的数量极其多(比如 userId,可能有百万、千万),则这样的 label 为高基数标签。

高基数会带来什么问题

最常见的两个问题:

  1. 导致数据存储压力大,指标查询速度变慢
  2. 导致应用程序 OOM

metric 数据,是按照 metric_name + label_value 组合存储的。

假如 http_requests_total 这个 metric 只有一个 label http.method,且 http.method 只使用的 GET 和 POST,则只会存储 2 条数据:

1
2
http_requests_total{http.method="GET"}
http_requests_total{http.method="POST"}

如果再加一个 label status,且 status 值只会出现 200, 400, 500,则会存储 2 * 3 条数据:

1
2
3
4
5
6
http_requests_total{http.method="GET", status="200"}
http_requests_total{http.method="GET", status="400"}
http_requests_total{http.method="GET", status="500"}
http_requests_total{http.method="POST", status="200"}
http_requests_total{http.method="POST", status="400"}
http_requests_total{http.method="POST", status="500"}

这些数据会一直在内存中存在。有些人就会问,如果只出现了一次 GET + 500,为什么不能把它从内存中干掉。个人理解,主要是为了区分以下两种状态:

  • metric 从来没有打过点
  • 实例存在问题,上报中断

高基数的一些解决方案:

  • 控制标签维度,高基数信息放到 log 或 trace 中
  • 在 OpenTelemetry Collector 里,可以用 filter processor 丢弃不必要的属性。
  • 在 Prometheus 里,可以通过 labeldroprelabel 去掉危险标签。

常用 PromQL

可以说,Prometheus 是目前云原生领域监控的事实标准,所以极其有必要要了解下其查询语法 PromQL,下面列举了下 对 CounterHistogram 两种指标常用的语法:

  • 错误率(过去 5 分钟):
1
2
3
4
5
(
increase(http_requests_total{job="order-service", status=~"5.."}[5m])
/
increase(http_requests_total{job="order-service"}[5m])
)
  • p99
1
2
3
4
5
6
histogram_quantile(
0.99,
sum(
rate(http_request_duration_seconds_bucket{job="order-service"}[1m])
) by (le)
)

其他概念

Exemplars

前面讲到过,Metric 是对一段时间的 Trace 聚合统计值,所以就会丢失原始的 Trace 信息。而我们收到告警的时候,比如 P99 超时,我们是需要对原始的 Trace 进行分析的。

在 OpenTelemetry Metrics 中,Exemplar 是从大量样本中挑选出来的“代表性数据点”,通常包含额外的上下文信息(比如 Trace Id、Span Id),方便将 Metric 与 Trace 数据关联起来。这就很方便的解决了上面的问题。

在 OpenTelemetry 中,Histogram 由于具有 bucket,可以方便的从某个 bucket 中抽取代表值,因此特别适合记录 Exemplar。但是不代表其他类型无法记录 exemplar。

Histogram 类型

Counter 类型

使用 Exemplar,有两个重要的概念:

  • ExemplarFilter:筛选哪些 Trace 可能成为 Exemplar
  • ExemplarReservoir:Exemplars 的存储与采样

ExemplarFilter

ExemplarFilter 有三个枚举值:

  • AlwaysOn:
    所有测量值都可能被选为 Exemplar(通常每次观测都会记录一个 exemplar)。
  • AlwaysOff:
    完全关闭 Exemplar 采样,不会生成任何 Exemplar。
  • TraceBased:
    只有当测量值是在一个被采样的 trace 上下文中产生的,才会被选为 exemplar。比如一个请求触发了 span,并且该 span 被采样,那么该请求相关的 metric 才会带有 exemplar。

ExemplarReservoir

ExemplarReservoir 是用来管理如何选择并存储 exemplars 的,是 OpenTelemetry SDK 中的一个 抽样存储接口。通俗来讲,它像是一个 OpenTelemetry Collector:

  • 接收数据:在指标更新时,决定是否把该数据点作为 exemplar。
  • 存储与采样:采用采样算法,从所有可能被选择的 exemplar 池子中选择样本。
  • 导出 exemplars:当 metric 被上报或读取时,从 reservoir 中返回 exemplar。

用户基本不需要对 ExemplarReservoir 的实现关心,根据 OpenTelemetry 的 协议规范,SDK 至少要有两个实现,且根据指标类型或 bucket 的个数来自动选择 ExemplarReservoir

Resource

在 OpenTelemetry 里,Resource 表示生成遥测数据(traces、metrics、logs)的 实体本身的属性集合,比如服务名、服务版本等,它描述的是“谁在产生这些数据”。

和 trace span 的属性、metric 的标签不一样,Resource 的属性是 全局性、静态的,通常在进程启动时确定。

由 Hexo 驱动 & 主题 Keep
总字数 5.1k 访客数 访问量