前言
前面的文章对tsdb的使用进行了记录,那么从现在开始会记录一些关于opentsdb内部实现的原理,标题写了是源码跟踪,其实跟踪源码就是为了探清楚它是怎么实现的。文章中不会贴上一堆一堆的源码,只会对它的实现过程,以及相关的内部结构进行讲述。本文的主要内容:
- uid和tsuid的概念以及这样设计的意义。
- /api/uid/assign,这个接口可以对metric,tagk,tagv申请uid,将会讲述其实现过程。
- /api/uid/uidmeta,这个接口类似于/api/uid/tsmeta,但是该接口是对uid的meta data进行操作,将会讲述其实现过程。
- /api/uid/tsmeta,这个接口可以对uid对应的meta data进行删除或者更新,将会讲述其实现过程。
uid以及tsuid
我们知道tsdb是通过metric,tags(包括tagk和tagv),timeserious来映射到value的。而且tsdb内部映射关系,是这样的:
- 将metric,tagk1,tagv1,tagk2,tagv2...按照规则生成对应的metric_UID,tagk1_UID,tagv1_UID,tagk2_UID,tagv2_UID,...
- 将第一步生成的全部uid生成一个tsUID,其形式如
[... ]。
一个tsUID就唯一映射到一个时间序列。
uid是由数字生成,并且官方默认的长度3byte,且说明了metric,tagk,tagv各自最大的uid个数是16,777,215。其计算方式是:
3(byte) = 24(bit)
Math.pow(2,24) = 16,777,216
uid默认是自增的形式,也就是从1一直自增到16,777,215。
这样设计的原因究竟是什么呢,为何平白无故耗费一些计算力去增加这个中间层面的uid呢。大兄弟不要急,下面会娓娓道来。
假如我们目前我们有这个时间序列
sys.cpu.0.user host=websv01.lga.mysite.com owner=operations.
我们可以就用这个字符串直接映射到这个时间序列,可是其所占的空间为60byte。
现在我们将metric和tags转化成uid,假设其分别对应的uid如下:
000000000000000000000001 000000000000000000000001 000000000000000000000001 000000000000000000000001
进一步将二进制换算为16进制,得到的tsUID如下:
000001000001000001
可以看到对于这个时间时序,单个时间点存储空间就可以从60byte变成18byte,假如这个时间序列时间跨度很大,并且十分密集,那么能节约的存储空间就非常可观了,也是tsdb这样设计的原因所在。
/api/uid/assign
opentsdb是依赖于hbase的下面几个表进行工作的:
tsdb , tsdb-meta , tsdb-tree , tsdb-uid
这里先介绍tsdb-uid这个标,这个库是用来持久化metric,tags--->uid,以及uid--->metirc,tags映射关系的。
数据库的结构如下:
这更正一下:列族id和列族name下面的metric列其实应该是metrics。
rowkey为\x00是计数器,前面讲到uid是以自增的形式,这个计数器就是用于实现uid的自增,每申请一个新的metric(或者tagk,tagv)对应的计数器就会增加1,并将增加得到数值作为uid。
可以看到metric:sys.cpu.0.user的uid为000001,表中持久化了两者的映射关系:
sys.cpu.0.user ---> 000001
000001 ---> sys.cpu.0.user
好了,现在我们知道表结构了,下面我们来看一下为metric分配uid的过程,即/api/uid/assign接口的实现过程,对于tags的uid分配过程也是一样的。比较简单,看一遍下面不标准的流程图就会明白,嘿嘿。
看了上图之后,可能会想到一个问题。
则多个线程对同一个字符串申请uid,此时就必须避免对uid的重复申请,tsdb中时这样处理的:
有一个线程安全的map,在申请之前会对name进行记录,申请完之后有会从中删除这条记录。为了避免重复申请,就必须在申请之前查询map中是否有这个name的相关记录,若有,则无法申请;若无,则可继续申请。
/api/uid/uidmeta
该接口可以对uid的meta data进行增删改,而meta data可以看成对uid的解释和说明。
上面说我们讲到了 tsdb-uid 这张表,现在将其表结构补充一下:
我们可以看到图1和图3的区别之处在于:
在name列族下面多了:metric_meta,tagk_meta,tagv_meta三个列,这三个列正是以json的格式分别存储了metric,tagk,tagv的meta_data。
meta_data的增删改查也就是对这个三个列进行操作,其过程也十分简单。有个地方需要提一下的是,那就是当有多个线程尝试去修改同一个meta_data时,一定需要避免发生脏读的情况。解决的办法是:
和采取版本号的机制类似,即Atomic Compare-And-Set (CAS) ,先从库中查询原始值,更新的时候会比较如果库中的值已经不是原始值了就会更新失败。
/api/uid/tsmeta
该接口的作用和 /api/uid/uidmeta 接口类似,只是这个接口是对tsUID的mete data进行操作。
tsUID的meta data并不是存储在 tsdb-uid 表中的,而是在tsdb-meta表中,现在我们来看一下 tsdb-meta 的表结构:
rowkey即是tsuid,name列族下面有ts_sct,和ts_meta两个列:
- ts_sct:存储该时间序列收到最新数据的时间,所以每次在对这个时间序列写入新的data point时,ts_sct都会更新。
- ts_meta:以json的形式存储tsuid对应的meta data。
清楚了这个表结构的之后,对meta data的增删改查的操作就十分简单。
在查询中,除了返回meta data之外,还会将tsuid解析为metric,tags,并在 tsdb-uid 表中查询metric,tags的meta data一并返回。
总结
关于/api/uid的内容到此就全部结束了,内容上较为简单,主要是熟悉其中的表结构以及考虑数据的线程安全。
嘿嘿嘿,以后博主画流程图会画好看一点。。。(#^.^#)