Elasticsearch原理

Elasticsearch原理,第1张

Elasticsearch原理 1、基本概念 索引(Index):

ES将数据存储于一个或多个索引中,索引是具有类似特性的文档的集合。类比传统的关系型数据库领域来说,索引相当于SQL中的一个数据库,或者一个数据存储方案(schema)。索引由其名称(必须为全小写字符)进行标识,并通过引用此名称完成文档的创建、搜索、更新及删除 *** 作。一个ES集群中可以按需创建任意数目的索引。

索引指的是针对某个字段的索引,如果有三个字段,就是三个索引。虽然类似MySQL的数据库,但是还是不一样,可以理解es索引组是只有一张表的MySQL数据库
题外话:正排索引存在磁盘。

类型(Type)

类型是索引内部的逻辑分区(category/partition),其意义完全取决于用户需求。因此,一个索引内部可定义一个或多个类型(type)。一般来说,类型就是为那些拥有相同的域的文档做的预定义。例如,在索引中,可以定义一个用于存储用户数据的类型,一个存储日志数据的类型,以及一个存储评论数据的类型。类比传统的关系型数据库领域来说,类型相当于 “表” 。Elasticsearch在 7.X版本中去除type的概念。

文档(document)

文档是索引和搜索的原子单位,它是包含了一个或多个域(Field)的容器,基于JSON格式进行表示。文档由一个或多个域组成,每个域拥有一个名字及一个或多个值,有多个值的域通常称为 “多值域” 。每个文档可以存储不同的域集,但同一类型下的文档至应该有某种程度上的相似之处。

倒排索引(Inverted Index)

每一个文档都对应一个ID。倒排索引会按照指定语法对每一个文档进行分词,然后维护一张表,列举所有文档中出现的terms以及它们出现的文档ID和出现频率。搜索时同样会对关键词进行同样的分词分析,然后查表得到结果。

什么是倒排索引:

假设有下列几条数据:

ID是Elasticsearch自建的文档id,那么Elasticsearch建立的索引如下:

Elasticsearch分别为每个field都建立了一个倒排索引,Kate, John, 24, Female这些叫term,而 [1,2] 就是Posting List。Posting list就是一个int的数组,存储了所有符合某个term的文档id。通过posting list这种索引方式可以很快进行查找,比如要找age=24的人。

Term Dictionary

Elasticsearch为了能快速找到某个term,将所有的term排序,二分法查找term,logN的查找效率,就像通过字典查找一样,这就是Term Dictionary。类似于传统数据库的B-Tree的,但是Term Dictionary较B-Tree的查询快。

Term Index

B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,直接通过内存查找term,不读磁盘,但是如果term太多,term dictionary也会很大,放内存不现实,于是有了Term Index,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,term index其实是一颗 (trie) 前缀树。

这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。

所以term index不需要存下所有的term,而仅仅是他们的一些前缀与Term Dictionary的block之间的映射关系,再结合FST(Finite State Transducers)的压缩技术,可以使term index缓存到内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘随机读的次数。

假设我们现在要将mop, moth, pop, star, stop, top (term index里的term前缀) 映射到序号:0,1,2,3,4,5 (term dictionary的block位置)。最简单的做法就是定义个Map,大家找到自己的位置取值即可,但从内存占用少的角度考虑,FST更节省空间。

⭕️ 表示一种状态

–> 表示状态的变化过程,上面的字母/数字表示状态变化和权重

将单词分成单个字母通过⭕️和–>表示出来,0权重不显示。如果⭕️后面出现分支,就标记权重,最后整条路径上的权重加起来就是这个单词对应的序号。

FST以字节的方式存储所有的term,这种压缩方式可以有效的缩减存储空间,使得term index足以放进内存,但这种方式也会导致查找时需要更多的CPU资源。

前缀索引是常驻内存的,lucene6之前是字典树,之后是FST结构。
题外话:正排索引存在磁盘。

压缩技巧

Elasticsearch里除了上面说到用FST压缩term index外,对posting list也有压缩技巧。如果Elasticsearch需要对人的性别进行索引,如果有上千万个人,而性别只分男/女,每个posting list都会有至少百万个文档id。Elasticsearch采用一定的压缩算法对这些文档id进行压缩:

增量编码压缩,将大数变小数,按字节存储(frame Of Reference),用于倒排索引

首先,Elasticsearch要求posting list是有序的(为了提高搜索的性能),这样做的好处是方便压缩,看下面这个图例:

原理就是通过增量,将原来的大数变成小数仅存储增量值,再精打细算按bit排好队,最后通过字节存储,而不是用int(4个字节)来存储。

step 1、将排序的整数列表转换成delta列表

step 2、切分成blocks。具体是怎么做的呢?Lucene是规定每个block是256个delta,这里为了简化一下,搞成3个delta。

step 3、看下每个block最大的delta是多少。上图的第一个block,最大的delta是227,最接近的2次幂是256(8bits),于是规定这个block里都用8bits来编码(看绿色的header就是8),第二个block,最大的delta是30,最接近的2次幂是32(5bits),于是规定这个block里都用5bit来编码(看绿色的header就是5)

Roaring bitmaps,用于Filter缓存

Roaring bitmaps基于bitmap。Bitmap是一种数据结构,假设某个posting list:[1,3,4,7,10],其对应的bitmap就是:[1,0,1,1,0,0,1,0,0,1]。

用0/1表示某个值是否存在,存在的值对应的bit值是1,即一个字节 (8位) 可以代表8个文档id,旧版本 (5.0之前) 的Lucene就是用这样的方式来压缩的,但这样的压缩方式仍然不够高效,如果有1亿个文档,那么需要12.5MB的存储空间,这仅仅是对应一个索引字段。于是衍生出了Roaring bitmaps这样更高效的数据结构。

    Roaring Bitmap首先会根据每个id的高16位分配id到对应的block里面(将posting list按照65535为界限分块 (block) ,比如第一块所包含的文档id范围在0-65535之间,第二块的id范围是65536-131071,以此类推。再用<商,余数>的组合表示每一组id,这样每组里的id范围都在0~65535内了。)对于每一个block里面的数据,根据id数量分成两类
    · 如果数量小于4096,就是用short数组保存
    · 数量大于等于4096,就使用bitmap保存

在每一个block里面,一个数字实际上只需要2个字节来保存就行了,因为高16位在这个block里面都是相同的,高16位就是block的id,block id和文档的id都用short保存。

Bitmap的缺点是存储空间随着文档个数线性增长,Roaring bitmaps利用了某些指数特性来规避这一点:

”为什么是以65535为界限?”

65535=2^16-1,正好是2个字节能表示的最大数,一个short的存储单位,如果是较大的 (block) 块,用 bitset 存,小块用一个 short[] 存储。

那为什么用4096来区分采用数组还是bitmap的阀值呢?

这个是从内存大小考虑的,当block块里元素超过4096后,用bitmap更省空间: 采用bitmap需要的空间是恒定的: 65536/8 = 8192bytes 而如果采用short[],所需的空间是: 2*N(N为数组元素个数) N=4096刚好是边界:

联合索引

上述都是单field索引,如果是多个field索引的联合查询,倒排索引如何满足快速查询的要求呢?

利用跳表(Skip list)的数据结构快速做“与”运算,或者利用上面提到的bitset按位“与”。先看看跳表的数据结构:

将一个有序链表level0,挑出其中几个元素到level1及level2,每个level越往上,选出来的指针元素越少,查找时依次从高level往低查找,比如55,先找到level2的31,再找到level1的47,最后找到55,一共3次查找,查找效率和2叉树的效率相当,但也是用了一定的空间冗余来换取的。

假设有下面三个posting list需要联合索引:

如果使用跳表,对最短的posting list中的每个id,逐个在另外两个posting list中查找看是否存在,最后得到交集的结果。

如果使用bitset,就很直观了,直接按位与,得到的结果就是最后的交集。

节点(Node)

一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。

ES集群中的节点有三种不同的类型:

主节点:负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 主节点并不需要涉及到文档级别的变更和搜索等 *** 作。可以通过属性node.master进行设置。数据节点:存储数据和其对应的倒排索引。默认每一个节点都是数据节点(包括主节点),可以通过node.data属性进行设置。协调节点:如果node.master和node.data属性均为false,则此节点称为协调节点,用来响应客户请求,均衡每个节点的负载。 分片(Shard)

一个索引中的数据保存在多个分片中,相当于水平分表。一个分片便是一个Lucene 的实例,它本身就是一个完整的搜索引擎。我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。

ES实际上就是利用分片来实现分布式。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, ES会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。

一个分片可以是主分片或者副本分片。 索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。一个副本分片只是一个主分片的拷贝。 副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读 *** 作提供服务。

如果当前插入大量数据,那么会对es集群造成一定的压力,所以在插入大量数据前,也就是在建立索引的时候,我们最好把副本数设置为0;等数据建立完索引之后,在手动的将副本数更改到2,这样可以提高数据的索引效率。

在索引建立的时候就已经确定了主分片数,但是副本分片数可以随时修改。默认情况下,一个索引会有5个主分片,而其副本可以有任意数量。

主分片和副本分片的状态决定了集群的健康状态。每一个节点上都只会保存主分片或者其对应的一个副本分片,相同的副本分片不会存在于同一个节点中。如果集群中只有一个节点,则副本分片将不会被分配,此时集群健康状态为yellow,存在丢失数据的风险。

实际上,每一个分片还会进一步拆分为分段(Segment)。这是ES写入文档所采用的机制决定的。

3个节点,3个主分片,1份副本:
3个节点,3个主分片,2份副本:

NODE1宕机:

2、写数据

原理解析预备知识

index:类似数据库,是存储、索引数据的地方。shard:index 由 shard 组成,一个 primary shard,其他是 replica shard。segment:shard 包含 segment,segment 中是倒排索引,它是不可变的;segment 内的文档数量的上限是 2^31。倒排索引:倒排索引是 Lucene 中用于使数据可搜索的数据结构。translog:记录文档索引和删除 *** 作的日志。Lucene 在每次 commit 之后把数据持久化到磁盘,但是 commit *** 作代价很大,所以不能在每次数据变更之后执行 commit。Elasticsearch 为防止宕机造成数据丢失,每次写入数据时会同步写到buffer和translog,在 flush *** 作时把数据持久化。commit point:列出所有已知 segment 的文件。

当用户向一个节点提交了一个索引新文档的请求,节点会计算新文档应该加入到哪个分片(shard)中。每个节点都存储有每个分片存储在哪个节点的信息,因此协调节点会将请求发送给对应的节点。注意这个请求会发送给主分片,等主分片完成索引,会并行将请求发送到其所有副本分片,保证每个分片都持有最新数据。

每次写入新文档时,都会先写入内存buffer(这时数据是搜索不到的),同时将数据写入translog日志文件。
es每隔1秒(可配置)或者buffer快满时执行一次刷新(refresh) *** 作,将buffer数据refresh到os cache即 *** 作系统缓存。这时数据就可以被搜索到了;buffer的文档被写入到一个新的segment 中(这1秒时间内写入内存的新文档都会被写入一个文件系统缓存(filesystem cache)中,并构成一个分段(segment));segment被打开以供搜索(此时这个segment里的文档可以被搜索到,但是尚未写入硬盘,即如果此时发生断电,则这些文档可能会丢失);内存buffer清空。
不断有新的文档写入,则这一过程将不断重复执行。每隔一秒将生成一个新的segment,当translog越来越大达到一定长度的时候,就会触发 flush *** 作(flush 完成了Lucene的commit *** 作)第一步将buffer中现有数据refresh到os cache中去,清空buffer;然后,将一个commit point写入磁盘文件,同时强行将os cache中目前所有的数据都fsync到磁盘文件中去;最后清空现有translog日志文件并生成新的translog。

由上面的流程可以看出,在两次fsync *** 作之间,存储在内存和文件系统缓存中的文档是不安全的,一旦出现断电这些文档就会丢失。所以ES引入了translog来记录两次fsync之间所有的 *** 作,这样机器从故障中恢复或者重新启动,ES便可以根据translog进行还原。

当然,translog本身也是文件,存在于内存当中,如果发生断电一样会丢失。因此,**ES会在每隔5秒时间或是一次写入请求完成后将translog写入磁盘。**可以认为一个对文档的 *** 作一旦写入磁盘便是安全的可以复原的,因此只有在当前 *** 作记录被写入磁盘,ES才会将 *** 作成功的结果返回发送此 *** 作请求的客户端。

3、segement合并

buffer每refresh一次,就会产生一个segment(默认情况下是 1 秒钟产生一个),这样segment会越来越多,此时会定期执行merge。

将多个segment合并成一个,并将新的segment写入磁盘;新增一个commit point,标识所有新的segment;新的segment被打开供搜索使用;删除旧的segment。

对于一个分片进行查询请求,将会轮流查询分片中的所有segment,这将降低搜索的效率。因此ES会自动启动合并segment的工作,将一部分相似大小的segment合并成一个新的大segment。合并的过程实际上是创建了一个新的segment,当新segment被写入磁盘,所有被合并的旧segment被清除。


合并完成后删除旧segment,新segment可供搜索。

整个过程如图:

4、删除和更新

由于 segment 是不可变的,索引删除的时候既不能把文档从 segment 删除,也不能修改 segment 反映文档的更新。

删除 *** 作,会生成一个.del文件,commit point会包含这个 .del 文件。.del文件将文档标识为deleted状态,在结果返回前从结果集中删除。更新 *** 作,会将原来的文档标识为deleted状态,然后新写入一条数据。查询时两个文档有可能都被索引到,但是被标记为删除的文档会被从结果集删除。 5、查询

查询的过程大体上分为查询(query)和取回(fetch)两个阶段。这个节点的任务是广播查询请求到所有相关分片,并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端。

查询阶段

当一个节点接收到一个搜索请求,则这个节点就变成了协调节点。

第一步是广播请求到索引中每一个节点的分片拷贝。 查询请求可以被某个主分片或某个副本分片处理,协调节点将在之后的请求中轮询所有的分片拷贝来分摊负载。

每个分片将会在本地构建一个优先级队列。如果客户端要求返回结果排序中从第from名开始的数量为size的结果集,则每个节点都需要生成一个from+size大小的结果集,因此优先级队列的大小也是from+size。分片仅会返回一个轻量级的结果给协调节点,包含结果集中的每一个文档的ID和进行排序所需要的信息。

协调节点会将所有分片的结果汇总,并进行全局排序,得到最终的查询排序结果。此时查询阶段结束。

取回阶段

查询过程得到的是一个排序结果,标记出哪些文档是符合搜索要求的,此时仍然需要获取这些文档返回客户端。

协调节点会确定实际需要返回的文档,并向含有该文档的分片发送get请求;分片获取文档返回给协调节点;协调节点将结果返回给客户端。

相关性计算

在搜索过程中对文档进行排序,需要对每一个文档进行打分,判别文档与搜索条件的相关程度。在旧版本的ES中默认采用TF/IDF(term frequency/inverse document frequency)算法对文档进行打分。

预热filesystem cache

如果我们重启了Elasticsearch,那么filesystem cache是空的,每次数据查询时再加载数据进filesystem cache,我们可以先对一些数据进行查询,提前将一些常用数据加载到内存,待真实客户使用时,可以直接使用内存数据,响应就很快了。

6、Filter执行原理

当一个filter搜索请求打到Elasticsearch的时候,ES会进行下面的 *** 作:

(1)在倒排索引中查找搜索串,获取document list

以date来举例:

filter: 2019-02-02
在倒排索引中寻找,我们发现2019-02-02对应的document list是doc2、doc3

(2)为每个在倒排索引中搜索到的结果,构建一个bitset

这一步是非常重要的,使用找到的doc list,构建一个bitset,即一个二进制的数组,数组的每个元素都是0或1,用来标识一个doc对一个filter条件是否匹配,如果匹配的话值就是1,不匹配值就是0。

所以上面的filter的bitset的结果就是:

[0,1,1]

doc1:不匹配这个filter的
doc2和doc3:匹配这个filter的

注:尽可能用简单的数据结构去实现复杂的功能,可以节省内存空间,提升性能。

(3)遍历每个过滤条件对应的bitset,优先从最稀疏的开始搜索,查找满足所有条件的document

由于一次性可以在一个search请求中发出多个filter条件,那么就会产生多个bitset,遍历每个filter条件对应的bitset优先从最稀疏的开始遍历

[0,0,0,0,0,0,0,1]  比较稀疏的bitset
[1,0,1,1,0,1,0,1]

这里主要是因为先遍历比较稀疏的bitset,就可以先过滤掉尽可能多的数据

(4)caching bitset

caching bitset会跟踪query,在最近256个query中超过一定次数的过滤条件,缓存其bitset。对于小segment(<1000 或<3%),不缓存bitset。这样下次如果在有这个条件过来的时候,就不用重新扫描倒排索引,反复生成bitset,可以大幅度提升性能。

说明:
1、在最近的256个filter中,有某个filter超过了一定次数,这个次数不固定,那么elasticsearch就会缓存这个filter对应的bitset
2、filter针对小的segment获取到的结果,是可以不缓存的,segment记录数小于1000,或者segment大小小于index总大小的3%。因为此时segment数据量很小,哪怕是扫描也是很快的;segment会在后台自动合并,小segment很快会跟其它小segment合并成大segment,此时缓存就没有什么意思了,segment很快会消失。

filter比query好的原因除了不计算相关度分数以外还有这个caching bitset。所以filter性能会很高。

(5)filter大部分的情况下,是在query之前执行的,可以尽可能过滤掉多的数据

query: 会计算每个doc的相关度分数,还会根据这个相关度分数去做排序
filter: 只是简单过滤出想要的数据,不计算相关度分数,也不排序

(6)如果document有新增和修改,那么caching bitset会被自动更新

这个过程是ES内部做的,比如之前的bitset是[0,0,0,1]。那么现在插入一条数据或是更新了一条数据doc5,而且doc5也在缓存的bitset[0,0,0,1]的filter查询条件中,那么ES会自动更新这个bitset,变为[0,0,0,1,1]

(7)以后只要有相同的filter条件的查询请求打过来,就会直接使用这个过滤条件对应的bitset

这样查询性能就会很高,一些热的filter查询,就会被cache住。


文章参考:
整体逻辑点这里
整体流程图参考点这里
倒排索引架构点这里
概念清晰解读点这里
压缩算法解读点点这里
压缩算法细节点这里
压缩算法FOR和RB怎么选择点这里

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5717701.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存