ES工作原理及底层原理

基本概念

  • Cluster:包含多个Node的集群。
  • Node:集群服务单元。
  • Index:一个ES索引包含一个或多个物理分片,它只是这些分片的逻辑命名空间。
  • Type:一个index的不同分类,6.x后只能配置一个type,以后将移除。
  • Document:最基础的可被索引的数据单元,如一个JSON串。
  • Shards:一个分片是一个底层的工作单元,它仅保存全部数据中的一部分,它是一个Lucence实例 (一个lucene索引最大包含2,147,483,519 (= Integer.MAX_VALUE-128)个文档数量)。
  • Replicas:分片备份,用于保障数据安全与分担检索压力。

ES基础结构

ES依赖一个重要的组件Lucene,关于数据结构的优化通常来说是对Lucene的优化,它是集群的一个存储于检索工作单元,结构如下图:

Lucene结构

在Lucene中,分为索引(录入)与检索(查询)两部分,索引部分包含 分词器过滤器字符映射器等,检索部分包含 查询解析器 等。

一个Lucene索引包含多个segments,一个segment包含多个文档,每个文档包含多个字段,每个字段经过分词后形成一个或多个term。

通过Luke工具查看ES的lucene文件如下,主要增加了_id和_source字段:
Lucene文件结构

Lucene索引实现

Lucene 索引文件结构主要的分为:词典倒排表正向文件DocValues等,如下图:

Lucene索引文件

Lucene索引文件

Lucene 随机三次磁盘读取比较耗时。其中.fdt文件保存数据值损耗空间大,.tim.doc则需要SSD存储提高随机读写性能。

另外一个比较消耗性能的是打分流程,不需要则可屏蔽。

ES索引与检索分片

ES中一个索引由一个或多个lucene索引构成,一个lucene索引由一个或多个segment构成,其中segment是最小的检索域。

数据具体被存储到哪个分片上:

shard = hash(routing) % number_of_primary_shards

默认情况下 routing参数是文档ID (murmurhash3),可通过 URL中的 _routing 参数指定数据分布在同一个分片中,index和search的时候都需要一致才能找到数据。

如果能明确根据_routing进行数据分区,则可减少分片的检索工作,以提高性能。

ES工作原理

ES写数据工作原理

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

  2. coordinating node 对 document 进行路由,将请求转发给对应的 node(所有的primary shard)。

  3. 实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node。

  4. coordinating node 如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端。

![ES写数据工作原理][link1]

ES读数据工作原理

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

  1. 客户端发送请求到任意一个 node,成为 coordinate node。

  2. coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。

  3. 接收请求的 node 返回 document 给 coordinate node。

  4. coordinate node 返回 document 给客户端。

ES搜索数据工作原理

  1. 客户端发送请求到一个 coordinate node。

  2. 协调节点将搜索请求转发到所有的 shard 对应的 primary shard 或 replica shard,都可以。

  3. query phase:每个 shard 将自己的搜索结果(其实就是一些 doc id)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。

  4. fetch phase:接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端。

  • 写请求是写入 primary shard,然后同步给所有的 replica shard;

  • 读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。


ES底层原理

ES写数据底层原理

先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。

如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 refresh 到一个新的 segment file 中

但是此时数据不是直接进入 segment file 磁盘文件,而是先进入 os cache 。这个过程就是 refresh。

每隔 1 秒钟,es 将 buffer 中的数据写入一个新的 segment file,每秒钟会产生一个新的磁盘文件 segment file

这个 segment file 中就存储最近 1 秒内 buffer 中写入的数据。

但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作

如果 buffer 里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。

操作系统里面,磁盘文件其实都有一个东西,叫做 os cache,即操作系统缓存

就是说数据写入磁盘文件之前,会先进入 os cache,先进入操作系统级别的一个内存缓存中去。只要 buffer中的数据被 refresh 操作刷入 os cache中,这个数据就可以被搜索到了。

为什么叫 es 是准实时的?

NRT,全称 near real-time。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的。

可以通过 es 的 restful api 或者 java api,手动执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 os cache中,让数据立马就可以被搜索到。

只要数据被输入 os cache 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。

重复上面的步骤,新的数据不断进入 buffer 和 translog,不断将 buffer 数据写入一个又一个新的 segment file 中去,每次 refresh 完 buffer 清空,translog 保留。

随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 commit 操作。

commit 操作发生第一步,就是将 buffer 中现有数据 refresh 到 os cache 中去,清空 buffer。

然后,将一个 commit point写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file,同时强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去。

最后清空 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。

这个 commit 操作叫做 flush。默认 30 分钟自动执行一次 flush,但如果 translog 过大,也会触发 flush。

flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。

translog 日志文件的作用是什么?

执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中

无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 translog 中

一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。

translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去

所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,如果此时机器挂了,会丢失 5 秒钟的数据。

但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。

其实 es 第一是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的。有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盘上,此时如果宕机,会导致 5 秒的数据丢失。

总结一下,数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。

每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。

ES删除/更新数据底层原理

如果是删除操作,commit 的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 是否被删除了。

如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据。

buffer 每 refresh 一次,就会产生一个 segment file,所以默认情况下是 1 秒钟一个 segment file,这样下来 segment file 会越来越多

此时会定期执行 merge。每次 merge 的时候,会将多个 segment file 合并成一个

同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘

这里会写一个 commit point,标识所有新的 segment file,然后打开 segment file 供搜索使用,同时删除旧的 segment file。

ES性能优化

优化索引性能

  1. 批量写入,看每条数据量的大小,一般都是几百到几千。

  2. 多线程写入,写入线程数一般和机器数相当,可以配多种情况,在测试环境通过Kibana观察性能曲线。

  3. 增加segments的刷新时间,通过上面的原理知道,segment作为一个最小的检索单元,比如segment有50个,目的需要查10条数据,但需要从50个segment分别查询10条,共500条记录,再进行排序或者分数比较后,截取最前面的10条,丢弃490条。

  4. 内存分配方面,很多文章已经提到,给系统50%的内存给Lucene做文件缓存,它任务很繁重,所以ES节点的内存需要比较多(比如每个节点能配置64G以上最好)。

  5. 磁盘方面配置SSD,机械盘做阵列RAID5 RAID10虽然看上去很快,但是随机IO还是SSD好。

  6. 使用自动生成的ID,在我们的案例中使用自定义的KEY,也就是与HBase的ROW KEY,是为了能根据rowkey删除和更新数据,性能下降不是很明显。

  7. 关于段合并,合并在后台定期执行,比较大的segment需要很长时间才能完成,为了减少对其他操作的影响(如检索),elasticsearch进行阈值限制,默认是20MB/s,

    可配置的参数:"indices.store.throttle.max_bytes_per_sec" : "200mb" (根据磁盘性能调整)

    合并线程数默认是:Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2))

    如果是机械磁盘,可以考虑设置为1:index.merge.scheduler.max_thread_count: 1

优化检索性能

  1. 关闭不需要字段的doc values。

  2. 尽量使用keyword替代一些long或者int之类,term查询总比range查询好 (参考 lucene说明 )。

  3. 关闭不需要查询字段的_source功能,不将此存储仅ES中,以节省磁盘空间。

  4. 评分消耗资源,如果不需要可使用filter过滤来达到关闭评分功能,score则为0,如果使用constantScoreQuery则score为1。

  5. 关于分页:

    • from + size: 每分片检索结果数最大为 from + size
      假设from = 20, size = 20,则每个分片需要获取20 * 20 = 400条数据,多个分片的结果在协调节点合并(假设请求的分配数为5,则结果数最大为 400*5 = 2000条) 再在内存中排序后然后20条给用户。
      这种机制导致越往后分页获取的代价越高,达到50000条将面临沉重的代价,默认from + size默认如下:
      index.max_result_window : 10000
    • search_after: 使用前一个分页记录的最后一条来检索下一个分页记录。
      比如首先使用from+size,检索出结果后再使用search_after,在页面上限制用户只能跳5页,不能跳到最后一页。
    • scroll: 用于大结果集查询,缺陷是需要维护scroll_id
  6. 关于排序:可以增加一个long字段,它用于存储时间和ID的组合(通过移位即可),正排与倒排性能相差不明显。

  7. 关于CPU消耗,检索时如果需要做排序则需要字段对比,消耗CPU比较大,如果有可能尽量分配16cores以上的CPU,具体看业务压力。

  8. 关于合并被标记删除的记录,设置为0表示在合并的时候一定删除被标记的记录,默认应该是大于10%才删除:"merge.policy.expunge_deletes_allowed": "0"

{
    "mappings": {
        "data": {
            "dynamic": "false",
            "_source": {
                "includes": ["XXX"]  -- 仅将查询结果所需的数据存储仅_source中
            },
            "properties": {
                "state": {
                    "type": "keyword",   -- 虽然state为int值,但如果不需要做范围查询,尽量使用keyword,因为int需要比keyword增加额外的消耗。
                    "doc_values": false  -- 关闭不需要字段的doc values功能,仅对需要排序,汇聚功能的字段开启。
                },
                "b": {
                    "type": "long"    -- 使用了范围查询字段,则需要用long或者int之类 (构建类似KD-trees结构)
                }
            }
        }
    },
   "settings": {......}
}

本文标题:ES工作原理及底层原理

文章作者:王洪博

发布时间:2019年01月18日 - 11:01

最后更新:2019年12月03日 - 05:12

原始链接:http://whb1990.github.io/posts/67a89a08.html

▄︻┻═┳一如果你喜欢这篇文章,请点击下方"打赏"按钮请我喝杯 ☕
0%