PI实时数据历史记录存储机制
在PI的很多文档上都提到了,存储一条32位浮点型的变量的记录平均只需5个字节,以前就很难想明白PI是怎么做到的。因为一条记录至少需要包含三个字段VQT(V:Value,指变量值;Q:Quality,指数据质量;T:Timestamp,指变量值对应的时间戳),对于32位浮点型变量而言,变量值V就需要4个字节存储,这就意味着Q和T平均只占用一个字节,这几乎不可能。
但PI做到了。当然可能其他品牌也有类似的design。
首先简单介绍一下PI的历史数据文件的基本格式,PI的历史数据都存储在形如piarch.001这样的数据文件中,与之相对应的还有一个形如piarch.001.ann这样的文件,后者是用来存储针对变量记录的注释用的,一般很少使用。对于形如piarch.001这样的数据文件内部,PI采用的是分块(分页)的管理方式,每块(页)的大小为1024字节,这实质上隐含限制了一条变量记录的长度是不可能超过1000字节的。页从文件开头开始编号,第一个页号为0,用于存储跟文件相关的信息。之后的页就用于存储变量历史数据,每个变量初始被分配一个页用于存储数据,随着记录的追加,更多的页被分配给变量,当页数多余3时(根据我的观察)时,PI采用如下的数据结构来组织变量数据(图1)。
图1 PI的历史数据存储结构
从图1可以看到,PI采用的是一种两层的简单存储结构,而不是大家通常想象的多层树型结构(例如B+树)。每一层相邻的两个页之间是互相链接在一起的,同时叶子节点(记录节点)也维护到父节点(索引节点)的反向引用指针。在索引节点上存储的记录是每个记录节点的起始时间和对应的页号。
前面已经说过每个页的大小为1024字节,除去30字节左右的固定页头(不同数据类型的页头会有细微差别,但都包含以下信息:变量ID、当前页号、上一页号、下一页号、父页号、是否为索引节点标志、记录条数、起始时间戳等),每个页用于存储历史数据的有效空间为990个字节左右。
对于历史记录的存储,PI最核心的理念就是“不顾一切的尽可能缩减存储记录所需要的磁盘空间”,为了达到这个目标,PI采用的是变长记录的存储方式(注意:没有关系数据库里面的所谓行偏移数组),而且还不惜牺牲时间精度。对于时间戳T的存储,PI采用的是差分法,即只存储与前一条记录的时间戳之差值,如果这个差值能够用一个字节表示(通常单位为秒,不过依据PI的文档,好像还可能是更大的单位),那么这个时间戳T就只需用一个字节表示;如果无法用一个字节表示,或者差值小于1秒,则使用多个字节表示(2~5?)。对于质量戳Q的存储,PI的策略是如果该质量戳是好的(GOOD),则不存;即只存坏质量戳,这通常意味着对于99.99%的数据记录而言,不需要存储质量戳。
如此一来,存储一条32位浮点变量记录平均只需5个字节就很容易了。