Fork me on GitHub

OpenTSDB 存储结构

时序处理数据库,更快的存储和聚合

OpenTSDB 是为存储时序数据而设计的,它基于 HBase 存储数据,充分发挥了 HBase 的分布式列存储特性,支持数百万每秒的读写,支持千万数目的 Metric,它的特点就是容易扩展,具有灵活的 Tag 机制。其主要用途,就是做监控系统,譬如收集大规模集群( 包括网络设备、操作系统、应用程序 )的 监控数据 并进行存储和聚合查询,在目前的 IoT 方面具有很大的应用价值。

Hbase Schema

OpenTSDB 是基于 Hbase 存储系统的,主要利用了 Hbase 数据自动排序 以及 可靠的分布式特性OpenTSDB 在安装启动时,默认在 Hbase 里面创建 四张 表。分别为:

  1. tsdb: 存储数据点表,也就是存储实际的时序数据,绝大部分的数据是存在这个表中;
  2. tsdb-uid: 存储 nameuid 的映射关系,也就是给字符串的键、值映射成数值,通常包括 metrictagktagv 等字符串映射。
  3. tsdb-meta: 元数据表,这个只有通过配置文件开启才会存储数据的,默认不开启。如果开启了存储的就是你传过来的完整的 json 格式数据,没有经过解析的,这就是元数据。
  4. tsdb-tree: 树形表,这个也是只有开启配置文件选项才能使用,开启后可以由自己创建和管理自己的树形 metric 结构,需要自己设计管理的。

下面主要说明下 tsdbtsdb-uid 的表结构。

UID Table Schema

这里我们主要分析下 OpenTSDB 存储 UID 的表 – tsdb-uid

首先,看下 tsdb-uid 的表结构。其存储的是字符串到 UID 的映射关系。

我们可以通过 hbase shell 通过 Hbase 访问数据库入口查看小 tsdb-uid 的表结构:

1
2
3
4
5
6
7
8
list 'tsdb-uid'
# == 结果如下 ==
Table tsdb-uid is ENABLED
tsdb-uid
COLUMN FAMILIES DESCRIPTION
{NAME => 'id', BLOOMFILTER => 'ROW', VERSIONS => '1',... }
{NAME => 'name', BLOOMFILTER => 'ROW', VERSIONS => '1',... }
2 row(s) in 0.0360 seconds

上面结果我只列出了少量信息,主要看 NAME 就可以了,表示该表的 Column Family, 分别为 name 列族和 id 列族。那具体怎么将字符串映射为 UID ? 这里就需要通过实际的例子来说明。

我们先上传一个数据,格式内容如下,可以通过 Postman 进行上传测试,OpenTSDB 默认上传地址为 http://ip:4242/api/put?details,后缀 details 是为了查看上传反馈。

1
2
3
4
5
6
7
8
9
10
11
[
{
"metric": "sys.test.metric",
"timestamp": 1528784369,
"value": 10,
"tags": {
"hostname": "jiyiren",
"area": "shanghai"
}
}
]

上传成功后返回:

1
2
3
4
5
{
"success": 1,
"failed": 0,
"errors": []
}

我们先看下我们上传的数据格式,需要进行映射的字符串是对应 metric, tagkey, tagvalue 的,这里 tag 有两组,所以要映射的有 5 个字符串,分别为: sys.test.metric, hostname, jiyiren, area, shanghai.

那我们就再用 Hbase Shell 查看表内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
scan 'tsdb-uid'
# === 结果如下 ===
ROW COLUMN+CELL
\x00\x00\x01 column=name:metrics, timestamp=1528517476774, value=sys.test.metric
\x00\x00\x01 column=name:tagk, timestamp=1528517476790, value=area
\x00\x00\x01 column=name:tagv, timestamp=1528517476803, value=shanghai
\x00\x00\x02 column=name:tagk, timestamp=1528517476816, value=hostname
\x00\x00\x02 column=name:tagv, timestamp=1528517476830, value=jiyiren
area column=id:tagk, timestamp=1528517476793, value=\x00\x00\x01
hostname column=id:tagk, timestamp=1528517476819, value=\x00\x00\x02
jiyiren column=id:tagv, timestamp=1528517476832, value=\x00\x00\x02
shanghai column=id:tagv, timestamp=1528517476806, value=\x00\x00\x01
sys.test.metric column=id:metrics, timestamp=1528517476778, value=\x00\x00\x01

从中我们可以看到,数据总是成对出现的,包括 UID 映射字符串字符串映射 UID。上面 5 组是 UID 映射成字符串,下面 5 组是字符串映射为 UID. 前面已经看过 tsdb-uid 表有两个列族,而其中的 name 列族对应的就是 UID 映射成字符串,而 id 列族对应字符串映射为 UID, 这正是这两个列族的作用,这样对于正反查找速度都是极快的。

此外,对于 UID 映射字符串的,每行数据,也就是 rowkey 相同的,至少包含三个列,分别是 metrics, tagk, tagv. 我们可以通过前三行结果看出。

到这里我们知道了 UID 与字符串间是怎么映射以便于查询的,但是 UID 到底是怎么生成的呢?

实际上大家在前面操作 scan 'tsdb-uid' 的时候,结果会列出额外三行以 \x00 开头的数据:

1
2
3
4
ROW                                      COLUMN+CELL 
\x00 column=id:metrics, timestamp=1528517476737, value=\x00\x00\x00\x00\x00\x00\x00\x01
\x00 column=id:tagk, timestamp=1528517476811, value=\x00\x00\x00\x00\x00\x00\x00\x02
\x00 column=id:tagv, timestamp=1528517476825, value=\x00\x00\x00\x00\x00\x00\x00\x02

实际上 UID 是用 3 bytes 表示的非负整型数,并且是自增的,而自增的就要依赖于上一次插入的最新 ID 值,这三行就是分别保存 metrics, tagk, tagv 插入的最新数据的 UID,这样下次插入新的数据只要在对应的值上加 1 就能得到其对应的 UID 了。

Data Table Schema

我们再看看 OpenTSDB 的实际存储时序数据的表 – tsdb

既然 UID 与字符串的映射关系搞定了,那么真实的时序数据存储就好理解了。tsdb 保存了所有的时序数据,其 rowkey 就是由各个字段对应的 UID 组成的。

先查看下 tsdb 数据库结果:

1
2
3
4
scan 'tsdb'
# == 结果如下 ==
ROW COLUMN+CELL
\x00\x00\x01[\x1Fa`\x00\x00\x01\x00\x00\x01\x00\x00\x02\x00\x00\x02 column=t:I\x00, timestamp=1528521000278, value=\x0A

结果值太长了,可以分开看,先看列族里的数据【18.7.10 更正为】:

1
column=t:I\x00, timestamp=1528521000278, value=\x0A

其中 value=\x0A0X0A 化为十进制就是 10, 正好是我们前面上传的 metric 的值。

再看看 rowkey

1
\x00\x00\x01[\x1Fa`\x00\x00\x01\x00\x00\x01\x00\x00\x02\x00\x00\x02

rowkeyOpenTSDB 设计的独特之处,其构成规则为:

1
[salt]<metric_uid><timestamp><tagk1><tagv1>[...<tagkN><tagvN>]

salt 是为了更好的分布式,

我们的上面添加的 metric 为,其中 tagk 会自动按字母排序,所以 area 排在前面:

1
2
3
4
# 字符串对应
sys.test.metric 1528784369 area shanghai hostname jiyiren
# UID 对应 ( timestamp 先不变 )
000001 1528784369 000001 000001 000002 000002

除了 timestamp 和上面结果完全对应,而 timestamp 则是按小时存储的,也就是取 3600 的整数倍的 timestamp 作为当前时间戳。计算方法 timestamp - timestamp % 3600.

这样,我们应该对 OpenTSDBUID 以及 Rowkey 的生成和存储结构都基本了解了。

相关链接

img.godjiyi.cn

苟且一下