Elasticsearch之数据压缩算法
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Elasticsearch之数据压缩算法相关的知识,希望对你有一定的参考价值。
参考技术ALucene使用FST算法以字节的方式来存储所有的Term,重复利用Term Index的前缀和后缀,使Term Index小到可以放进内存,减少存储空间,不过相对的也会占用更多的cpu资源。FST在Lucene4.0以后的版本中用于快速定位所查单词在字典中的位置。
Finite StateTransducers,简称 FST,通常中文译作 有穷状态转换器 ,在语音识别和自然语言搜索、处理等方向被广泛应用。
FST的功能类似于字典,可以表示成FST<Key, Value>的形式。其最大的特点是,可以用 O(length(key)) 的复杂度来找到key对应的value,也就是说查找复杂度仅取决于所查找的key长度。
假设我们现在要将以下term index映射到term dictionary的block序号:
最简单的做法就是定义个Map<string, integer="">,大家找到自己的位置对应入座就好了,但从内存占用少的角度想想,有没有更优的办法呢?答案就是FST。
对于经典FST算法来说,要求 Key必须按字典序从小到大加入到FST 中。上面的例子中key已经排好序了。
按照以下步骤建立FST:
最后加入 dogs ,得到最后的结果:
从上图可以看出,每条边有两条属性,一个表示label(key的元素),另一个表示Value(out)。注意Value不一定是数字,还可一是另一个字符串,但要求Value必须满足叠加性,如这里的正整数2 + 8 = 10。字符串的叠加行为: aa + b = aab。
建完这个图之后,我们就可以很容易的查找出任意一个key的Value了。例如:查找dog,我们查找的路径为:0 → 4 → 8 → 9。 其权值和为: 2 + 0 + 0 + 0 = 2。其中最后一个零表示 node[9].finalOut = 0。所以“dog”的Value为2。
Lucene除了上面说到用FST压缩term index外,对posting list也会进行压缩。
有人可能会有疑问:“posting list不是已经只存储文档id了吗?还需要压缩吗?”。设想这样一种情况,Lucene需要对一千万个同学的性别进行索引,而世界上只有男/女这样两个性别,每个posting list都会有数百万个文档id,这里显然有很大的压缩空间与价值,对于减少索引尺寸有非常重要的意义。
Lucene使用 Frame Of Reference 编码来实现对posting list压缩,其思路简单来说就是: 增量编码压缩,将大数变小数,按字节存储 。
示意图如下:
除此之外,Lucene在执行filter操作还会使用一种叫做 Roaring bitmaps 的数据结构来存储文档ID,同样可以达到压缩存储空间的目的。
说到Roaring bitmaps,就必须先从bitmap说起。bitmap是一种很直观的数据结构,假设有某个posting list:
对应的bitmap就是:
非常直观,用0/1表示某个值是否存在,比如10这个值就对应第10位,对应的bit值是1,这样用一个字节就可以代表8个文档id,旧版本(5.0之前)的Lucene就是用这样的方式来压缩的,但这样的压缩方式仍然不够高效,如果有1亿个文档,那么需要12.5MB的存储空间,这仅仅是对应一个索引字段(我们往往会有很多个索引字段)。于是有人想出了Roaring bitmaps这样更高效的数据结构。Roaring bitmaps压缩的原理可以理解为,与其保存100个0,占用100个bit,还不如保存0一次,然后声明这个0有100个。
Bitmap的缺点是存储空间随着文档个数线性增长,Roaring bitmaps需要打破这个魔咒就一定要用到某些指数特性:
将posting list按照65535为界限分块,比如第一块所包含的文档id范围在0 65535之间,第二块的id范围65536 131071,以此类推。再用<商,余数>的组合表示每一组id,这样每组里的id范围都在0~65535内了,剩下的就好办了,既然每组id不会变得无限大,那么我们就可以通过最有效的方式对这里的id存储。
为什么是以65535为界限呢?
程序员的世界里除了1024外,65535也是一个经典值,因为它=2^16-1,正好是用2个字节能表示的最大数,一个short的存储单位,注意到上图里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大块,用节省点用bitset存,小块就豪爽点,2个字节我也不计较了,用一个short[]存着方便。
那为什么用4096来区分大块还是小块呢?
都说程序员的世界是二进制的,4096*2bytes = 8192bytes < 1KB, 磁盘一次寻道可以顺序把一个小块的内容都读出来,再大一位就超过1KB了,需要两次读。
一种常见的压缩存储时间序列的方式是把多个数据点合并成一行。Opentsdb支持海量数据的一个绝招就是定期把很多行数据合并成一行,这个过程叫compaction。类似的vivdcortext使用mysql存储的时候,也把一分钟的很多数据点合并存储到mysql的一行里以减少行数。
这个过程可以示例如下:
合并之后就变成了:
可以看到,行变成了列了。每一列可以代表这一分钟内一秒的数据。
Elasticsearch有一个功能可以实现类似的优化效果,那就是Nested Document。我们可以把一段时间的很多个数据点打包存储到一个父文档里,变成其嵌套的子文档。示例如下:
可以打包成:
这样可以把数据点公共的维度字段上移到父文档里,而不用在每个子文档里重复存储,从而减少索引的尺寸。
在存储的时候,无论父文档还是子文档,对于 Lucene 来说都是文档,都会有文档 Id。但是对于嵌套文档来说,可以保存起子文档和父文档的文档 id 是连续的,而且父文档总是最后一个。有这样一个排序性作为保障,那么有一个所有父文档的 posting list 就可以跟踪所有的父子关系。也可以很容易地在父子文档 id 之间做转换。把父子关系也理解为一个 filter,那么查询时检索的时候不过是又 AND 了另外一个 filter 而已。前面我们已经看到了 Elasticsearch 可以非常高效地处理多 filter 的情况,充分利用底层的索引。
使用了嵌套文档之后,对于 term 的 posting list 只需要保存父文档的 doc id 就可以了,可以比保存所有的数据点的 doc id 要少很多。如果我们可以在一个父文档里塞入 50 个嵌套文档,那么 posting list 可以变成之前的 1/50。
以上是关于Elasticsearch之数据压缩算法的主要内容,如果未能解决你的问题,请参考以下文章
ElasticSearch实战-FST有限状态转换算法(索引数据压缩算法)