目前java常见的针对大数据存储的方案并不多,常见的就是mysql的分库分表、es存储
这里偏向es存储方案,es不同的版本之间其实差异还挺大的,本篇博文版本Elasticsearch 7.14.0
Springboot整合Easy-Es
Easy-Es官方文档
Elasticsearch的初步认识
小史是一个非科班的程序员,虽然学的是电子专业,但是通过自己的努力成功通过了面试,现在要开始迎接新生活了。
随着央视诗词大会的热播,小史开始对诗词感兴趣,最喜欢的就是飞花令的环节。
但是由于小史很久没有背过诗词了,飞一个字很难说出一句,很多之前很熟悉的诗句也想不起来。
吕老师:但是我让你说出带“前”字的诗句,由于没有索引,你只能遍历脑海中所有诗词,当你的脑海中诗词量大的时候,就很难在短时间内得到结果了
索引量爆炸
搜索引擎原理
Elasticsearch 简介
吕老师:但是 Lucene 还是一个库,必须要懂一点搜索引擎原理的人才能用的好,所以后来又有人基于 Lucene 进行封装,写出了 Elasticsearch
吕老师:类型是用来定义数据结构的,你可以认为是 MySQL 中的一张表。文档就是最终的数据了,你可以认为一个文档就是一条记录
吕老师:比如一首诗,有诗题、作者、朝代、字数、诗内容等字段,那么首先,我们可以建立一个名叫 Poems 的索引,然后创建一个名叫 Poem 的类型,类型是通过 Mapping 来定义每个字段的类型。
比如诗题、作者、朝代都是 Keyword 类型,诗内容是 Text 类型,而字数是 Integer 类型,***就是把数据组织成 Json 格式存放进去了
吕老师:这个问题问得好,这涉及到分词的问题,Keyword 类型是不会分词的,直接根据字符串内容建立反向索引,Text 类型在存入 Elasticsearch 的时候,会先分词,然后根据分词后的内容建立反向索引。
Elasticsearch 分布式原理
吕老师:没错,Elasticsearch 也是会对数据进行切分,同时每一个分片会保存多个副本,其原因和 HDFS 是一样的,都是为了保证分布式环境下的高可用。
吕老师:没错,在 Elasticsearch 中,节点是对等的,节点间会通过自己的一些规则选取集群的 Master,Master 会负责集群状态信息的改变,并同步给其他节点
吕老师:注意,只有建立索引和类型需要经过 Master,数据的写入有一个简单的 Routing 规则,可以 Route 到集群中的任意节点,所以数据写入压力是分散在整个集群的。
深入理解ElasticSearch工作原理
鉴于Lucene如此强大却难以上手的特点,诞生了ES。ES也是使用Java编写的,它的内部使用Lucene做索引与搜索,它的目的是隐藏Lucene的复杂性,取而代之的提供一套简单一致的RESTful API。
ES具有如下特点:
一个分布式的实时文档存储引擎,每个字段都可以被索引与搜索
一个分布式实时分析搜索引擎,支持各种查询和聚合操作
能胜任上百个服务节点的扩展,并可以支持PB级别的结构化或者非结构化数据
ElasticSearch VS MySQ
- 大数据量高并发环境下的 MySQL 应用开发越来越复杂,也越来越具有技术挑战性。分表分库的规则把握都是需要经验的。虽然有像淘宝这样技术实力强大的公司开发了透明的中间件层来屏蔽开发者的复杂性,但是避免不了整个架构的复杂性。分库分表的子库到一定阶段又面临扩展问题。还有就是需求的变更,可能又需要一种新的分库方式。
关系数据库很强大,但是它并不能很好的应付所有的应用场景。MySQL 的扩展性差(需要复杂的技术来实现),大数据下 IO 压力大,表结构更改困难,正是当前使用 MySQL 的开发人员面临的问题。
- 非关系型、搜索引擎、近实时搜索与分析、高可用、天然分布式、横向可扩展
- 业务宽表(数据库字段太多,查询太慢,索引没有办法再做优化)
数据库做统计查询 - 日志分析
倒排索引
索引是构成搜索引擎的核心技术之一,索引在日常生活中其实也是非常常见的,比如当我们看一本书的时候,我们首先会看书的目录,通过目录可以快速定位到某一章节的页码,加快对内容的查询速度。
倒排索引,也常被称为反向索引,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射,它是文档检索系统中最常用的数据结构。
下面我们通过具体实例深入理解倒排索引,通过简单文档以小见大,体验倒排索引的建过程
对于文档内容,先要经过词条化处理。与英文不同的是,英文通过空格分隔单词,中文的词与词之间没有明确的分隔符号,经过分词系统进行中文分词以后把矩阵切分成一个个的词条
然而这套索引的实现,要上生产环境,那还远着。这个世界上那么多单词,中文、英文、日文、韩文 … 每次搜索一个单词,都要全局遍历一遍,很明显不行。
于是有了排序,我们需要对单词进行排序,像 B+ 树一样,可以在页里实现二分查找
Lucene 的倒排索引,增加了最左边的一层「字典树」term index,它不存储所有的单词,只存储单词前缀,通过字典树找到单词所在的块,也就是单词的大概位置,再在块里二分查找,找到对应的单词,再找到单词对应的文档列表。
Lucene 的实现会要更加复杂,针对不同的数据结构采用不同的字典索引,使用了FST模型、BKDTree等结构。
真实的倒排记录也并非一个链表,而是采用了SkipList、BitSet等结构
ElasticSearch如何建立索引
索引的不变性
由于倒排索引的结构特性,在索引建立完成后对其进行修改将会非常复杂。再加上几层索引嵌套,更让索引的更新变成了几乎不可能的动作。
所以索性设计成不可改变的:倒排索引被写入磁盘后是不可改变的,它永远不会修改。
不变性有重要的价值:
1.不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
2.一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
3.其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
写入单个大的倒排索引允许数据压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量
当然,一个不变的索引也有不好的地方。主要事实是它是不可变的,你不能修改它。如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制
动态更新索引
怎样在保留不变性的前提下实现倒排索引的更新?答案是: 用更多的索引。
通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到—从最早的开始—查询完后再对结果进行合并。
Elasticsearch 基于 Lucene, 引入了 按段搜索 的概念。 每一 段 本身都是一个倒排索引, 但 索引 在 Lucene 中除表示所有 段 的集合外, 还增加了 提交点 的概念 — 一个列出了所有已知段的文件,新的文档首先被添加到内存索引缓存中,然后写入到一个基于磁盘的段。
在 lucene 中查询是基于 segment。每个 segment 可以看做是一个独立的 subindex,在建立索引的过程中,lucene 会不断的 flush 内存中的数据持久化形成新的 segment。多个 segment 也会不断的被 merge 成一个大的 segment,在老的 segment 还有查询在读取的时候,不会被删除,没有被读取且被 merge 的 segement 会被删除。
1)数据先写入内存buffer,在写入buffer的同时将数据写入translog日志文件,注意:此时数据还没有被成功es索引记录,因此无法搜索到对应数据;
2)如果buffer快满了或者到一定时间,es就会将buffer数据refresh到一个新的segment file中,但是此时数据不是直接进入segment file的磁盘文件,而是先进入os cache的。这个过程就是refresh。
每隔1秒钟,es将buffer中的数据写入一个新的segment file,因此每秒钟会产生一个新的磁盘文件segment file,这个segment file中就存储最近1秒内buffer中写入的数据。
操作系统中,磁盘文件其实都有一个操作系统缓存os cache,因此数据写入磁盘文件之前,会先进入操作系统级别的内存缓存os cache中。
一旦buffer中的数据被refresh操作,刷入os cache中,就代表这个数据就可以被搜索到了。
这就是为什么es被称为准实时(NRT,near real-time):因为写入的数据默认每隔1秒refresh一次,也就是数据每隔一秒才能被 es 搜索到,之后才能被看到,所以称为准实时。
只要数据被输入os cache中,buffer就会被清空,并且数据在translog日志文件里面持久化到磁盘了一份,此时就可以让这个segment file的数据对外提供搜索了。
3)重复1~2步骤,新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去,每次refresh完,buffer就会被清空,同时translog保留一份日志数据。随着这个过程推进,translog文件会不断变大。当translog文件达到一定程度时,就会执行commit操作。
4)commit操作发生第一步,就是将buffer中现有数据refresh到os cache中去,清空buffer。
5)将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file,同时强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去。
6)将现有的translog清空,然后再次重启启用一个translog,此时commit操作完成。
translog日志文件的作用是什么?
在你执行commit操作之前,数据要么是停留在buffer中,要么是停留在os cache中,无论是buffer还是os cache都是内存,一旦这台机器死了,内存中的数据就全丢了。
因此需要将数据对应的操作写入一个专门的日志文件,也就是translog日志文件,一旦此时机器宕机,再次重启的时候,es会自动读取translog日志文件中的数据,恢复到内存buffer和os cache中去。
综上可以看出:
es是准实时的,因此数据写入1秒后才可以搜索到。
如果translog是异步写入的话,es可能会丢失数据:有n秒的数据停留在buffer、translog的os cache、segment file的os cache中,也就是这n秒的数据不在磁盘上,此时如果宕机,会导致n秒的数据丢失。
translog
写入ES的数据首先会被写入translog文件,该文件持久化到磁盘,保证服务器宕机的时候数据不会丢失,由于顺序写磁盘,速度也会很快。
同步写入:每次写入请求执行的时候,translog在fsync到磁盘之后,才会给客户端返回成功
异步写入:写入请求缓存在内存中,每经过固定时间之后才会fsync到磁盘,写入量很大,对于数据的完整性要求又不是非常严格的情况下,可以开启异步写入
refresh
经过固定的时间,或者手动触发之后,将内存中的数据构建索引生成segment,写入文件系统缓冲区
commit/flush
- 超过固定的时间,或者translog文件过大之后,触发flush操作:
- 内存的buffer被清空,相当于进行一次refresh
- 文件系统缓冲区中所有segment刷写到磁盘
- 将一个包含所有段列表的新的提交点写入磁盘
- 启动或重新打开一个索引的过程中使用这个提交点来判断哪些segment隶属于当前分片
- 删除旧的translog,开启新的translog
merge
上面提到,每次refresh的时候,都会在文件系统缓冲区中生成一个segment,后续flush触发的时候持久化到磁盘。所以,随着数据的写入,尤其是refresh的时间设置的很短的时候,磁盘中会生成越来越多的segment:
segment数目太多会带来较大的麻烦。 每一个segment都会消耗文件句柄、内存和cpu运行周期。
更重要的是,每个搜索请求都必须轮流检查每个segment,所以segment越多,搜索也就越慢。
merge的过程大致描述如下:
- 磁盘上两个小segment:A和B,内存中又生成了一个小segment:C
- A,B被读取到内存中,与内存中的C进行merge,生成了新的更大的segment:D
- 触发commit操作,D被fsync到磁盘
- 创建新的提交点,删除A和B,新增D
- 删除磁盘中的A和B
文档的更新与删除
删除
段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。
磁盘上的每个segment都有一个.del文件与它相关联。当发送删除请求时,该文档未被真正删除,而是在.del文件中标记为已删除。此文档可能仍然能被搜索到,但会从结果中过滤掉。当segment合并时,在.del文件中标记为已删除的文档不会被包括在新的segment中,也就是说merge的时候会真正删除被删除的文档
更新
创建新文档时,Elasticsearch将为该文档分配一个版本号。对文档的每次更改都会产生一个新的版本号。当执行更新时,旧版本在.del文件中被标记为已删除,并且新版本在新的segment中写入索引。旧版本可能仍然与搜索查询匹配,但是从结果中将其过滤掉
并发控制
在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失:
悲观并发控制
这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
乐观并发控制
Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
Elasticsearch 是分布式的。当文档创建、更新或删除时, 新版本的文档必须复制到集群中的其他节点。Elasticsearch 也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许 顺序是乱的。Elasticsearch 需要一种方法确保文档的旧版本不会覆盖新的版本。
每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。 Elasticsearch 使用这个 _version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
使用内部版本号:删除或者更新数据的时候,携带_version参数,如果文档的最新版本不是这个版本号,那么操作会失败,这个版本号是ES内部自动生成的,每次操作之后都会递增一。
PUT /website/blog/1?version=1
{"title": "My first blog entry","text": "Starting to get the hang of this..."
}
使用外部版本号:ES默认采用递增的整数作为版本号,也可以通过外部自定义整数(long类型)作为版本号,例如时间戳。通过添加参数version_type=external,可以使用自定义版本号。内部版本号使用的时候,更新或者删除操作需要携带ES索引当前最新的版本号,匹配上了才能成功操作。外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同, Elasticsearch 不是检查当前 _version 和请求中指定的版本号是否相同, 而是检查当前 _version 是否 小于 指定的版本号。 如果请求成功,外部的版本号作为文档的新 _version 进行存
PUT /website/blog/2?version=5&version_type=external
{"title": "My first external blog entry","text": "Starting to get the hang of this..."
}
批量操作
bulk API 允许在单个步骤中进行多次 create 、 index 、 update 或 delete 请求。如果你需要索引一个数据流比如日志事件,它可以排队和索引数百或数千批次。
bulk 请求不是原子的: 不能用它来实现事务控制。每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求。
整个批量请求都需要由接收到请求的节点加载到内存中,因此该请求越大,其他请求所能获得的内存就越少。 批量请求的大小有一个最佳值,大于这个值,性能将不再提升,甚至会下降。 但是最佳值不是一个固定的值。它完全取决于硬件、文档的大小和复杂度、索引和搜索的负载的整体情况
ElasticSearch数据类型
Elasticsearch 支持如下简单域类型:
- 字符串: string
- 整数 : byte, short, integer, long
- 浮点数: float, double
- 布尔型: boolean
- 日期: date
映射
为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。
索引中每个文档都有 类型 。每种类型都有它自己的 映射 ,或者 模式定义 。映射定义了类型中的域,每个域的数据类型,以及Elasticsearch如何处理这些域。映射也用于配置与类型有关的元数据。
当你索引一个包含新域的文档(之前未曾出现),Elasticsearch 会使用 动态映射,通过JSON中基本数据类型,尝试猜测域类型,使用如下规则:
ElasticSearch集群原理
Document:文档,指一行数据;
Index:索引,是多个document的集合(和sql数据库的表对应);
Shard:分片,当有大量的文档时,由于内存的限制、磁盘处理能力不足、无法足够快的响应客户端的请求等,一个节点可能不够。这种情况下,数据可以分为较小的分片。每个分片放到不同的服务器上。
当你查询的索引分布在多个分片上时,ES会把查询发送给每个相关的分片,并将结果组合在一起,而应用程序并不知道分片的存在。即:这个过程对用户来说是透明的
Replia:副本,为提高查询吞吐量或实现高可用性,可以使用分片副本。
副本是一个分片的精确复制,每个分片可以有零个或多个副本。ES中可以有许多相同的分片,其中之一被选择更改索引操作,这种特殊的分片称为主分片。
当主分片丢失时,如:该分片所在的数据不可用时,集群将副本提升为新的主分片。
Node:节点,形成集群的每个服务器称为节点,一个节点可以包含多个shard
Cluster:集群,ES可以作为一个独立的单个搜索服务器。不过,为了处理大型数据集,实现容错和高可用性,ES可以运行在许多互相合作的服务器上。这些服务器的集合称为集群。
我们往 Elasticsearch 添加数据时需要用到 索引 —— 保存相关数据的地方。 索引实际上是指向一个或者多个物理 分片 的 逻辑命名空间 。
一个 分片 是一个底层的 工作单元 ,它仅保存了全部数据中的一部分。 一个分片是一个 Lucene 的实例,以及它本身就是一个完整的搜索引擎。 我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。
Elasticsearch 是利用分片将数据分发到集群内各处的。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。
一个分片可以是 主分片或者 副本分片。索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。
一个副本分片只是一个主分片的拷贝。副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。
在索引建立的时候就已经确定了主分片数,但是副本分片数可以随时修改
集群节点角色
ES集群的服务器主要分为以下三种角色:
1)master节点:负责保存和更新集群的一些元数据信息,之后同步到所有节点,所以每个节点都需要保存全量的元数据信息:
- 集群的配置信息
- 集群的节点信息
- 模板template设置
- 索引以及对应的设置、mapping、分词器和别名
- 索引关联到的分片以及分配到的节点
2)data节点:负责数据存储和查询
3)coordinator节点:
- 路由索引请求
- 聚合搜索结果集
- 分发批量索引请求
master选举
选举策略
如果集群中存在master,认可该master,加入集群
如果集群中不存在master,从具有master资格的节点中选id最小的节点作为master
选举时机
集群启动:后台启动线程去ping集群中的节点,按照上述策略从具有master资格的节点中选举出master
现有的master离开集群:后台一直有一个线程定时ping master节点,超过一定次数没有ping成功之后,重新进行master的选举
避免脑裂
脑裂问题是采用master-slave模式的分布式集群普遍需要关注的问题,脑裂一旦出现,会导致集群的状态出现不一致,导致数据错误甚至丢失。
ES避免脑裂的策略:过半原则,可以在ES的集群配置中添加一下配置,避免脑裂的发生
数据副本
ES通过副本分片的方式,保证集群数据的高可用,同时增加集群并发处理查询请求的能力,相应的,在数据写入阶段会增大集群的写入压力。
数据写入的过程中,首先被路由到主分片,写入成功之后,将数据发送到副本分片,为了保证数据不丢失,最好保证至少一个副本分片写入成功以后才返回客户端成功。
水平扩容
Node 1 和 Node 2 上各有一个分片被迁移到了新的 Node 3 节点,现在每个节点上都拥有2个分片,而不是之前的3个。 这表示每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片的性能将会得到提升。
分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有6个分片(3个主分片和3个副本分片)的索引可以最大扩容到6个节点,每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。
但是如果我们想要扩容超过6个节点怎么办呢?
主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够 存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 但是,读操作——搜索和返回数据——可以同时被主分片 或 副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。
在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。让我们把副本数从默认的 1 增加到 2 :
故障转移
如果我们关闭第一个节点,这时集群的状态为:
我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点: Node 2 。
在我们关闭 Node 1 的同时也失去了主分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为 red :不是所有主分片都在正常工作。
幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片
路由机制
当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?
首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:
shard = hash(routing) % number_of_primary_shards
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。
这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。
新建、索引、删除文档
新建、索引和删除请求都是写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片。
以下是在主副分片和任何副本分片上面 成功新建,索引和删除文档所需要的步骤顺序:
- 客户端向 Node 1 发送新建、索引或者删除请求。
- 节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上
- Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。
在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。
查询文档
可以从主分片或者从其它任意副本分片检索文档
- 客户端向 Node 1 发送获取请求
- 节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node 2 。
- Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。
在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的
更新文档
以下是部分更新一个文档的步骤:
- 客户端向 Node 1 发送更新请求。
- 它将请求转发到主分片所在的 Node 3 。
- Node 3 从主分片检索文档,修改 _source 字段中的 JSON ,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤 3 ,超过 retry_on_conflict 次后放弃
- 如果 Node 3 成功地更新文档,它将新版本的文档并行转发到 Node 1 和 Node 2 上的副本分片,重新建立索引。 一旦所有副本分片都返回成功, Node 3 向协调节点也返回成功,协调节点向客户端返回成功
分布式检索
一个 CRUD 操作只对单个文档进行处理,文档的唯一性由 _index, _type, 和 routing values的组合来确定。这表示我们确切的知道集群中哪个分片含有此文档。
搜索需要一种更加复杂的执行模型因为我们不知道查询会命中哪些文档: 这些文档有可能在集群的任何分片上。一个搜索请求必须询问我们关注的索引(index or indices)的所有分片的某个副本来确定它们是否含有任何匹配的文档。
但是找到所有的匹配文档仅仅完成事情的一半。在 search 接口返回一个 page 结果之前,多分片中的结果必须组合成单个排序列表。 为此,搜索被执行成一个两阶段过程,我们称之为 query then fetch 。
查询阶段
在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的优先队列。
查询阶段包含以下三个步骤:
- 客户端发送一个 search 请求到 Node 3 , Node 3 会创建一个大小为 from + size 的空优先队列。
- Node 3 将查询请求转发到索引的每个主分片或副本分片中。每个分片在本地执行查询并添加结果到大小为 from + size 的本地有序优先队列中。
- 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,也就是 Node 3 ,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表
当一个搜索请求被发送到某个节点时,这个节点就变成了协调节点。 这个节点的任务是广播查询请求到所有相关分片并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端
协调节点将这些分片级的结果合并到自己的有序优先队列里,它代表了全局排序结果集合。至此查询过程结束。
取回阶段
查询阶段标识哪些文档满足搜索请求,但是我们仍然需要取回这些文档,这是取回阶段的任务。
分布式阶段由以下步骤构成:
- 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。
- 每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点
- 一旦所有的文档都被取回了,协调节点返回结果给客户端
协调节点首先决定哪些文档确实需要被取回。例如,如果我们的查询指定了 { “from”: 90, “size”: 10 } ,最初的90个结果会被丢弃,只有从第91个开始的10个结果需要被取回。这些文档可能来自和最初搜索请求有关的一个、多个甚至全部分片。
深分页
每个分片必须先创建一个 from + size 长度的队列,协调节点需要根据 number_of_shards * (from + size) 排序文档,来找到被包含在 size 里的文档。
取决于你的文档的大小,分片的数量和你使用的硬件,给 10,000 到 50,000 的结果文档深分页( 1,000 到 5,000 页)是完全可行的。但是使用足够大的 from 值,排序过程可能会变得非常沉重,使用大量的CPU、内存和带宽。
ElasticSearch实际使用过程中会有什么问题
分片数过小,数据写入形成瓶颈,无法水平拓展
分片数过多,每个分片都是一个lucene的索引,分片过多将会占用过多资源
如何计算分片数
需要注意分片数量最好设置为节点数的整数倍,保证每一个主机的负载是差不多一样的,特别的,如果是一个主机部署多个实例的情况,更要注意这一点,否则可能遇到其他主机负载正常,就某个主机负载特别高的情况。
一般我们根据每天的数据量来计算分片,保持每个分片的大小在 50G 以下比较合理。如果还不能满足要求,那么可能需要在索引层面通过拆分更多的索引或者通过别名 + 按小时 创建索引的方式来实现了。
ES数据近实时问题
S数据写入之后,要经过一个refresh操作之后,才能够创建索引,进行查询。但是get查询很特殊,数据实时可查。
ES5.0之前translog可以提供实时的CRUD,get查询会首先检查translog中有没有最新的修改,然后再尝试去segment中对id进行查找。5.0之后,为了减少translog设计的负责性以便于再其他更重要的方面对translog进行优化,所以取消了translog的实时查询功能。
get查询的实时性,通过每次get查询的时候,如果发现该id还在内存中没有创建索引,那么首先会触发refresh操作,来让id可查。
深分页问题
解决方案1:服务端缓存 Scan and scroll API
为了返回某一页记录,其实我们抛弃了其他的大部分已经排好序的结果。那么简单点就是把这个结果缓存起来,下次就可以用上了。根据这个思路,ES提供了Scroll API。它概念上有点像传统数据库的游标(cursor)。
scroll调用本质上是实时创建了一个快照(snapshot),然后保持这个快照一个指定的时间,这样,下次请求的时候就不需要重新排序了。从这个方面上来说,scroll就是一个服务端的缓存。既然是缓存,就会有下面两个问题:
- 一致性问题。ES的快照就是产生时刻的样子了,在过期之前的所有修改它都视而不见。
- 服务端开销。ES这里会为每一个scroll操作保留一个查询上下文(Search context)。ES默认会合并多个小的索引段(segment)成大的索引段来提供索引速度,在这个时候小的索引段就会被删除。但是在scroll的时候,如果ES发现有索引段正处于使用中,那么就不会对它们进行合并。这意味着需要更多的文件描述符以及比较慢的索引速度。
其实这里还有第三个问题,但是它不是缓存的问题,而是因为ES采用的游标机制导致的。就是你只能顺序的扫描,不能随意的跳页。而且还要求客户每次请求都要带上”游标”。
解决方案2:Search After
Scroll API相对于from+size方式当然是性能好很多,但是也有如下问题:
- Search context开销不小。
- 是一个临时快照,并不是实时的分页结果。
针对这些问题,ES 5.0 开始推出了Search After机制可以提供了更实时的游标(live cursor)。它的思想是利用上一页的分页结果来提高下一页的分页请求。