Elasticsearch分布式存储原理

Es分布式原理

ES分布式集群特点

  • 一个集群拥有相同的cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。
  • 主节点负责管理集群的变更例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作。
  • 屏蔽了分布式系统的复杂性
  • 集群内的扩容能力
    • 垂直扩容和水平扩容
    • 真正的扩容能力是来自于水平扩容–为集群添加更多的节点,并且将负载压力和稳定性分散到这些节点中
  • Coordinating Node 节点:协调节点主要负责协调客户端的请求,将接收到的请求分发给合适的节点,并把结果汇集到一起。比如客户端请求查询某个索引的数据,协调节点将会把请求分发给保存相关的数据的 DataNode 节点,找到相应的分片,并把查询到的结果都汇集返回。并且每个节点都默认起到了 Coordinating Node 的职责。

ES 集群架构

 image-20230106164908030

 ES的架构总体如上图所示,从下到上分为网关、搜索引擎、四大组件、自动发现、通信和Restful API

  1、Gateway网关

    其作用是用来对数据进行持久化以及ES重启后重新恢复数据。

    es支持多种类型的gateway,有本地文件系统、分布式文件系统、Hadoop的HDFS等。

    其存储的信息包括索引信息、集群信息、mapping等

  2、districted lucene directory搜索引擎

    Gateway上层就是Lucene的分布式检索框架。

    ES是分布式的搜索引擎,虽然底层用的是Lucene,但是需要在每个节点上都运行Lucene进行相应的索引、查询、更新等操作,所以需要做成一个分布式的运行框架来满足业务需要。

  3、四大组件模块

    districted lucene directory之上就是ES的四大模块。

    Index Model:索引模块,对数据建立索引(通常是建立倒排索引)

    Seacher Model:搜索模块,就是对数据进行查询搜索

    Mapping Model:是数据映射与解析模块,数据的每个字段可以根据建立的表结构通过mapping进行映射解析;如果没有建立表结构,那么ES会根据数据类型来推测数据结构,并自动生成一个mapping,然后根据mapping进行解析

    River Model:在es2.0之后被取消了,表示可以使用插件处理。例如可以通过一些自定义脚本将传统数据库的数据实时同步到es中。

  4、自动发现Discovery Script

    es集群中各个节点通过discovery相互发现的,默认使用的是Zen discovery(底层实现算法依赖Bully)。es是一个基于p2p的系统,他先通过广播寻找存在的节点,再通过多播协议来进行节点之间的通信,同时也支持点对点的交互。

    es还可以支持多种script脚本语言,例如mvel、js、python等。

  5、通信(Transport)

    代表es内部节点或集群与客户的交互方式,默认内部使用tcp协议进行交互,同时其还支持http协议,thrift、servlet、memcached、zeroMQ等通信协议。

    节点间通信端口默认9300-9400

  6、Restful接口

    最上层就是ES暴漏给我们的访问接口。 

Es分布式数据存储形式

image-20220223142346702

Elasticsearch 的数据是以shard的形式存储在每个es节点的。

shard是Elasticsearch数据存储的最小单位,index的存储容量为所有shard的存储容量之和。Elasticsearch集群的存储容量则为所有index存储容量之和。

一个shard就对应了一个lucene的library。对于一个shard,Elasticsearch增加了translog的功能,类似于HBase WAL,是数据写入过程中的中间数据,其余的数据都在lucene库中管理的。

ES分片Shard的特点

  • Elasticsearch 是利用分片将数据分发到集群内各处
  • 分片是数据的容器,文档保存在分片内
  • 分片又被分配到集群内的各个节点里
  • 当集群规模变动,ES会自动在各个节点中迁移分片。使得数据任然均匀分布在集群中
  • 副分片是主分片的一个拷贝,作为硬件故障时的备份。并提供返回文档读操作
  • 在创建索引时,确定主分片数,但是副分片可以在后面进行更改

ES分片的健康

GET /_cluster/health

返回值中的status 是我们关注的

  • green 主副分片均正常
  • yellow 主都正常,不是所有的副都正常
  • red 所有主分片都不正常

如何路由一个文档到一个分片中

  • 当索引一个文档时,我们怎么知道这个文档在什么位置
  • 使用下面的这个路由选择公式
1
shard = hash(routing) % number_of_primary_shards
  • shard: 哪个分片, 也就是分片id
  • routing: 一个可变值,默认是文档的id
  • hash: 一个哈希函数,对rounting字段进行哈希计算生成一个数字
  • number_of_primary_shards: 主分片的数量,routing字段经过hash函数计算之后的值,将对 主分片的数量也就是 number_of_primary_shards 进行取与,之后得到的shard就是我们文档所在的分片的位置

分布式节点间的数据交互

主分片和副分片如何交互

  • 假设一个集群由三个节点组成。有一个索引,这个索引有两个主分片,每个主分片有两个副分片,相同的分片的副本不会放在同一个节点。
  • 请求发送到集群任意节点,每个节点都有能力处理请求。每个节点都知道集群中任一文档的位置,所以可以将请求转发到需求节点上。

各节点间如何交互

插入、删除单个文档的流程

客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node(协调节点)。

实际的 node 上的 primary shard (主分片) 处理请求,然后将数据同步到 replica node(备份分片)。

  • image-20220223114537351
    1. 先向 node 1 来一个请求这个请求可能是发送新建,索引或者删除文档等。
    2. node 1 节点根据文档的_id 确定文档属于分片0, 请求被转发到node3 节点。
    3. node 3 在主分片执行了请求,如果主分片执行成功了,它将请求转发给node1 和node 2 节点。当所有的副分片都执行成功,node 3 向协调节点报告成功,并向客户端报告完成。
    • consistency 参数的值可以设为 one (只要主分片状态 ok 就允许执行写操作),all(必须要主分片和所有副本分片的状态没问题才允许执行操作), 或quorum 。默认值为 quorum , 即大多数的分片副本状态没问题就允许执行写操作

    • 在执行一个写操作时,主分片都需要必须有一个规定数量的(quorum),也就是在大多数主副分片处于活跃状态。这样是防止在网络分区故障是执行写操作会导致数据不一致

      quorum = int( (primary + number_of_replicas) / 2 ) + 1

取回一个文档

查询,GET某一条数据,写入了某个document,这个document会自动给你分配一个全局唯一的id,doc id,同时也是根据doc id进行hash路由到对应的primary shard上面去。也可以手动指定doc id,比如用订单id,用户id。

你可以通过doc id来查询,会根据doc id进行hash,判断出来当时把doc id分配到了哪个shard上面去,从那个shard去查询

  • image-20220223114418540
  1. 可以从主分片或者任意副本分片检索文档

  2. 节点使用节点文档_ID来确定文档属于分片0, 分片0 的副本分片存在于所有的三个节点上,在这种情况下,他将请求转发到node 2

  3. node 2 将文档返回给node 1 ,然后将文档返回给客户端

    在每次处理读取请求时,协调结点在每次请求的时候都会轮训所有的副本片来达到负载均衡

局部更新文档
  • image-20220223114444850
  1. 客户端向node1 发送一个请求

  2. 它将请求转发到主分片这个文档所在的Node 3

  3. node 3从主分片检索文档,修改_Source json ,并且尝试重新索引主分片的文档。如果文档被另一个进程修改,他会重复步骤3 直到超过retry_on_conflict 次后放弃

  4. node 3 成功更新文档,它将新版本的文档并行转发到node 1 和node 2 的副本分片,重新建立索引。所有副本分片都返回成功,node 3 向协调节点也返回成功,协调节点向客户端返回成功

  5. update 操作也支持 新建索引的时的那些参数 routing 、 replication 、 consistency 和 timeout

多文档模式
  • mget 和 bulk API 的 模式类似于单文档模式。 协调节点知道每个文档的位置,将多个文档分解成每个文档的的多文档请求,并且将这些请求并行的转发到每个参与节点中 。
  • 使用 mget 取回多个文档

image-20220223114458465

  1. 客户端向node 1 发送一个mget请求
  2. node 1 向每个分片构建多文档请求,并行的转发这些请求到托管在每个所需的主分片或者副分片的节点上一旦收到所有的回复,node 1 构建响应并将其返回给客户端
使用 bulk 修改多个文档
image-20220223114521834
  1. 一个bulk请求请求到node 1
  2. node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点
  3. 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。 一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端`
全文检索

对于全文检索而言,文档可能分散在各个节点上

可以分为搜索和取回两个阶段

image-20220223140043517

查询阶段:

  1. 客户端发送一个search(搜索)请求给Node3,Node3创建一个长度为from+size的空优先级队列;

  2. Node3转发这个搜索请求到索引中每个副本的主分片或副本分片,每个分片执行本地查询并将结果存储到一个本地的from+size长度的优先队列里;

  3. 每个分片返回document的ID和优先队列里所有document的排序值给到协调节点Node3。Node3把这些值合并到自己的优先队列里产生全局排序结果;

    image-20220223140524303 *

取回阶段:

  1. 协调节点先辨别哪些document需要取回,并且向相关分片发出get请求;
  2. 每个分片加载document并且丰富他们,然后返回给协调节点;
  3. 一旦所有的document都被取回,协调节点会将结果返回给客户端;

Es 数据存储原理

LUCENE 数据的写入与更新

LUCENE 数据的写入

数据写入流程
image-20220223141104012
  • 在 Elasticsearch 中, 每个字段的所有数据 都是 默认被索引的 。 即每个字段都有为了快速检索设置的专用倒排索引
  • 在 Elasticsearch 中,术语 文档 有着特定的含义。它是指最顶层或者根对象, 这个根对象被序列化成 JSON 并存储到 Elasticsearch 中,指定了唯一 ID。

1:写入请求,分发节点。
2:数据写入同时写入内存和translog各一份,tanslog为保证数据不丢失,每 5 秒,或每次请求操作结束前,会强制刷新 translog 日志到磁盘上
3:确定数据给那个分片,refresh 刷新内存中数据到分片的segment,默认1秒刷新一次,为了提高吞吐量可以增大60s。参数refresh_interval
4:segment刷新到磁盘中完成持久化,保存成功清除translog,新版本es的 translog 不会在 segment 落盘就删,而是会保留,默认是512MB,保留12小时。每个分片。所以分片多的话 ,要考虑 translog 会带来的额外存储开销
5:segment过多会进行合并merge为大的segment,消耗大量的磁盘io和网络io

索引写入原理
索引基础结构-segment段

倒排索引被写入磁盘后是不可变的,ES解决不变性和更新索引的方式是使用多个索引,利用新增的索引来反映修改,在查询时从旧的到新的依次查询,最后来一个结果合并。

ES底层是基于Lucene,最核心的概念就是Segment(段),每个段本身就是一个倒排索引。

ES中的Index由多个段的集合和commit point(提交点)文件组成。

提交点文件中有一个列表存放着所有已知的段,下面是一个带有1个提交点和3个段的Index示意图:

image-20220223145052221
索引写入步骤

1、写入缓存

Doc会先被搜集到内存中的Buffer内,这个时候还无法被搜索到,如下图所示:

image-20220223151110135

2、缓存flush

image-20220223151209705

每隔一段时间,会将buffer提交,在flush磁盘后打开新段使得搜索可见,详细过程如下:

  1. 创建一个新段,作为一个追加的倒排索引,写入到磁盘(文件系统缓存)
  2. 将新的包含新段的Commit Point(提交点)写入磁盘(文件系统缓存)
  3. 磁盘进行fsync,主要是将文件系统缓存中等待的写入操作全部物理写入到磁盘
  4. 这个新的段被开启, 使得段内文档对搜索可见
  5. 将内存中buffer清除,又可以把新的Doc写入buffer了

通过这种方式,可以使得新文档从被索引到可被搜索间的时间间隔在数分钟,但是还不够快。因为磁盘需要fsync,这个就成为性能瓶颈。我们前面提到过Doc会先被从buffer刷入段写入文件系统缓存(很快),那么就自然想到在这个阶段就让文档对搜索可见,随后再被刷入磁盘(较慢)。

2.1、Refresh

Lucene支持对新段写入和打开,可以使文档在没有完全刷入硬盘的状态下就能对搜索可见,而且是一个开销较小的操作,可以频繁进行。

下面是一个已经将Docs刷入段,但还没有完全提交的示意图:

image-20220223152031494

我们可以看到,新段虽然还没有被完全提交,但是已经对搜索可见了。

引入refresh操作的目的是提高ES的实时性,使添加文档尽可能快的被搜索到,同时又避免频繁fsync带来性能开销,依靠的就是文件系统缓存OS cache里缓存的文件可以被打开(open/reopen)和读取,而这个os cache实际是一块内存区域,而非磁盘,所以操作是很快的,这就是ES被称为近实时搜索的原因。

refresh默认执行的间隔是1秒,可以使用refreshAPI进行手动操作,但一般不建议这么做。还可以通过合理设置refresh_interval在近实时搜索和索引速度间做权衡。

3、translog

index segment刷入到os cache后就可以打开供查询,这个操作是有潜在风险的,因为os cache中的数据有可能在意外的故障中丢失,而此时数据必备并未刷入到os disk,此时数据丢失将是不可逆的,这个时候就需要一种机制,可以将对es的操作记录下来,来确保当出现故障的时候,已经落地到磁盘的数据不会丢失,并在重启的时候可以从操作记录中将数据恢复过来。elasticsearch提供了translog来记录这些操作,结合os cached segments数据定时落盘来实现数据可靠性保证(flush)。

文档被添加到buffer同时追加到translog:

image-20220223145441661

进行refresh操作,清空buffer,文档可被搜索但尚未flush到磁盘。translog不会清空:

image-20220223145500494

每隔一段时间(例如translog变得太大),index会被flush到磁盘,新的translog文件被创建,commit执行结束后,会发生以下事件:

  • 所有内存中的buffer会被写入新段
  • buffer被清空
  • 一个提交点被写入磁盘
  • 文件系统缓存通过fsync flush
  • 之前的旧translog被删除

下面示意图展示了这个状态:

image-20220223145516894

translog记录的是已经在内存生成(segments)并存储到os cache但是还没写到磁盘的那些索引操作(注意,有一种解释说,添加到buffer中但是没有被存入segment中的数据没有被记录到translog中,这依赖于写translog的时机,不同版本可能有变化,不影响理解),此时这些新写入的数据可以被搜索到,但是当节点挂掉后这些未来得及落入磁盘的数据就会丢失,可以通过trangslog恢复。

当然translog本身也是磁盘文件,频繁的写入磁盘会带来巨大的IO开销,因此对translog的追加写入操作的同样操作的是os cache,因此也需要定时落盘(fsync)。translog落盘的时间间隔直接决定了ES的可靠性,因为宕机可能导致这个时间间隔内所有的ES操作既没有生成segment磁盘文件,又没有记录到Translog磁盘文件中,导致这期间的所有操作都丢失且无法恢复。

translog的fsync是ES在后台自动执行的,默认是每5秒钟主动进行一次translog fsync,或者当translog文件大小大于512MB主动进行一次fsync,对应的配置是index.translog.flush_threshold_periodindex.translog.flush_threshold_size

当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。

translog 也被用来提供实时 CRUD 。当你试着通过ID来RUD一个Doc,它会在从相关的段检索之前先检查 translog 中最新的变更。

默认translog是每5秒或是每次请求完成后被fsync到磁盘(在主分片和副本分片都会)。也就是说,如果你发起一个index, delete, update, bulk请求写入translog并被fsync到主分片和副本分片的磁盘前不会反回200状态。

这样会带来一些性能损失,可以通过设为异步fsync,但是必须接受由此带来的丢失少量数据的风险:

1
2
3
4
5
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}

flush就是执行commit清空、干掉老translog的过程。默认每个分片30分钟或者是translog过于大的时候自动flush一次。可以通过flush API手动触发,但是只会在重启节点或关闭某个索引的时候这样做,因为这可以让未来ES恢复的速度更快(translog文件更小)。

满足下列条件之一就会触发冲刷操作:

  • 内存缓冲区已满,相关参数: indices.memory.index_buffer
  • 自上次冲刷后超过了一定的时间,相关参数:index.translog.flush_threshold_period
  • 事物日志达到了一定的阔值,相关参数:index.translog.flush_threshold_size
写入流程总结
image-20220223153823350

整体流程:

  1. 数据写入buffer缓冲和translog日志文件中。
    当你写一条数据document的时候,一方面写入到mem buffer缓冲中,一方面同时写入到translog日志文件中。
  2. buffer满了或者每隔1秒(可配),refresh将mem buffer中的数据生成index segment文件并写入os cache,此时index segment可被打开以供search查询读取,这样文档就可以被搜索到了(注意,此时文档还没有写到磁盘上);然后清空mem buffer供后续使用。可见,refresh实现的是文档从内存移到文件系统缓存的过程。
  3. 重复上两个步骤,新的segment不断添加到os cache,mem buffer不断被清空,而translog的数据不断增加,随着时间的推移,translog文件会越来越大。
  4. 当translog长度达到一定程度的时候,会触发flush操作,否则默认每隔30分钟也会定时flush,其主要过程:
    4.1. 执行refresh操作将mem buffer中的数据写入到新的segment并写入os cache,然后打开本segment以供search使用,最后再次清空mem buffer。
    4.2. 一个commit point被写入磁盘,这个commit point中标明所有的index segment。
    4.3. filesystem cache(os cache)中缓存的所有的index segment文件被fsync强制刷到磁盘os disk,当index segment被fsync强制刷到磁盘上以后,就会被打开,供查询使用。
    4.4. translog被清空和删除,创建一个新的translog。
Segment段合并

通过每秒自动刷新创建新的段,用不了多久段的数量就爆炸了,每个段消费大量文件句柄,内存,cpu资源。更重要的是,每次搜索请求都需要依次检查每个段。段越多,查询越慢。

ES通过后台合并段解决这个问题。ES利用段合并的时机来真正从文件系统删除那些version较老或者是被标记为删除的文档。被删除的文档(或者是version较老的)不会再被合并到新的更大的段中。

可见,段合并主要有两个目的:

  • 第一个是将分段的总数量保持在受控的范围内(这用来保障查询的性能)。
  • 第二个是真正地删除文挡。

ES对一个不断有数据写入的索引处理流程如下:

  1. 索引过程中,refresh会不断创建新的段,并打开它们。
  2. 合并过程会在后台选择一些小的段合并成大的段,这个过程不会中断索引和搜索。

合并过程如图:

image-20220223161438056

从上图可以看到,段合并之前,旧有的Commit和没Commit的小段皆可被搜索。

段合并后的操作:

  • 新的段flush到硬盘
  • 编写一个包含新段的新提交点,并排除旧的较小段。
  • 新的段打开供搜索
  • 旧的段被删除

合并完成后新的段可被搜索,旧的段被删除,如下图所示:

image-20220223161501064

注意:段合并过程虽然看起来很爽,但是大段的合并可能会占用大量的IO和CPU,如果不加以控制,可能会大大降低搜索性能。段合并的optimize API 不是非常特殊的情况下千万不要使用,默认策略已经足够好了。不恰当的使用可能会将你机器的资源全部耗尽在段合并上,导致无法搜索、无法响应。

LUCENE 数据删除

删除一个ES文档不会立即从磁盘上移除,它只是被标记成已删除。因为段是不可变的,所以文档既不能从旧的段中移除,旧的段也不能更新以反映文档最新的版本。

ES的做法是,每一个提交点包括一个.del /.liv文件(还包括新段),包含了段上已经被标记为删除状态的文档。所以,当一个文档被做删除操作,实际上只是在.del / .liv文件中将该文档标记为删除,依然会在查询时被匹配到,只不过在最终返回结果之前会被从结果中删除。ES将会在用户之后添加更多索引的时候,在后台进行要删除内容的清理。

LUCENE 数据更新

文档的更新操作和删除是类似的:当一个文档被更新,旧版本的文档被标记为删除,新版本的文档在新的段中索引。
该文档的不同版本都会匹配一个查询,但是较旧的版本会从结果中删除。

*ES如何处理冲突 ( 比如并发修改同一docId ) *

  • 使用乐观并发控制
  • 利用 _version 号来确保 应用中相互冲突的变更不会导致数据丢失
  • 通过外部系统使用版本控制

详细介绍: https://blog.csdn.net/R_P_J/article/details/78388783

LUCENE 数据存储

LUCENE 内文件构成

elasticsearch底层是lucene存储架构
elasticsearch的数据保存在lucene文件中,es的elasticsearch.yml配置文件中配置数据保存路径。
lucene包的文件是由很多segment文件组成的,segments_xxx文件记录了lucene包下面的segment文件数量。每个segment会主要包含如下的文件。

Name Extension Brief Description
Segment Info .si segment的元数据文件
Compound File .cfs, .cfe 一个segment包含了如下表的各个文件,为减少打开文件的数量,在segment小的时候,segment的所有文件内容都保存在cfs文件中,cfe文件保存了lucene各文件在cfs文件的位置信息
Fields .fnm 保存了fields的相关信息
Field Index .fdx 正排存储文件的元数据信息
Field Data .fdt 存储了正排存储数据,写入的原文存储在这
Term Dictionary .tim 倒排索引的元数据信息
Term Index .tip 倒排索引文件,存储了所有的倒排索引数据
Frequencies .doc 保存了每个term的doc id列表和term在doc中的词频
Positions .pos Stores position information about where a term occurs in the index 全文索引的字段,会有该文件,保存了term在doc中的位置
Payloads .pay Stores additional per-position metadata information such as character offsets and user payloads 全文索引的字段,使用了一些像payloads的高级特性会有该文件,保存了term在doc中的一些高级特性
Norms .nvd, .nvm 文件保存索引字段加权数据
Per-Document Values .dvd, .dvm lucene的docvalues文件,即数据的列式存储,用作聚合和排序
Term Vector Data .tvx, .tvd, .tvf Stores offset into the document data file 保存索引字段的矢量信息,用在对term进行高亮,计算文本相关性中使用
Live Documents .liv / .del 记录了segment中删除的doc

LUCENE数据元信息文件

文件名为:segments_xxx

该文件为lucene数据文件的元信息文件,记录所有segment的元数据信息。

该文件主要记录了目前有多少segment,每个segment有一些基本信息,更新这些信息定位到每个segment的元信息文件。

lucene元信息文件还支持记录userData,Elasticsearch可以在此记录translog的一些相关信息。

segment的元信息文件

文件后缀:.si

每个segment都有一个.si文件,记录了该segment的元信息。

segment元信息文件中记录了segment的文档数量,segment对应的文件列表等信息。

fields信息文件

文件后缀:.fnm

该文件存储了fields的基本信息。

fields信息中包括field的数量,field的类型,以及IndexOpetions,包括是否存储、是否索引,是否分词,是否需要列存等等。

数据存储文件

文件后缀:.fdx, .fdt

索引文件为.fdx,数据文件为.fdt,数据存储文件功能为根据自动的文档id,得到文档的内容,搜索引擎的术语习惯称之为正排数据,即doc_id -> content,es的_source数据就存在这

索引文件记录了快速定位文档数据的索引信息,数据文件记录了所有文档id的具体内容。

正排索引表是以文档的ID为关键字,表中记录文档中每个字段的值信息,主要场景是通过查询id来把整条文档拿出来,一般mysql关系型数据库是这种方式来查询的

正排表结构如下图所示

image-20220223165904782

这种组织方法在建立索引的时候结构比较简单,建立比较方便且易于维护,当对ID查询的时候检索效率会很高。

倒排索引文件

索引后缀:.tip,.tim

倒排索引也包含索引文件和数据文件,.tip为索引文件,.tim为数据文件,索引文件包含了每个字段的索引元信息,数据文件有具体的索引内容。

5.5.0版本的倒排索引实现为FST tree,FST tree的最大优势就是内存空间占用非常低

倒排索引表以字或词为关键字进行索引,表中关键字所对应的记录项记录了出现这个字或词的所有文档,每个字段记录该文档的ID和关键字在该文档中出现的位置情况。

image-20220223165935798

由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂,但是一旦完成创建,在查询的时候由于可以一次得到查询关键字所对应的所有文档

列存文件(docvalues)

文件后缀:.dvm, .dvd

索引文件为.dvm,数据文件为.dvd。

上面的倒排索引满足了关键字搜索的效率,但是对于从另外一个方向的相反操作并不高效,比如聚合(aggregations)、排序(Sorting)和字段的全值查询等时候需要其它的访问模式。

我们首先想到的是遍历正向索引来进行统计。但是这很慢而且难以扩展,随着词项和文档的数量增加,执行时间也会增加。

为了能够解决上述问题,我们使用了Doc values通过转置两者间的关系来解决这个问题。

doc_values表如下:

image-20220223172931852

DocValues是在索引时与倒排索引同时生成的,并且是不可变的。与倒排一样,保存在lucene文件中(序列化到磁盘),此值默认是启动状态,如果没有必要使用可以设置 doc_values: false来禁用。

Doc values 是不支持 analyzed 字符串字段的

lucene实现的docvalues有如下类型:

  • 1、NONE 不开启docvalue时的状态
  • 2、NUMERIC 单个数值类型的docvalue主要包括(int,long,float,double)
  • 3、BINARY 二进制类型值对应不同的codes最大值可能超过32766字节,
  • 4、SORTED 有序增量字节存储,仅仅存储不同部分的值和偏移量指针,值必须小于等于32766字节
  • 5、SORTED_NUMERIC 存储数值类型的有序数组列表
  • 6、SORTED_SET 可以存储多值域的docvalue值,但返回时,仅仅只能返回多值域的第一个docvalue
  • 7、对应not_anaylized的string字段,使用的是SORTED_SET类型,number的类型是SORTED_NUMERIC类型

其中SORTED_SET 的 SORTED_SINGLE_VALUED类型包括了两类数据 : binary + numeric, binary是按ord排序的term的列表,numeric是doc到ord的映射。

Fielddata (内存)

在ES中,text类型的字段使用一种叫做fielddata的查询时内存数据结构。当字段被排序,聚合或者通过脚本访问时这种数据结构会被创建。它是通过从磁盘读取每个段的整个反向索引来构建的,然后存存储在java的堆内存中。

fileddata默认是不开启的。Fielddata可能会消耗大量的堆空间,尤其是在加载高基数文本字段时。一旦fielddata已加载到堆中,它将在该段的生命周期内保留。此外,加载fielddata是一个昂贵的过程,可能会导致用户遇到延迟命中。

(非磁盘Field Data 文件,该结构为Fied Data 映射的内存结构)

Doc values 是不支持 analyzed 字符串字段的,然而,这些字段仍然可以使用聚合和排序

当ES进行排序、统计时,ES把涉及到的字段数据全部读取到内存(JVM Heap)中进行操作。与 doc values 不同,fielddata 构建和管理 100% 在内存中,常驻于 JVM 内存堆。

Fielddata默认是不启用的,因为text字段比较长,一般只做关键字分词和搜索,很少拿它来进行全文匹配和聚合还有排序,因为大多数这种情况是无意义的,一旦启用将会把text都加载到内存中,那将带来很大的内存压力。

Fielddata一些特性:

  1. Fielddata 是延迟加载的。如果你从来没有聚合一个分析字符串,就不会加载 fielddata 到内存中,是在查询时候构建的。
  2. fielddata 是基于字段加载的, 只有很活跃地使用字段才会增加fielddata 的负担。
  3. fielddata 会加载索引中(针对该特定字段的) 所有的文档,而不管查询是否命中。逻辑是这样:如果查询会访问文档 X、Y 和 Z,那很有可能会在下一个查询中访问其他文档。
  4. 如果空间不足,使用最久未使用(LRU)算法移除fielddata。

所以,fielddata应该在JVM中合理利用,否则会影响es性能。

刘小恺(Kyle) wechat
如有疑问可联系博主