0.时序数据库
时间序列(Time Series):是一组按照时间发生先后顺序进行排列的数据点序列,通常一组时间序列的时间间隔为一恒定值(如1秒,5分钟,1小时等)。
时间序列数据可被简称为时序数据。实时监控系统所收集的监控指标数据,通常就是时序数据 。时序数据具有如下特点:
- 每一个时间序列通常为某一固定类型的数值
- 数据按一定的时间间隔持续产生,每条数据拥有自己的时间戳信息
- 通常只会不断的写入新的数据,几乎不会有更新、删除的场景
- 在读取上,也往往倾向于读取最近写入的数据。
OpenTSDB是其中一种时序数据库实现,是一种基于Hbase、分布式、可伸缩的时间序列数据库。
1.OpenTSDB基本概念
- metric:指标名,这个就是我们监控的指标,比如 sys.cpu.user;
- timestamp:时间戳,监控数据产生的时间;
- value:监控值,Long 或者 Double 类型的数据,这个是监控指标在某个时间的具体值;
- tag:标签,包括标签名字(tagk)和标签值(tagv),比如 tagk1=tagv1,主要用于描述数据属性,每条时序数据必须包含一组和多组的标签数据。目前 OpenTSDB 最多支持8组标签。
2.数据样例:
sys.cpu.user host=webserver01 1356998400 50
sys.cpu.user host=webserver01,cpu=0 1356998400 1
sys.cpu.user host=webserver01,cpu=1 1356998400 0
sys.cpu.user host=webserver01,cpu=2 1356998400 2
sys.cpu.user host=webserver01,cpu=3 1356998400 0
............
sys.cpu.user host=webserver01,cpu=63 1356998400 1
其中,每一行表示时间序列中的一个DataPoint,每部分对应如下:
每一个Data Point,都关联一个metrics名称,但可能关联多组<tagKey,tagValue>信息。而关于时间序列,事实上就是具有相同的metrics名称以及相同的<tagKey,tagValue>组信息的Data Points的集合。
3.存储模型
OpenTSDB是基于HBase进行数据存储,在HBase中存放tsdb、tsdb-meta、tsdb-tree、tsdb-uid四张表,主要用到的是tsdb、tsdb-uid两个。
1)tsdb-uid
为了统一各个值的长度以及节省空间,OpenTSDB中为每一个metrics名、tagKey以及tagValue都定义了一个唯一的数字类型的标识码(Unique Identifier, UID),这些UID信息被保存在OpenTSDB的元数据表tsdb-uid中。同时,为了从UID索引到metrics(或tagKey、tagValue),同时也要从metrics(或tagKey、tagValue)索引到UID,OpenTSDB同时保存了这两种映射关系数据。
列族Column Family:
在元数据表中,把这两种数据分别保存到两个名为**"id"与"name"**的列族(Column Family)中,Column Family描述信息如下所示:
{NAME => 'id', BLOOMFILTER => 'ROW', COMPRESSION => 'SNAPPY'}
{NAME =>'name',BLOOMFILTER => 'ROW', COMPRESSION => 'SNAPPY', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE => '0'}
OpenTSDB分配UID时遵循如下规则:
- metrics、tagKey和tagValue的UID分别独立分配
- 每个metrics名称(或tagKey/tagValue)的UID值都是唯一。不存在不同的metrics(或tagKey/tagValue)使用相同的UID,也不存在同一个metrics(或tagKey/tagValue)使用多个不同的UID
- UID值使用三个字节进行存储,其范围是0x000000到0xFFFFFF。
关于metrics名为"cpu.hum",tagKey值为"host",tagValue值分别为"189.120.205.26"、"189.120.205.27"的UID信息定义如下:
说明:
- RowKey为"0"的行中,分别保存了metrics、tagKey和tagValue的当前UID的最大值。当为新的metrics、tagKey和tagValue分配了新的UID后,会更新对应的最大值
- RowKey为"1"的行中,RowKey为UID,Qualifier(列名)为"id:metrics"的值"metrics",Qualifier为"id:tagk"的值为tagKey,Qualifier为id:tagv的值为tagValue
- RowKey为"2"的行中,RowKey为UID,Qualifier为"id:tagv"的值为tagValue,暂不存在UID为"2"的metrics和tagKey
- RowKey为"189.120.205.26"的行中,Qualifer为"name:tagv"的值为UID。表示当"189.120.205.26"为tagValue时,其UID为1
- RowKey为"189.120.205.27"的行中,Qualifer为"name:tagv"的值为UID。表示当"189.120.205.26"为tagValue时,其UID为2
- RowKey为"cpu.hum"的行中,Qualifer为"name:metrics"的值为UID。表示当cpu.hum为metrics时,其UID为1
- RowKey为"host"的行中,Qualifer为"name:tagk"的值为UID。表示当host为tagValue时,其UID为1
由于HBase的存储数据类型是Bytes,所以UID在存储时会被转换为3个字节长度的Bytes数组进行存储。
TUID:
对每一个Data Point,metrics、timestamp、tagKey和tagValue都是必要的构成元素。除timestamp外,metrics、tagKey和tagValue的UID就可组成一个TSUID,每一个TSUID关联一个时间序列,如下所示:
<metrics_UID><tagKey1_UID><tagValue1_UID>[...<tagKeyN_UID><tagValueN_UID>]
在上面的例子中就涉及两个TSUID,分别是:
2)tsdb
tsdb为存放OpenTSDB中所有数据记录的表,下面具体介绍tsdb的结构设计。
① RowKey设计
tsdb的HBase RowKey中包含主要组成部分为:盐值(Salt)、metrics名称、时间戳、tagKey、tagValue等部分。
在tsdb-uid中提到,为了统一各个值的长度以及节省空间,对metrics名称、tagKey和tagValue分配了UID信息。所以,在HBase RowKey中实际写入的是metrics UID、tagKey UID和tagValue UID(存放在tsdb-uid中)。
HBase RowKey的数据模型如下图所示:
- SALT:建议开启SALT功能,可以有效提高性能。SALT数据的长度是变长的:如果SALT的值值少于256,那么只用一个字节表示即可;如果需要设置更大的SALT值,也会相应地占用更多的空间。
- Metric ID:metrics名经过编码后,每个Metric ID的长度为三个字节。
- Timestamp:这里整点小时级别的时间戳。
- tagKey UID & tagValue UID:tagKey和tagValue经过编码后,每个tagKey UID和tagValue UID的长度都为三个字节。tagKey UID和tagValue UID必须成对出现,最少必须存在1对,最多存在8对。
② Qualifier设计
Qualifier用于保存一个或多个DataPoint中的时间戳、数据类型、数据长度等信息。
由于时间戳中的小时级别的信息已经保存在RowKey中了,所以Qualifier只需要保存一个小时中具体某秒或某毫秒的信息即可,这样可以减少数据占用的空间。
一个小时中的某一秒(少于3600)最多需要2个字节即可表示,而某一毫秒(少于3600000)最多需要4个字节才可以表示。为了节省空间,OpenTSDB没有使用统一的长度,而是对特定的类型采用特性的编码方法。Qualifer的数据模型主要分为如下三种情况:秒、毫秒、秒和毫秒混合。
秒类型
当OpenTSDB接收到一个新的DataPoint的时候,如果请求中的时间戳是秒,那么就会插入一个如下模型的数据。
判断请求中的时间戳为秒或毫秒的方法是基于时间戳数值的大小,如果时间戳的值的超过无符号整数的最大值(即4个字节的长度),那么该时间戳是毫秒,否则为秒。
- Value长度:Value的实际长度是Qualifier的最后3个bit的值加1,即(qualifier & 0x07) + 1。表示该时间戳对应的值的字节数。所以,值的字节数的范围是1到8个字节。
- Value类型:Value的类型由Qualifier的倒数第4个bit表示,即(qualifier & 0x08)。如果值为1,表示Value的类型为float;如果值为0,表示Value的类型为long。
- 时间戳:时间戳的值由Qualifier的第1到第12个bit表示,即(qualifier & 0xFFF0) >>>4。由于秒级的时间戳最大值不会大于3600,所以qualifer的第1个bit肯定不会是1。
毫秒类型
当OpenTSDB接收到一个新的DataPoint的时候,如果请求中的时间戳是毫秒,那么就会插入一个如下模型的数据。
- Value长度:与秒类型相同。
- Value类型:与秒类型相同。
- 时间戳: 时间戳的值由Qualifier的第5到第26个bit表示,即(qualifier & 0x0FFFFFC0) >>>6。
- 标志位:标志位由Qualifier的前4个bit表示。当该Qualifier表示毫秒级数据时,必须全为1,即(qualifier[0] & 0xF0) == 0xF0。
- 第27到28个bit未使用。
混合类型
当同一小时的数据发生合并后,就会形成混合类型的Qualifier。
合并的方法很简单,就是按照时间戳顺序进行排序后,从小到大依次拼接秒类型和毫秒类型的Qualifier即可。
- 秒类型和毫秒类型的数量没有限制,并且可以任意组合。
- 不存在相同时间戳的数据,包括秒和毫秒的表示方式。
遍历混合类型中的所有DataPoint的方法是:
- 从左到右,先判断前4个bit是否为0xF
- 如果是,则当前DataPoint是毫秒型的,读取4个字节形成一个毫秒型的DataPoint
- 如果否,则当前DataPoint是秒型的,读取2个字节形成一个秒型的DataPoint
- 以此迭代即可遍历所有的DataPoint
③ Value设计
HBase Value部分用于保存一个或多个DataPoint的 具体某个时间戳对应的值。
由于在Qualifier中已经保存了DataPoint Value的类型和DataPoint Value的长度,所以无论是秒级还是毫秒级的值,都可以用相同的表示方法,而混合类型就是多个DataPoint Value的拼接。
HBase Value按照长度可以分为如下几种类型:
单字节:当DataPoint Value为long型,且大于等于-128(Byte.MIN_VALUE),且少于或等于127(Byte.MAX_VALUE)的时候,使用1个字节存储。
两字节:当DataPoint Value为long型,且大于等于-32768(Short.MIN_VALUE),且少于或等于32767(Short.MAX_VALUE)的时候,使用2个字节存储。
四字节:当DataPoint Value为long型,且大于等于0x80000000(Integer.MIN_VALUE),且少于或等于0x7FFFFFFF(Integer.MAX_VALUE)的时候,使用4个字节存储。
八字节:当DataPoint Value为long型,且不是上面三种类型的时候,使用8个字节存储。当DataPoint Value为float型的时候,使用8个字节表示。
多字节:
按照时间戳的顺序,把多个Value拼接起来的数据模型如下:
每个格子表示一个DataPoint Value的值,这个DataPoint Value的长度可能是1或2或4或8个字节。
DataPoint Value的顺序与Qualifier中时间戳的顺序一一对应。
混合标志:如果最后1个字节为0x01,表示存在秒级类型和毫秒级类型混合的情况。
指标名字:这个就是我们监控的指标,比如 sys.cpu.user;
时间戳:监控数据产生的时间;
值:Long 或者 Double 类型的数据,这个是监控指标在某个时间的具体值;
标签:包括标签名字(tagk)和标签值(tagv),比如 tagk1=tagv1,主要用于描述数据属性,每条时序数据必须包含一组和多组的标签数据。目前 OpenTSDB 最多支持8组标签。
参考: