Elasticsearch 原理解析(介绍)

Posted 倾听铃的声

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Elasticsearch 原理解析(介绍)相关的知识,希望对你有一定的参考价值。

介绍

Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene(TM) 基础上的搜索引擎.当然 Elasticsearch 并不仅仅是 Lucene 那么简单,它不仅包括了全文搜索功能,还可以进行以下工作:

分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索。

实时分析的分布式搜索引擎。

可以扩展到上百台服务器,处理 PB 级别的结构化或非结构化数据。

基本概念

对应关系

一个文档的数据,通常是 json 格式

    "name" :     "John",    "sex" :      "Male",    "age" :      25,    "birthDate": "1990/05/01",    "about" :    "Hello world",    "interests": [ "sports", "game" ]

复制代码

名词解析

  • 集群(cluster):由一个或多个节点组成, 并通过集群名称与其他集群进行区分

  • 节点(node):单个 ElasticSearch 实例

  • 索引(index):在 ES 中, 索引是一组文档的集合

  • 分片(shard):因为 ES 是个分布式的搜索引擎, 所以索引通常都会分解成不同部分, 而这些分布在不同节点的数据就是分片. ES 自动管理和组织分片, 并在必要的时候对分片数据进行再平衡分配, 所以用户基本上不用担心分片的处理细节,一个分片默认最大文档数量是 20 亿.

  • 副本(replica):ES 默认为一个索引创建 5 个主分片, 并分别为其创建一个副本分片. 也就是说每个索引都由 5 个主分片成本, 而每个主分片都相应的有一个 copy.

分片及副本的分配是高可用及快速搜索响应的设计核心。主分片与副本都能处理查询请求, 它们的唯一区别在于只有主分片才能处理索引请求。

架构

集群

节点架构图:

分片

每一个 shard 就是一个 Lucene Index,包含多个 segment 文件,和一个 commit point 文件。

在 es 配置好索引后,集群运行中是无法调整分片配置的。如果要调整分片数量,只能新建索引对数据进重新索引(reindex),该操作很耗时,但是不用停机。

分片时主要考虑数据集的增长趋势,不要做过度分片。

每个分片都有额外成本:

  • 每个分片本质上就是一个 Lucene 索引, 因此会消耗相应的文件句柄, 内存和 CPU 资源

  • 每个搜索请求会调度到索引的每个分片中. 如果分片分散在不同的节点倒是问题不太. 但当分片开始竞争相同的硬件资源时, 性能便会逐步下降

  • 每个搜索请求会遍历这个索引下的所有分片

  • ES 使用词频统计来计算相关性. 当然这些统计也会分配到各个分片上. 如果在大量分片上只维护了很少的数据, 则将导致最终的文档相关性较差

es 推荐的最大 JVM 堆空间时 30~32G,所以如果分片最大容量限制为 30G,假如数据量达到 200GB,那么最多分配 7 个分片就足够了。过早的优化是万恶之源,过早的分片也是。

流程

写入数据

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

  2. coordinating node,对 document 进行路由,将请求转发给对应的 node

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

  4. coordinating node,如果发现 primary node 和所有的 replica node 都搞定之后,就会返回请求到客户端

其中步骤 3 中 primary 直接落盘 IO 效率低,所以参考操作系统的异步落盘机制:

  1. ES 使用了一个内存缓冲区 Buffer,先把要写入的数据放进 buffer;同时将数据写入 translog 日志文件(其实是些 os cache)。

  2. refresh:buffer 数据满/1s 定时器到期会将 buffer 写入操作系统 segment file 中,进入 cache 立马就能搜索到,所以说 es 是近实时(NRT,near real-time

  3. flush:tanslog 超过指定大小/30min 定时器到期会触发 commit 操作将对应的 cache 刷到磁盘 file,commit point 写入磁盘,commit point 里面包含对应的所有的 segment file

  4. translog 默认 5s 把 cache fsync 到磁盘,所以 es 宕机会有最大 5s 窗口的丢失数据

读取数据

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

  2. coordinate node 对 document 进行路由,将请求 rr 轮训转发到对应的 node,在 primary shard 以及所有的 replica 中随机选择一个,让读请求负载均衡,

  3. 接受请求的 node,返回 document 给 coordinate note

  4. coordinate node 返回给客户端

搜索过程

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

  2. 协调节点将搜索的请求转发给所有的 shard 对应的 primary shard 或 replica shard

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

  4. fetch phase ,接着由协调节点,根据唯一标识去各个节点进行拉去数据,最总返回给客户端

索引

一切设计都是为了提高搜索的性能

为了提高搜索的性能,难免会牺牲某些其他方面,比如插入/更新,否则其他数据库不用混了。前面看到往 Elasticsearch 里插入一条记录,其实就是直接 PUT 一个 json 的对象,这个对象有多个 fields,比如上面例子中的 name, sex, age, about, interests,那么在插入这些数据到 Elasticsearch 的同时,Elasticsearch 还默默的为这些字段建立索引--倒排索引,因为 Elasticsearch 最核心功能是搜索。

倒排索引

假如数据如下

那么建立的索引如下

Posting List

es 分别为每个 field 都建立了一个倒排索引,Kate, John, 24, Female 这些叫 term,而[1,2]就是 Posting List。Posting list 就是一个 int 的数组,存储了所有符合某个 term 的文档 id。

数据有两种情况,一种是 term 特别多,一种是 posting list 特别多。es 对此分别进行优化,优化方式是添加索引(提升时间效率),压缩(提升空间效率)

Term dictionary

Elasticsearch 为了能快速找到某个 term,将所有的 term 排个序,二分法查找 term,logN 的查找效率,就像通过字典查找一样。

Term Index

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

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

Term Index 不需要存所有 term,只是一个前缀数,再结合 FST 压缩技术,存放到内存中。通过 TermIndex 定位到 TermDictionary 在磁盘上的 block 后,在顺序查找磁盘,降低随机读磁盘的次数

压缩技巧

Posting List 压缩

比如以性别作为倒排索引,es 数据有成千上万条,那么 Posting List 数据量也会特别大。需要进行压缩存储

5。0 版本之前 Lucene 直接使用 bitMap 的形式进行压缩。假设某个 posting list:

[1,3,4,7,10]

对应的 bitMap 是:

[1,0,1,1,0,0,1,0,0,1]

单这样压缩方式仍然不够高效,如果有 1 亿个文档,那么需要 12.5MB 的存储空间,这仅仅是对应一个索引字段(我们往往会有很多个索引字段)。于是有人想出了 Roaring bitmaps 这样更高效的数据结构。

Roaring bitmap(RBM)

Bitmap 的缺点是存储空间随着文档个数线性增长,RBM 需要打破这个魔咒就一定要用到某些指数特性:

将 posting list 按照 65535 为界限分块,比如第一块所包含的文档 id 范围在 0~65535 之间,第二块的 id 范围是 65536~131071,以此类推。再用<商,余数>的组合表示每一组 id,这样每组里的 id 范围都在 0~65535 内了

举个栗子说明就好了。现在我们要将 821697800 这个 32 bit 的整数插入 RBM 中,整个算法流程是这样的:

  1. 821697800 对应的 16 进制数为 30FA1D08, 其中高 16 位为 30FA, 低 16 位为 1D08。

  2. 我们先用二分查找从一级索引中找到数值为 30FA 的容器(如果该容器不存在,则新建一个),从图中我们可以看到,该容器是一个 Bitmap 容器。

  3. 找到了相应的容器后,看一下低 16 位的数值 1D08,它相当于是 7432,因此在 Bitmap 中找到相应的位置,将其置为 1 即可。

联合索引

  • 利用跳表(Skip list)的数据结构快速做“与”运算,或者对最短的 posting list 中的每个 id,逐个在另外两个 posting list 中查找看是否存在,最后得到交集的结果。

  • 利用上面提到的 bitset 按位“与”直接按位与,得到的结果就是最后的交集。

总结

将磁盘里的东西尽量搬进内存减少磁盘随机读取次数(同时也利用磁盘顺序读特性),结合各种奇技淫巧的压缩算法,用及其苛刻的态度使用内存。

注意事项

  • 不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的

  • 同样的道理,对于 String 类型的字段,不需要 analysis 的也需要明确定义出来,因为默认也是会 analysis 的

  • 选择有规律的 ID 很重要,随机性太大的 ID(比如 java 的 UUID)不利于查询

  • 上面看到的压缩算法,都是对 Posting list 里的大量 ID 进行压缩的,那如果 ID 是顺序的,或者是有公共前缀等具有一定规律性的 ID,压缩比会比较高;

注意事项 2

  • 拒绝大聚合

  • 拒绝模糊查询

  • 拒绝深度分野 ES 获取数据时,每次默认最多获取 10000 条,获取更多需要分页,但存在深度分页问题,一定不要使用 from/Size 方式,建议使用 scroll 或者 searchAfter 方式。scroll 会把上一次查询结果缓存一定时间(通过配置 scroll=1m 实现),所以在使用 scroll 时一定要保证 search 结果集不要太大。

  • 拒绝多层嵌套,不要超过 2 层,避免内存泄漏

  • 拒绝 top》100 的潮汛 top 查询是在聚合的基础上再进行排序,如果 top 太大,cpu 的计算量和耗费的内存都会导致查询瓶颈

实战

es 常见用法

有些小伙伴不知道本文内容和更多相关学习资料的请点赞收藏+评论转发+关注我,后面会有很多干货。我有一些面试题、架构、设计类资料可以说是程序员面试必备!所有资料都整理到网盘了,需要的话欢迎下载!私信我回复【000】即可免费获取 

 

《Elasticsearch 源码解析与优化实战》第17章:Shrink原理分析

一、简介

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-shrink-index.html

索引分片数量一般在模板中统一定义,在数据规模比较大的索引中,索引分片数一般也大一些,在笔者的集群中设置为24。同时按天生成新的索引,使用别名关联。但是,并非每天的索引数据量都很大,小数据量的索引同样有较大的分片数。在ES中,主节点管理分片是很大的工作量,降低集群整体分片数量可以减少recovery时间,减小集群状态的大小。因此,可以使用Shrink API缩小索引分片数。当索引缩小完成后,源索引可以删除。

**Shrink API****ES 5.0****之后提供的新功能,其可以缩小主分片数量。但其并不对源索引直接进行缩小操作,而是使用与源索引相同的配置创建一个新索引,仅降低分片数。由于添加新文档时使用对分片数量取余获取目的分片的关系,新索引的主分片数必须是源索引主分片数的因数。**例如,8个分片可以缩小到4、2、1个分片。如果源索引的分片数为素数,则目标索引的分片数只能为1。

下面举一个例子来分析缩小过程。

1.1、准备源索引

创建索引: my_source_index, 包括5个主分片和1个副分片,并写入几条测试数据通过下面的命令,将索引标记为只读,且所有分片副本都迁移到名为 node-idea 的节点上。

注意,“所有分片副本”不指索引的全部分片,无论主分片还是副分片,任意一个就可以。分配器也不允许将主副分片分配到同-节点。

PUT my_source_index
{
    "settings": {
        "index.number_of_replicas": 0, # 移除源索引的副本
        "index.routing.allocation.require._name": "node-idea", # 将源索引的分片迁移到同一个节点上
        "index.blocks.write": true  # 设置索引为只读模式
    }
}

选项index.blocks.write设置为 true 来禁止对索引的写操作。但索引的 metadata 可以正常写。

It can take a while to relocate the source index. Progress can be tracked with the_cat recoveryAPI, or thecluster healthAPIcan be used to wait until all shards have relocated with thewait_for_no_relocating_shardsparameter.

1.2、缩小索引

待分片迁移完毕,我们就可以执行执行Shrink操作了:

以上代码将创建含有一个主分片和一个副分片的目的索引my_target_index

缩小索引需要满足下列要求:

  • 目标索引存在
  • 索引状态必须是Green
  • 原索引主分片数量比目标索引多,且原索引主分片数量是目标索引倍数
  • 索引中的所有文档在目标索引将会被缩小到一个分片的数量不会超过 2,147,483,519 ,因为这是一个分片的承受的最大文档数量。
  • 执行缩小进程的节点必须要有足够的空闲磁盘空间满足原索引的分片能够全部复制迁徙到该节点。

三、Shrink的工作原理

引用官方手册对Shrink工作过程的描述:

  • 以相同配置创建目标索引,但是降低主分片数量
  • 从源索引的Lucene分段创建硬链接到目的索引。如果系统不支持硬链接,那么索引的所有分段都将复制到新索引,将会花费大量时间
  • 对目标索引执行恢复操作,就像一个关闭的索引重新打开时一样

3.1、创建新索引

使用旧索引的配置创建新索引,只是减少主分片的数量,所有副本都迁移到同一个节点。显然,创建硬链接时,源文件和目标文件必须在同一台主机。

3.2、创建硬链接

从源索引到目的索引创建硬链接。如果操作系统不支持硬链接,则复制Lucene分段。

在Linux下通过strace命令跟踪硬链接创建过程:

strace -e trace=file -p {pid}

Linux下的strace命令用于跟踪系统调用,trace=file表示只跟踪与文件操作相关的系统调用,关于该命令的完整使用方式请求可参考man手册。在strace命令的输出结果中,我们能清晰看到内部过程:

  • JYglvWRnSqmNgA3E1CahZw为源索引;
  • RvDP65d-QD-QTpwOCaLWOg为目的索引;
  • 0.cfe、0.si、_0.cfs 类型的文件为Lucene编号为0的segment,编号依次类推;

链接过程:从源索引的shard[0]开始,遍历所有shard,将所有segment链接到目的索引,目的索引的segment从0开始命名,依次递增。在本例中,由于源索引的shard[0]没有数据,因此从shard[ 1]开始链接。

3.2.1、为什么一定要硬链接,不使用软链接?

Linux的文件系统由两部分组成(实际上任何文件系统的基本概念都相似):inode和block。block用于存储用户数据,inode 用于记录元数据,系统通过inode 定位唯一的文件。

  • 硬链接:文件有相同的inode和block。
  • 软链接:文件有独立的inode和block,block 内容为目的文件路径名。

那么为什么一定要硬链接过去呢?从本质上来说,我们需要保证Shrink之后,源索引和目的索引是完全独立的,读写和删除都不应该互相影响。**如果软链接过去,删除源索引,则目的索引的数据也会被删除,硬链接则不会。**满足下面条件时操作系统才真正删除文件:

文件被打开的fd数量为0且硬链接数量为0。

使用硬链接,删除源索引,只是将文件的硬链接数量减1,删除源索引和目的索引中的任何一个,都不影响另一个正常读写。

由于使用了硬链接,也因为硬链接的特性带来一些限制:不能交叉文件系统或分区进行硬链接的创建,因为不同分区和文件系统有自己的inode。

不过,既然都是链接,Shrink 完成后,修改源索引,目的索引会变吗?答案是不会。虽然链接到了源分段,Shrink期间索引只读,目标索引能看到的只有源索引的当前数据,Shrink 完成后,由于Lucene中分段的不变性,“write once”机制保证每个文件都不会被更新。源索引新写入的数据随着refresh会生成新分段,而新分段没有链接,在目标索引中是看不到的。如果源索引进行merge,对源分段执行删除时,只是硬链接数量减1,目标索引仍然不受影响。因此,Shrink完毕后最终的效果就是,两个索引的数据看起来是完全独立的。

经过链接过程之后,主分片已经就绪,副分片还是空的,通过recovery 将主分片数据复制到副分片。下面看一下相关实现代码

3.2、硬链接过程源码分析

硬链接过程在目标索引my_target_index 的恢复流程中,入口为IndexShard#startRecovery,有下列几种类型的recovery:

  • EXISTING_STORE,主分片从translog恢复;
  • PEER,副分片从主分片远程拉取;
  • SNAPSHOT,从快照中恢复;
  • LOCAL_SHARDS,从同一个节点的其他分片恢复Shrink使用这种恢复类型;

shrink index 时的恢复类型为LOCAL_SHARDS,执行storeRecovery.recoverFromLocalShards

addIndices中,调用Lucene中的org.apache.lucene.store.HardlinkCopyDirectoryWrapper实现硬链接。

addIndices将整个源索引的全部shard链接到目标路径:

addIndices (RecoveryState.Index indexRecoveryStats, Directory target, Directory. .. sources)

本例中源索引有5个分片,sources 值如下:

0 = "(store (mmapfs (/V/ idea/ nodes/0/ indices/-Puacb8gSQG4UAvr -vNopQ/0/index)))"
1 = " (store (mmapfs (/V/idea/ nodes/0/ indices/- Puacb8gSQG4UAvr -vNopQ/1/index)))"
2 = " (store (mmapfs (/V/idea/ nodes/0/ indices/-Puacb8gSQG4UAvr-vN0opQ/2/index)))"
3 = " (store (mmapfs (/V/ idea/ nodes/0/ indices/ -Puacb8gSQG4UAvr-vNopQ/3/ index)))"
4 ="(store (mmapfs (/V/idea/ nodes/0/ indices/-Puacb8gSQG4UAvr-vN0pQ/4/index)))"

target值如下:

store(mmapfs (/Volumes/RamDisk/idea/nodes/0/indices/Dcfi3m9kTW2Dfc2zUjMOoQ/0/index))

关注我的公众号【宝哥大数据】

以上是关于Elasticsearch 原理解析(介绍)的主要内容,如果未能解决你的问题,请参考以下文章

Elasticsearch学习2-安装介绍

69-日志分析系统ELK-Elasticsearch集群搭建和数据读写以及数据分片原理解析

《Elasticsearch 源码解析与优化实战》第17章:Shrink原理分析

《Elasticsearch 源码解析与优化实战》第17章:Shrink原理分析

Elasticsearch-基础介绍及索引原理分析

从诗词大会到图解 ElasticSearch 原理解析