什么是可观测性?
系统/应用的可观测性性,是指在不系统内部运作的情况下,从外部就可以理解一个系统的运行状态,排查和处理出现的问题。
当然,为了对系统实施可观测性,应用程序就需要进行适当的 插桩 (Instrumentation), 使得应用程序可以对外发出信号。 这些信号,就是在外部理解系统运行状况的重要依据。
可观测性的三大信号
可观测性的三大支柱是:Logs(日志) / Metrics(指标) / Traces(链路/链路追踪)。三者各有侧重,联合起来才能快速定位问题。
Traces
Trace(链路追踪) 能够观察到请求是如何在分布式系统中传播的,它提高了应用程序或系统健康状况的可见性,并让你能够调试那些难以在本地重现的行为。
对于分布式系统来说,分布式链路是必不可少的,因为这些系统通常存在不确定性问题,或者过于复杂而无法在本地重现。
在 W3C Trace Context 标准里,一个典型的 traceparent HTTP 头可能长这样:
1 | traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 |
拆开:
00→ 版本号 (目前仅有 00)4bf92f3577b34da6a3ce929d0e0e4736→ Trace ID00f067aa0ba902b7→ Span ID01→ 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。
上午是一个链路示意图,图中的每个矩形代表一个 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=POSThttp.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 规范:
ServerSpan,父级通常是远程ClientSpanClientSpan,子级通常是ServerSpanConsumerSpan,父级始终是ProducerSpanProducerSpan,子级始终是消费者
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
这两个概念是在实际代码工程中会用到的,其中 TracerProvider 是 Tracer 的工厂,用来创建 Tracer。每个 Tracer 一般对应一个库、模块或服务(通过名字区分)。在大多数应用程序中,TracerProvider 只需被初始化一次,其生命周期与应用程序的生命周期相匹配。
TracerProvider 的初始化也包括 Resource 和 Exporter 的初始化,它是使用 OpenTelemetry 进行追踪的典型第一步。
1 | Tracer OtelTracer = GlobalOpenTelemetry.getTracerProvider().get("my-service", "1.0.0") |
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 | # HELP http_requests_total A counter metric that represents the cumulative number of HTTP requests received by the application |
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_total、orders_created_total。Prometheus 风格通常以_total结尾。 - Gauge:表示瞬时值,==可增可减==,例如当前在线连接数、队列深度、内存使用量。
- Histogram:把观测值放进预定义的桶(buckets),==常用于延迟分==布(例如
http_request_duration_seconds_bucket{le="0.1"})。Histogram 在 Prometheus 中可以计算分位数(通过histogram_quantile())并有sum和count指标。
高基数问题
新手在使用 metric 的时候,一不小心就会掉入到高基数的坑中。
什么是高基数
如果一个 label 的所有值不可枚举(例如 traceId),或者值的数量极其多(比如 userId,可能有百万、千万),则这样的 label 为高基数标签。
高基数会带来什么问题:
最常见的两个问题:
- 导致数据存储压力大,指标查询速度变慢
- 导致应用程序 OOM
metric 数据,是按照 metric_name + label_value 组合存储的。
假如 http_requests_total 这个 metric 只有一个 label http.method,且 http.method 只使用的 GET 和 POST,则只会存储 2 条数据:
1 | http_requests_total{http.method="GET"} |
如果再加一个 label status,且 status 值只会出现 200, 400, 500,则会存储 2 * 3 条数据:
1 | http_requests_total{http.method="GET", status="200"} |
这些数据会一直在内存中存在。有些人就会问,如果只出现了一次 GET + 500,为什么不能把它从内存中干掉。个人理解,主要是为了区分以下两种状态:
- metric 从来没有打过点
- 实例存在问题,上报中断
高基数的一些解决方案:
- 控制标签维度,高基数信息放到 log 或 trace 中
- 在 OpenTelemetry Collector 里,可以用
filterprocessor 丢弃不必要的属性。 - 在 Prometheus 里,可以通过
labeldrop、relabel去掉危险标签。
常用 PromQL
可以说,Prometheus 是目前云原生领域监控的事实标准,所以极其有必要要了解下其查询语法 PromQL,下面列举了下 对 Counter 和 Histogram 两种指标常用的语法:
- 错误率(过去 5 分钟):
1 | ( |
- p99
1 | histogram_quantile( |
其他概念
Exemplars
前面讲到过,Metric 是对一段时间的 Trace 聚合统计值,所以就会丢失原始的 Trace 信息。而我们收到告警的时候,比如 P99 超时,我们是需要对原始的 Trace 进行分析的。
在 OpenTelemetry Metrics 中,Exemplar 是从大量样本中挑选出来的“代表性数据点”,通常包含额外的上下文信息(比如 Trace Id、Span Id),方便将 Metric 与 Trace 数据关联起来。这就很方便的解决了上面的问题。
在 OpenTelemetry 中,Histogram 由于具有 bucket,可以方便的从某个 bucket 中抽取代表值,因此特别适合记录 Exemplar。但是不代表其他类型无法记录 exemplar。
使用 Exemplar,有两个重要的概念:
ExemplarFilter:筛选哪些 Trace 可能成为 ExemplarExemplarReservoir: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 的属性是 全局性、静态的,通常在进程启动时确定。