听说过时序数据库吗?( 四 )


如何减少文档数?
一种常见的压缩存储时间序列的方式是把多个数据点合并成一行 。Opentsdb支持海量数据的一个绝招就是定期把很多行数据合并成一行,这个过程叫compaction 。类似的vivdcortext使用mysql存储的时候,也把一分钟的很多数据点合并存储到mysql的一行里以减少行数 。
这个过程可以示例如下:

听说过时序数据库吗?

文章插图
 
合并之后就变成了:
听说过时序数据库吗?

文章插图
 
可以看到,行变成了列了 。每一列可以代表这一分钟内一秒的数据 。
Elasticsearch有一个功能可以实现类似的优化效果,那就是Nested Document 。我们可以把一段时间的很多个数据点打包存储到一个父文档里,变成其嵌套的子文档 。示例如下:
{timestamp:12:05:01, idc:sz, value1:10,value2:11}{timestamp:12:05:02, idc:sz, value1:9,value2:9}{timestamp:12:05:02, idc:sz, value1:18,value:17}可以打包成:
{max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,records: [ {timestamp:12:05:01, value1:10,value2:11}{timestamp:12:05:02, value1:9,value2:9}{timestamp:12:05:02, value1:18,value:17}]}这样可以把数据点公共的维度字段上移到父文档里,而不用在每个子文档里重复存储,从而减少索引的尺寸 。
听说过时序数据库吗?

文章插图
 
在存储的时候,无论父文档还是子文档,对于Lucene来说都是文档,都会有文档Id 。但是对于嵌套文档来说,可以保存起子文档和父文档的文档id是连续的,而且父文档总是最后一个 。有这样一个排序性作为保障,那么有一个所有父文档的posting list就可以跟踪所有的父子关系 。也可以很容易地在父子文档id之间做转换 。把父子关系也理解为一个filter,那么查询时检索的时候不过是又AND了另外一个filter而已 。前面我们已经看到了Elasticsearch可以非常高效地处理多filter的情况,充分利用底层的索引 。
使用了嵌套文档之后,对于term的posting list只需要保存父文档的doc id就可以了,可以比保存所有的数据点的doc id要少很多 。如果我们可以在一个父文档里塞入50个嵌套文档,那么posting list可以变成之前的1/50 。
如何利用索引和主存储,是一种两难的选择 。
  • 选择不使用索引,只使用主存储:除非查询的字段就是主存储的排序字段,否则就需要顺序扫描整个主存储 。
  • 选择使用索引,然后用找到的row id去主存储加载数据:这样会导致很多碎片化的随机读操作 。
没有所谓完美的解决方案 。MySQL支持索引,一般索引检索出来的行数也就是在1~100条之间 。如果索引检索出来很多行,很有可能MySQL会选择不使用索引而直接扫描主存储,这就是因为用row id去主存储里读取行的内容是碎片化的随机读操作,这在普通磁盘上很慢 。
Opentsdb是另外一个极端,它完全没有索引,只有主存储 。使用Opentsdb可以按照主存储的排序顺序快速地扫描很多条记录 。但是访问的不是按主存储的排序顺序仍然要面对随机读的问题 。
Elasticsearch/Lucene的解决办法是让主存储的随机读操作变得很快,从而可以充分利用索引,而不用惧怕从主存储里随机读加载几百万行带来的代价 。
Opentsdb 的弱点
Opentsdb没有索引,主存储是Hbase 。所有的数据点按照时间顺序排列存储在Hbase中 。Hbase是一种支持排序的存储引擎,其排序的方式是根据每个row的rowkey(就是关系数据库里的主键的概念) 。MySQL存储时间序列的最佳实践是利用MySQL的Innodb的clustered index特性,使用它去模仿类似Hbase按rowkey排序的效果 。所以Opentsdb的弱点也基本适用于MySQL 。Opentsdb的rowkey的设计大致如下:
[metric_name][timestamp][tags]举例而言:
Proc.load_avg.1m 12:05:00 ip=10.0.0.1Proc.load_avg.1m 12:05:00 ip=10.0.0.2Proc.load_avg.1m 12:05:01 ip=10.0.0.1Proc.load_avg.1m 12:05:01 ip=10.0.0.2Proc.load_avg.5m 12:05:00 ip=10.0.0.1Proc.load_avg:5m 12:05:00 ip=10.0.0.2也就是行是先按照metric_name排序,再按照timestamp排序,再按照tags来排序 。
对于这样的rowkey设计,获取一个metric在一个时间范围内的所有数据是很快的,比如Proc.load_avg.1m在12:05到12:10之间的所有数据 。先找到Proc.load_avg.1m 12:05:00的行号,然后按顺序扫描就可以了 。
但是以下两种情况就麻烦了 。