深入理解Grafana的数据模型

315 阅读8分钟

原文地址:grafana.github.io/dataplane/c…

概述

相信如今几乎每个开发人员都使用过 Grafana。但大家是否思考过这样一个问题:Grafana 支持各种不同的数据源,每个数据源(Prometheus、MySQL、Loki 和 ElasticSearch 等)都有其独特的数据模型 。那Grafana 的数据源是如何将来自外部服务和 API 的多种数据格式转换为面板可以理解的格式的呢?

数据帧(DataFrame)

为实现这一目标,Grafana 将来自每个数据源的查询结果整合到一个统一的数据结构中,这个数据结构被称为数据帧(Data Fame)。

数据帧是从 R 语言和 Pandas 等分析工具中借用的概念。在 Pandas 中,数据表(Data Table)被称为数据帧(DataFame)。

在 Grafana 中,数据帧是通用数据容器,它既可以表示时序数据(Time Series)也可以表示表数据(Table)。

数据帧(DataFrame)是一种列存储的数据结构,它由元数据(Metadata)和字段(Field) 的集合构成。其中,每一列对应一个字段。每个字段都有其明确地字段类型(FieldType),并支持设置一组可选的标签(Labels)

type Frame struct {
    // Name is used in some Grafana visualizations.
    Name string

    // Fields are the columns of a frame.
    // All Fields must be of the same the length when marshalling the Frame for transmission.
    // There should be no `nil` entries in the Fields slice (making them pointers was a mistake).
    Fields []*Field

    // RefID is a property that can be set to match a Frame to its originating query.
    RefID string

    // Meta is metadata about the Frame, and includes space for custom metadata.
    Meta *FrameMeta
}

帧类型(Kinds and Formats)

数据有多种类型(Kinds),如时序数据(Time Series Data)、数字(Numeric)、日志(Logs)、直方图(Histogram)等。每种类型都有相应的格式(Formats) ,如宽格式(Wide)、长格式(Long)、多格式(Multi)等。

帧类型(FrameType)的定义包含数据的类型和格式,例如,TimeSeriesWide表示时序数据的宽格式,类型和格式如下:

  • Kind:Time Series
  • Format:Wide

时间序列类型(Time Series Kind Formats)

属性

  • 帧应按照时间字段升序排序

  • 时间字段(Time Field(s) ):

    • 不应包含空值
    • 字段名称仅用于显示目的,不应包含labels
    • 对于每个帧,第一个时间字段之后的所有额外时间字段都被视为剩余数据
  • 值字段(Value Field(s) ):

    • 每个数据点(时间,值)的值所在的字段

    • 可以包含一个数值或者布尔类型字段,对于数值字段来说:

      • 在 Go 中可以是 Float64、* Float64 或 Int64 等
      • 在 JS 中可以是 Number
    • 值字段的名称通常是时序的名称(__ name __)

时间序列的宽格式(TimeSeriesWide)

宽格式(Wide Format)在单个帧中包含一组具有相同时间字段的时间序列。它被称为“宽”,是因为随着更多序列的添加,它会变得更宽。

例如:

Name: Wide
Dimensions: 3 fields by 2 rows
+---------------------+-----------------+-----------------+
| Name: time          | Name: cpu       | Name: cpu       |
| Labels:             | Labels: host=a  | Labels: host=b  |
| Type: []time.Time   | Type: []float64 | Type: []float64 |
+---------------------+-----------------+-----------------+
| 2020-01-02 03:04:00 | 3               | 4               |
| 2020-01-02 03:05:00 | 6               | 7               |
+---------------------+-----------------+-----------------+

在宽格式的时间序列数据中,应该具备以下属性:

  • 帧中第一个时间类型的字段是所有时序的时间索引。这意味着它为帧内所有时间序列提供了一个共同的时间参照。
  • 应该只有一个帧包含数据类型声明。这确保了数据类型的一致性和清晰性。
  • 至少应该有一个字段是值字段类型(value FieldType)。值字段存储每个时间点数据值。
  • 如果存在多个数值字段(numeric fields),那么时间字段与帧中每个值字段的组合将构成一个时间序列(指标)。
  • 时间字段中不应包含重复的值(重复的时间戳)。这样可以确保每个时间点都是唯一的,从而避免数据出现歧义。

剩余数据(Remainder Data):

  • 任何没有声明类型或者具有其他类型声明的额外帧被视为剩余数据。
  • 帧中的任何字符串字段(string fields)被视为剩余数据。

注意:

查看 Go 的示例

时间序列多格式(TimeSeriesMulti)

在多格式(Multi Format)中,每个帧都对应一个时间序列。当数据源的响应中包含多个时间序列,并且这些序列中的时间值不一致时,就必须使用多格式来替代宽格式。多格式之所以被称为“多(multi)”,就是因为它的数据可以横跨多个帧。

例如:

Name: multiExample
Dimensions: 2 Fields by 2 Rows
+-------------------------------+-----------------+
| Name: time                    | Name: cpu       |
| Labels:                       | Labels: host=a  |
| Type: []time.Time             | Type: []float64 |
+-------------------------------+-----------------+
| 2020-01-02 03:04:00 +0000 UTC | 3               |
| 2020-01-02 03:05:00 +0000 UTC | 6               |
+-------------------------------+-----------------+

Name: multiExample
Dimensions: 2 Fields by 2 Rows
+-------------------------------+-----------------+
| Name: time                    | Name: cpu       |
| Labels:                       | Labels: host=b  |
| Type: []time.Time             | Type: []float64 |
+-------------------------------+-----------------+
| 2020-01-02 03:04:01 +0000 UTC | 4               |
| 2020-01-02 03:05:01 +0000 UTC | 7               |
+-------------------------------+-----------------+

在多格式的时间序列数据中,应该具备以下属性:

  • 每个帧至少应包含一个时间字段和一个数值字段。在帧中,每种类型的字段(如时间戳字段或数值字段)首次出现时,将被用于定义或标识时间序列。
  • 不同的帧可以有不同长度的字段,也就是说,它们可以包含不同数量的列。然而,在同一帧内,所有列的长度必须相同,以确保数据的一致性。
  • 时间字段中不应包含重复的值(重复的时间戳)。

剩余数据(Remainder Data):

  • 任何没有声明类型或者具有其他类型声明的额外帧被视为剩余数据。
  • 帧中的任何字符串字段(string fields)被视为剩余数据。

注意:

  • 多格式(Multi Format)是唯一能够在不进行数据操作的情况下从其他格式转换而来的格式。因此,它是一种可以包含所有其他类型序列信息的格式。
  • 在这里可以查看 Go 的示例

时间类型长格式(TimeSeriesLong)

这是一种在类似 SQL 的系统中常见的响应格式。你可以在 Grafana 的文档:Multiple dimensions in table format 查看到一些简单(但不是完整的)示例。目前,它作为某些数据源在后端查询类似 SQL 的数据时的数据转换存在,可以参见这个 Go 语言示例来了解该代码的工作原理。

这种格式被称为长(Long)格式(也称为窄(narrow)格式),因为与宽格式相比,它需要更多的行来保存相同的序列数据,因此它会变得更长。示例如下:

Name: Long
Dimensions: 4 Fields by 4 Rows
+-------------------------------+-----------------+-----------------+------------------+
| Name: time                    | Name: aMetric   | Name: bMetric   | Name: host       |
| Labels:                       | Labels:         | Labels:         | Labels:          |
| Type: []time.Time             | Type: []float64 | Type: []float64 | Type: []string   |
+-------------------------------+-----------------+-----------------+------------------+
| 2020-01-02 03:04:00 +0000 UTC | 2               | 10              | foo              |
| 2020-01-02 03:04:00 +0000 UTC | 5               | 15              | bar              |
| 2020-01-02 03:05:00 +0000 UTC | 3               | 11              | foo              |
| 2020-01-02 03:05:00 +0000 UTC | 6               | 16              | bar              |
+-------------------------------+-----------------+-----------------+------------------+

上述的长格式数据可以被转换为宽格式,如下:

Name: Wide
Dimensions: 5 fields by 2 rows
+---------------------+------------------+------------------+------------------+------------------+
| Name: time          | Name: aMetric    | Name: bMetric    | Name: aMetric    | Name: bMetric    |
| Labels:             | Labels: host=foo | Labels: host=foo | Labels: host=bar | Labels: host=bar |
| Type: []time.Time   | Type: []float64  | Type: []float64  | Type: []float64  | Type: []float64  |
+---------------------+------------------+------------------+------------------+------------------+
| 2020-01-02 03:04:00 | 2                | 10               | 5                | 15               |
| 2020-01-02 03:05:00 | 3                | 11               | 6                | 16               |
+---------------------+------------------+------------------+------------------+------------------+

在宽格式的时间序列数据中,应该具备以下属性:

  • 第一个时间字段用作时间戳。

  • 时间字段可以包含重复的时间戳,但必须按时间的升序排序。

  • 可以有多个字符串字段,对于字符串字段来说:

    • 字段的名代表维度名称(例如,时序数据中的 label 名称)。
    • 字段的值代表 label 的值。

剩余数据(Remainder Data):

  • 任何没有声明类型或者具有其他类型声明的额外帧被视为剩余数据。
  • 第一个时间字段之后的任何其他的时间字段。

注意:

  • 在这种格式中,维度(例如:host=value)是从字段中的值中提取的,而不是像其他格式那样在字段模式中声明。

日志类型(Logs)

属性和字段

  • Time 字段:必须

    • 第一个字段是名称为 timestamp的时间字段
    • 不能为空
  • Body 字段:必须

  • Severity 字段:可选

  • ID 字段:可选

示例:

data.NewFrame(
    "logs",
    data.NewField("timestamp", nil, []time.Time{time.UnixMilli(1645030244810), time.UnixMilli(1645030247027), time.UnixMilli(1645030247027)}),
    data.NewField("body", nil, []string{"message one", "message two", "message three"}),
    data.NewField("severity", nil, []string{"critical", "error", "warning"}),
    data.NewField("id", nil, []string{"xxx-001", "xyz-002", "111-003"}),
    data.NewField("labels", nil, []json.RawMessage{[]byte(`{}`), []byte(`{"hello":"world"}`), []byte(`{"hello":"world", "foo": 123.45, "bar" :["yellow","red"], "baz" : { "name": "alice" }}`)}),
)