Apache Parquet schema
从 Tempo 2.0 开始,Apache Parquet 用作默认的列式块格式。有关更多信息,请参阅 Parquet 配置选项。
本文档描述了与 Parquet 块格式一起使用的 schema。
完全嵌套式 schema 与 Span 导向式 schema
有两种主要的列式 schema 方法:完全嵌套式或 Span 导向式。Span 导向式意味着扁平化的 schema,其中追踪被解构为 Span 行。完全嵌套式 schema 意味着保留当前的追踪结构,例如 Resource/Scope/Spans/Events(嵌套数据在 Parquet 中被原生支持)。在这两种情况下,单个叶子值(如 Span 名称和持续时间)都是独立的列。
我们选择嵌套式 schema 的原因有几个
- 嵌套式 schema 的块大小要小得多。这是因为将资源级属性(例如
service.name
)扁平化到每个 Span 会导致数据大量重复。 - 扁平化 schema 并非真正的“扁平化”,因为每个 Span 仍然包含嵌套数据,例如属性和事件。
- 嵌套式 schema 搜索资源级属性的速度要快得多,因为资源级列非常小(每个批次只有 1 行)。
- 与 OpenTelemetry Protocol Specification (OTLP) 之间的转换非常直接。
- 轻松添加计算列(例如,追踪持续时间),可在多个级别添加,例如按追踪、按批次等。
静态列 vs 动态列
动态列与静态列为 schema 增加了另一层。动态 schema 将每个属性(例如 service.name
和 http.status_code
)存储为自己的列,并且每个 Parquet 文件中的列可以不同。静态 schema 对数据的形态不敏感,所有属性都存储在通用的键/值容器中。
动态 schema 是列式格式的终极梦想,但对于首次发布来说过于复杂。然而,该方法的优势又太好,不容错过,因此我们提出了混合方法。它主要是一个静态 schema,但从追踪数据中提取了一些动态列,这些列基于一些启发式的频繁查询属性。我们计划继续朝这个方向投入,以实现完全动态的 schema,其中追踪属性将在运行时被展开为独立的 Parquet 列。
有关更多信息,请参阅 Parquet 设计文档。
Schema 详情
采用的 Parquet schema 大部分直接翻译自 OTLP,但有一些关键区别。
下表使用了这些缩写
rs
- Resource Spansss
- Scope Spans
名称 | 类型 | 描述 |
TraceID | 字节数组 | 16 字节二进制格式的追踪 ID。 |
TraceIDText | 字符串 | 十六进制文本格式的追踪 ID。 |
StartTimeUnixNano | int64 | 追踪中第一个 Span 的开始时间,自 Unix epoch 以来的纳秒。 |
EndTimeUnixNano | int64 | 追踪中最后一个 Span 的结束时间,自 Unix epoch 以来的纳秒。 |
DurationNano | int64 | 总追踪持续时间(纳秒),计算方法是 EndTimeUnixNano 减去 StartTimeUnixNano。 |
RootServiceName | 字符串 | 根 Span(如果存在)的资源级 service.name 属性(rs.Resource.ServiceName),否则为 null。 |
RootSpanName | 字符串 | 根 Span(如果存在)的名称 (rs.ss.Spans.Name),否则为 null。 |
rs | ResourceSpans 的简写 | |
rs.Resource.ServiceName | 字符串 | 如果存在资源级 service.name 属性,则为其专用列。 https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/#service |
rs.Resource.Cluster | 字符串 | 如果存在资源级 cluster 属性且为字符串类型,则为其专用列。其他类型的值将存储在通用属性列中。 |
rs.Resource.Namespace | 字符串 | 如果存在资源级 namespace 属性且为字符串类型,则为其专用列。其他类型的值将存储在通用属性列中。 |
rs.Resource.Pod | 字符串 | 如果存在资源级 pod 属性且为字符串类型,则为其专用列。其他类型的值将存储在通用属性列中。 |
rs.Resource.Container | 字符串 | 如果存在资源级 container 属性且为字符串类型,则为其专用列。其他类型的值将存储在通用属性列中。 |
rs.Resource.K8sClusterName | 如果存在资源级 k8s.cluster.name 属性且为字符串类型,则为其专用列。其他类型的值将存储在通用属性列中。 https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/k8s/#cluster | |
rs.Resource.K8sNamespaceName | 字符串 | 如果存在资源级 k8s.namespace.name 属性且为字符串类型,则为其专用列。其他类型的值将存储在通用属性列中。 https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/k8s/#namespace |
rs.Resource.K8sPodName | 字符串 | 如果存在资源级 k8s.pod.name 属性且为字符串类型,则为其专用列。其他类型的值将存储在通用属性列中。 https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/k8s/#pod |
rs.Resource.K8sContainerName | 字符串 | 如果存在资源级 k8s.container.name 属性且为字符串类型,则为其专用列。其他类型的值将存储在通用属性列中。 https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/k8s/#container |
rs.Resource.Attrs.Key | 字符串 | 所有没有专用列的资源属性都存储在这些列中作为键值对。Key 列存储名称,然后根据属性的数据类型填充 Value 列中的一个。其他值列将包含 null。 |
rs.Resource.Attrs.Value | 字符串 | 如果属性值为字符串类型,否则为 null。 |
rs.Resource.Attrs.ValueInt | int | 如果属性值为整数类型,否则为 null。 |
rs.Resource.Attrs.ValueDouble | float | 如果属性值为浮点类型,否则为 null。 |
rs.Resource.Attrs.ValueBool | bool | 如果属性值为布尔类型,否则为 null。 |
rs.Resource.Attrs.ValueArray | 字节数组 | 如果属性值为嵌套数组类型,否则为 null。Protocol buffer 编码的二进制数据。 |
rs.Resource.Attrs.ValueKVList | 字节数组 | 如果属性值为嵌套键/值映射类型,否则为 null。Protocol buffer 编码的二进制数据。 |
rs.Resource.DedicatedAttributes | 包含资源范围内专用属性列备用槽的组 | |
rs.Resource.DedicatedAttributes.String01 … String10 | 字符串 | 10 个用于专用属性列的备用列 |
rs.ss | ResourceSpans.ScopeSpans 的简写 | |
rs.ss.Scope | ResourceSpans.ScopeSpans.Scope 的简写 | |
rs.ss.Scope.Name | 字符串 | 如果存在 Scope 名称,否则为空字符串。 https://opentelemetry.io/docs/specs/otel/glossary/#instrumentation-scope |
rs.ss.Scope.Version | 字符串 | 如果存在 Scope 版本,否则为空字符串。 https://opentelemetry.io/docs/specs/otel/glossary/#instrumentation-scope |
rs.ss.Spans.SpanID | 字节数组 | Span 的唯一 ID。 |
rs.ss.Spans.ParentSpanID | 字节数组 | Span 父级的唯一 ID。对于没有父级的根 Span,此值为 null。 |
rs.ss.Spans.ParentID | int32 | 追踪本地数字父级 ID。 |
rs.ss.Spans.NestedSetLeft | int32 | 嵌套集合模型的左边界。也用作追踪本地数字 Span ID。 |
rs.ss.Spans.NestedSetRight | int32 | 嵌套集合模型的右边界。 |
rs.ss.Spans.Name | 字符串 | Span 名称。 |
rs.ss.Spans.StartTimeUnixNano | int64 | Span 的开始时间,自 Unix epoch 以来的纳秒。 |
rs.ss.Spans.DurationNano | int64 | Span 持续时间(纳秒)。 |
rs.ss.Spans.Kind | int | Span 的类型。定义的值:0. 未设置;1. Internal;2. Server;3. Client;4. Producer;5. Consumer; https://opentelemetry.io/docs/reference/specification/trace/api/#spankind |
rs.ss.Spans.StatusCode | int | Span 状态。定义的值:0: Unset; 1: OK; 2: Error。 https://opentelemetry.io/docs/reference/specification/trace/api/#set-status |
rs.ss.Spans.StatusMessage | 字符串 | 随 Error 状态提供的可选消息。 |
rs.ss.Spans.HttpMethod | 字符串 | 如果存在 Span 级 http.method 属性且为字符串类型,则为其专用列,否则为 null。其他类型的值将存储在通用属性列中。 https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#common-attributes |
rs.ss.Spans.HttpStatusCode | int | 如果存在 Span 级 http.status_code 属性且为整数类型,则为其专用列,否则为 null。其他类型的值将存储在通用属性列中。 https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#common-attributes |
rs.ss.Spans.HttpUrl | 字符串 | 如果存在 Span 级 http.url 属性且为字符串类型,则为其专用列,否则为 null。其他类型的值将存储在通用属性列中。 https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#http-client |
rs.ss.Spans.DroppedAttributesCount | int | 被丢弃的属性数量 |
rs.ss.Spans.Attrs.Key | 字符串 | 所有没有专用列的 Span 属性都存储在这些列中作为键值对。Key 列存储名称,然后根据属性的数据类型填充 Value 列中的一个。其他值列将包含 null。 |
rs.ss.Spans.Attrs.Value | 字符串 | 如果属性值为字符串类型,否则为 null。 |
rs.ss.Spans.Attrs.ValueInt | int | 如果属性值为整数类型,否则为 null。 |
rs.ss.Spans.Attrs.ValueDouble | float | 如果属性值为浮点类型,否则为 null。 |
rs.ss.Spans.Attrs.ValueBool | bool | 如果属性值为布尔类型,否则为 null。 |
rs.ss.Spans.Attrs.ValueArray | 字节数组 | 如果属性值为嵌套数组类型,否则为 null。Protocol buffer 编码的二进制数据。 |
rs.ss.Spans.Attrs.ValueKVList | 字节数组 | 如果属性值为嵌套键/值映射类型,否则为 null。Protocol buffer 编码的二进制数据。 |
rs.ss.Spans.DedicatedAttributes | 包含 Span 范围内专用属性列备用槽的组 | |
rs.ss.Spans.DedicatedAttributes.String01 … String10 | 字符串 | 10 个用于专用属性的备用列 |
rs.ss.Spans.DroppedEventsCount | int | 被丢弃的事件数量 |
rs.ss.Spans.Events.TimeUnixNano | int64 | 事件的时间戳,自 Unix epoch 以来的纳秒。 |
rs.ss.Spans.Events.Name | 字符串 | 事件名称或消息。 |
rs.ss.Spans.Events.DroppedAttributesCount | int | 被丢弃的事件属性数量。 |
rs.ss.Spans.Events.Attrs.Key | 字符串 | 所有事件属性都存储在这些列中作为键值对。Key 列存储名称。 |
rs.ss.Spans.Events.Attrs.Value | 字节数组 | 属性值,Protocol buffer 编码的二进制数据。 |
rs.ss.Spans.DroppedLinksCount | int | 被丢弃的链接数量。 |
rs.ss.Spans.Links | 字节数组 | 如果存在 Protocol buffer 编码的 Span 链接,否则为 null。 |
rs.ss.Spans.TraceState | 字符串 | 如果存在 Span 的 TraceState 值,否则为空字符串。https://opentelemetry.io/docs/reference/specification/trace/api/#tracestate |
为了提高可读性,表格省略了 Parquet 中为嵌套列表类型添加的 list.element
组。
Parquet Message 格式的块 Schema 显示
message Trace {
required binary TraceID;
required binary TraceIDText (STRING);
required int64 StartTimeUnixNano (INTEGER(64,false));
required int64 EndTimeUnixNano (INTEGER(64,false));
required int64 DurationNano (INTEGER(64,false));
required binary RootServiceName (STRING);
required binary RootSpanName (STRING);
required group rs (LIST) {
repeated group list {
required group element {
required group Resource {
required group Attrs (LIST) {
repeated group list {
required group element {
required binary Key (STRING);
optional binary Value (STRING);
optional int64 ValueInt (INTEGER(64,true));
optional double ValueDouble;
optional boolean ValueBool;
optional binary ValueKVList (STRING);
optional binary ValueArray (STRING);
}
}
}
required binary ServiceName (STRING);
optional binary Cluster (STRING);
optional binary Namespace (STRING);
optional binary Pod (STRING);
optional binary Container (STRING);
optional binary K8sClusterName (STRING);
optional binary K8sNamespaceName (STRING);
optional binary K8sPodName (STRING);
optional binary K8sContainerName (STRING);
required group DedicatedAttributes {
optional binary String01 (STRING);
optional binary String02 (STRING);
optional binary String03 (STRING);
optional binary String04 (STRING);
optional binary String05 (STRING);
optional binary String06 (STRING);
optional binary String07 (STRING);
optional binary String08 (STRING);
optional binary String09 (STRING);
optional binary String10 (STRING);
}
}
required group ss (LIST) {
repeated group list {
required group element {
required group Scope {
required binary Name (STRING);
required binary Version (STRING);
}
required group Spans (LIST) {
repeated group list {
required group element {
required binary SpanID;
required binary ParentSpanID;
required int32 ParentID (INTEGER(32,true));
required int32 NestedSetLeft (INTEGER(32,true));
required int32 NestedSetRight (INTEGER(32,true));
required binary Name (STRING);
required int64 Kind (INTEGER(64,true));
required binary TraceState (STRING);
required int64 StartTimeUnixNano (INTEGER(64,false));
required int64 DurationNano (INTEGER(64,false));
required int64 StatusCode (INTEGER(64,true));
required binary StatusMessage (STRING);
required group Attrs (LIST) {
repeated group list {
required group element {
required binary Key (STRING);
optional binary Value (STRING);
optional int64 ValueInt (INTEGER(64,true));
optional double ValueDouble;
optional boolean ValueBool;
optional binary ValueKVList (STRING);
optional binary ValueArray (STRING);
}
}
}
required int32 DroppedAttributesCount (INTEGER(32,true));
required group Events (LIST) {
repeated group list {
required group element {
required int64 TimeUnixNano (INTEGER(64,false));
required binary Name (STRING);
required group Attrs (LIST) {
repeated group list {
required group element {
required binary Key (STRING);
required binary Value;
}
}
}
required int32 DroppedAttributesCount (INTEGER(32,true));
}
}
}
required int32 DroppedEventsCount (INTEGER(32,true));
required binary Links;
required int32 DroppedLinksCount (INTEGER(32,true));
optional binary HttpMethod (STRING);
optional binary HttpUrl (STRING);
optional int64 HttpStatusCode (INTEGER(64,true));
required group DedicatedAttributes {
optional binary String01 (STRING);
optional binary String02 (STRING);
optional binary String03 (STRING);
optional binary String04 (STRING);
optional binary String05 (STRING);
optional binary String06 (STRING);
optional binary String07 (STRING);
optional binary String08 (STRING);
optional binary String09 (STRING);
optional binary String10 (STRING);
}
}
}
}
}
}
}
}
}
}
}
追踪级属性
为了提高速度和易用性,我们将一些值投影到追踪级列中
- 追踪 ID - 不存储在每个 Span 上。
- 根服务/Span 名称/StartTimeUnixNano - 这些是每个追踪中根 Span(如果存在)的选定属性。它们用于在 Grafana UI 中显示结果。这些属性在摄取时计算并存储一次以提高效率,这样我们就不必查找根 Span。
DurationNanos
- 总追踪持续时间,在摄取时计算。这支持当前 Tempo 搜索中的最小/最大持续时间过滤,并且比扫描 Span 持续时间列更高效。然而,它可能会随 TraceQL 消失,或者我们可以决定将其更改为 Span 级持续时间过滤。
周知属性
将属性投影到自己的列中对搜索速度和大小都有好处。因此,我们采取了一种有主见的方法,将一些周知属性存储到它们自己的专用列中。所有其他属性存储在通用的键/值映射中,仍然可搜索,但速度不及专用列。我们根据 OpenTelemetry 语义约定 以及我们自己常用(解决我们自身痛点)的属性来选择,但我们认为它们对大多数工作负载都很有用。
资源级属性包括以下内容
service.name
cluster
和k8s.cluster.name
namespace
和k8s.namespace.name
pod
和k8s.pod.name
container
和k8s.container.name
Span 级属性包括以下内容
http.method
http.url
http.status_code
(int)
“Any”类型属性
OTLP 属性具有可变数据类型,这在 protocol-buffers 等格式中很容易实现,但不能直接转换为 Parquet。每个列必须有一个具体的类型。这里有几种可能性,但我们选择为每种具体类型设置可选值。Array
和 KeyValueList
类型存储为 protocol-buffer 编码的字节数组。
repeated group Attrs {
required binary Key (STRING);
# Only one of these will be set
optional binary Value (STRING);
optional boolean ValueBool;
optional double ValueDouble;
optional int64 ValueInt (INT(64,true));
optional binary ValueArray (STRING);
optional binary ValueKVList (STRING);
}
事件属性
事件属性存储为 protocol-buffer 编码。
repeated group Attrs {
required binary Key (STRING);
required binary Value (STRING);
}
压缩和编码
Parquet 对许多压缩算法和数据编码有强大的支持。我们发现以下组合在存储大小和性能方面表现出色
- Snappy 压缩 - 在所有列上启用
- 字典编码 - 在所有字符串列(包括字节数组 ParentSpanID)上启用。大多数字符串具有很高的重复性,因此这非常适合优化存储大小。然而,我们可以通过首先检查字典并消除不匹配的页面来大大加快搜索速度。
- 时间和持续时间(UNIX 纳秒)- Delta 编码
- 很少使用的列,例如
DroppedAttributesCount
- 这些列通常都是零,RLE 效果很好。
Bloom 过滤器
Parquet 原生支持 Bloom 过滤器。然而,Tempo 目前尚未使用它们。Tempo 已拥有复杂的 sharding 和缓存 Bloom 过滤器的支持。