ES其他知识点
文章目录
- ES其他知识点
- 一:分布式集群和路由计算
- 1:单节点集群
- 2:故障转移
- 3:水平扩容
- 4:宕机故障
- 5:路由计算
- 二:分片控制流程
- 1:协调节点
- 2:写流程
- 3:读流程
- 4:更新流程
- 5:多文档操作流程
- 5.1:mget操作流程
- 5.2:bulk操作流程
- 6:ES数据一致性的保证
- 6.1:乐观并发控制 - 版本号
- 6.2:乐观并发控制 - 外部系统
- 三:分片原理(重要)
- 1:文档搜索 - segment的引入
- 1.1:不可变的倒排索引
- 1.2:动态更新倒排索引
- 1.3:按照段进行搜索 - commit point
- 1.4:分段思想下的写数据
- 2:近实时搜索 - os cache的引入
- 3:持久化变更 - translog的引入
- 4:段合并 - merge segement
- 4.1:段合并的流程
- 4.2:段合并的性能影响
- 4.3:再谈写流程
- 4.4:再谈读操作
- 5:搜索流程详解
- 5.1:四种searchType
- 5.2:详解Query Then Fetch
- 四:面试相关
- 1:Text和KeyWord的区别(重)
- 2:B+树为什么不适合做全文检索
- 3:ES集群脑裂问题
- 3.1:脑裂成因
- 3.2:解决方案
- 4:ES的性能问题
一:分布式集群和路由计算
1:单节点集群
我们在包含一个空节点的集群内创建名为 users 的索引,为了演示目的,我们将分配 3 个主分片和一份副本(每个主分片拥有一个副本分片)
{"settings" : {"number_of_shards" : 3, // 3 个主分片"number_of_replicas" : 1 // 每 1 个主分片都有 1 个副本}
}
在 Postman 发送 PUT
请求:http://127.0.0.1:7001/users
我们的集群现在是拥有一个索引的单节点集群。所有 3 个主分片都被分配在 node - 1
看到的 users 是刚才添加的索引
当前我们的集群是正常运行的,但是在硬件故障时有丢失数据的风险
2:故障转移
当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。
幸运的是,我们只需再启动一个节点即可防止数据丢失。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name
配置,它就会自动发现集群并加入到其中。
但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。
之所以配置为使用单播发现,以防止节点无意中加入集群。
只有在同一台机器上 运行的节点才会自动组成集群。
如果启动了第二个节点,我们的集群将会拥有两个节点的集群: 所有主分片和副本分片都已被分配
3:水平扩容
怎样为我们的正在增长中的应用程序按需扩容呢?当启动了第三个节点,我们的集群将会拥有三个节点的集群: 为了分散负载而对分片进行重新分配
.geoip_databases
不用看,包括下面的图,它是自带的一个索引,我们探索的是 users 索引
Node 7001 和 Node 7002 上各有一个分片被迁移到了新的 Node 7003 节点,现在每个节点上都拥有 2 个分片,而不是之前的 3 个。
这表示每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片的性能将会得到提升。
分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。
我们这个拥有 6 个分片(3 个主分片和 3 个副本分片)的索引可以最大扩容到 6 个节点
每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。
但是如果我们想要扩容超过 6 个节点怎么办呢?
主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储的最大数据量。(实际大小取决于你的数据、硬件和使用场景。)但是,读操作和返回数据可以同时被主分片或副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。
在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。让我们把副本数从默认的 1 增加到 2
{"number_of_replicas" : 2
}
在 Postman 发送 PUT
请求:http://127.0.0.1:7001/users/_settings
users 索引现在拥有 9 个分片:3 个主分片和 6 个副本分片。
这意味着我们可以将集群扩容到 9 个节点,每个节点上一个分片。相比原来 3 个节点时,集群搜索性能可以提升 3 倍。
当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。
你需要增加更多的硬件资源来提升吞吐量。
但是更多的副本分片数提高了数据冗余量:按照上面的节点配置,我们可以在失去 2 个节点的情况下不丢失任何数据。
4:宕机故障
没宕机前的集群状态:
宕机之后的集群状态:【这里关闭Node1】
我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点:Node 7002。
在我们关闭 Node 7001 的同时也失去了主分片 1 和 2,并且在缺失主分片的时候索引也不能正常工作。
如果此时立即
检查集群的状况,我们看到的状态将会为 red:不是所有主分片都在正常工作。
但是过一会
检查集群状态如下:
幸运的是,在其它节点上存在着这两个主分片的完整副本,所以新的主节点立即将这些分片在 Node 7002 和 Node 7003 上对应的副本分片提升为主分片
,此时集群的状态将会为 yellow。
这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。
为什么我们集群状态是 yellow 而不是 green 呢?
虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应 2 份副本分片,而此时只存在一份副本分片。
所以集群不能为 green 的状态,不过我们不必过于担心:如果我们同样关闭了 Node 7002,我们的程序依然可以保持在不丢任何数据的情况下运行,因为 Node 7003 为每一个分片都保留着一份副本。
主分片消失,则副本上位,变成主分片
如果我们重新启动 Node 7001,集群可以将缺失的副本分片再次进行分配,那么集群的状态也将恢复成之前的状态。
如果 Node 7001 依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。
和之前的集群相比,只是 Master 节点切换了。
重启Node1
只不过 Master 从 Node 7001 变成了 Node 7002,类比绝大多数主节点,从节点设计
5:路由计算
当检索一个文档的时候,文档会被存储到一个主分片中。
Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?
当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?
首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:
位置 = hash(路由哈希值) % 分片数
shard = hash(routing) % number_of_primary_shards
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。
routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards(主分片的数量)后得到余数。
这个分布在 0 到 number_of_primary_shards - 1 之间的余数,就是我们所寻求的文档所在分片的位置。
这就解释了为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量
因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。
所有的文档 API( get、index、delete、bulk、update 以及 mget )都接受一个叫做 routing 的路由参数
通过这个参数我们可以自定义文档到分片的映射。
一个自定义的路由参数可以用来确保所有相关的文档 -> 例如所有属于同一个用户的文档都被存储到同一个分片中。
二:分片控制流程
1:协调节点
我们假设有一个集群由三个节点组成。它包含一个叫 kele 的索引,有两个主分片,每个主分片有两个副本分片。
相同分片的副本不会放在同一节点
在 Postman 发送 PUT
请求:http://127.0.0.1:7001/xustudyxu:
{"settings": {"number_of_shards": 2,"number_of_replicas" : 2}
}
我们可以发送请求到集群中的任一节点。每个节点都有能力处理任意请求。
每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 每一个被发到请求的节点就是当前请求的协调节点
在下面的例子中,将所有的请求发送到 Node 7001,我们将其称为协调节点(coordinating node)。也可以理解为转发到其他节点的节点。
当发送请求的时候,为了扩展负载,更好的做法是轮询集群中所有的节点
2:写流程
为了便于区分节点个数。Node 7001 是 Node 1,Node 7002 是 Node 2,Node 7003 是 Node 3。
新建、索引和删除请求都是写
操作,必须在主分片上面完成写入操作之后才能被复制到相关的副本分片
- 客户端向 Node 1 发送新建、索引或者删除请求
- 节点使用文档的
_id
确定文档属于分片P0
。请求会被转发到 Node 3,因为分片P0
的主分片目前被分配在 Node 3 上 - Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。
- 一旦所有的副本分片都报告成功,Node 3 将向协调节点报告成功,协调节点向客户端报告成功
在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。
有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。很少使用 -> consistency & timeout
新索引默认有 1 个副本分片,这意味着为满足规定数量应该需要两个活动的分片副本。
但是,这些默认的设置会阻止我们在单一节点上做任何事情。
为了避免这个问题,要求只有当 number_of_replicas 大于 1 的时候,规定数量才会执行
3:读流程
- 客户端向 Node 1 发送获取请求
- 节点使用文档的
_id
来确定文档属于分片P0
。分片P0
的副本分片存在于所有的三个节点上。在这种情况下,它将请求转发到 Node 2 - Node 2 将文档返回给 Node 1,然后将文档返回给客户端
注意,不是每次都是Node2返回文档给客户端,而是采用轮询,通过轮询所有的副本分片来达到负载均衡。
在文档被检索时,已经被检索的文档可能已经存在于主分片上,但是还没有复制到副本分片。
在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。
一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的
4:更新流程
- 客户端向 Node 1 发送更新请求
- 节点使用文档的
_id
来确定文档属于分片P0
,它将请求转发到主分片所在的 Node 3,因为分片P0
的主分片目前被分配在 Node 3 上 - Node 3 从主分片检索文档,修改
_source
字段中的 JSON 数据,并且尝试重新检索主分片的文档。如果文档已经被另一个进程修改,它会重试步骤 3,超过 retry_on_conflict 次数后放弃 - 如果 Node 3 成功地更新文档,它将新版本的文档并行转发到 Node 1 和 Node 2 上的副本分片,重新建立索引。一旦所有副本分片都返回成功,Node 3 向协调节点也返回成功,协调节点向客户端返回成功
主分片同步到副本分片时,是转发更新请求吗?
不是。当主分片把更改转发到副本分片时,它不会转发更新请求。
相反,它转发完整文档的新版本。
这些数据更改文档将会异步转发到副本分片,并且不能保证数据更改文档以发送它们相同的顺序到达。
如果 Elasticsearch 仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档
5:多文档操作流程
5.1:mget操作流程
-
客户端向node-9201节点发送mget请求。
-
node-9201节点为每一个节点都创建多文档获取请求,然后并行转发这些请求给所有节点,例如node-9202和node-9203。
-
当node-9202节点以及node-9203节点处理完请求后,会将结果响应给node-9201节点。
-
node-9201节点将请求结果响应给客户端。
整个流程相当于是批量的get请求,每个节点对于请求的处理参考前面介绍的读流程。
5.2:bulk操作流程
- 客户端向node-9201节点发送bulk请求。
- node-9201节点为每个节点都创建批量请求,然后并行转发这些请求给每个包含主分片的节点。
- 当所有节点处理完请求后,会将结果响应给node-9201节点。
- node-9201节点将请求结果响应给客户端。
整个流程相当于是批量的新建、删除、更新请求,每个节点对于请求的处理参考前面介绍的写流程。
6:ES数据一致性的保证
6.1:乐观并发控制 - 版本号
ES 数据并发冲突控制是基于的乐观锁和版本号的机制
一个document第一次创建的时候,它的_version
内部版本号就是1;
以后,每次对这个document执行修改或者删除操作,都会对这个_version
版本号自动加1;哪怕是删除,也会对这条数据的版本号加1(假删除)。
客户端对es数据做更新的时候,如果带上了版本号,那带的版本号与es中文档的最新版本号一致才能修改成功,否则抛出异常。
如果客户端没有带上版本号,首先会读取最新版本号才做更新尝试,这个尝试类似于CAS操作,可能需要尝试很多次才能成功。
乐观锁的好处是不需要互斥锁的参与。
es节点更新之后会向副本节点同步更新数据(同步写入),直到所有副本都更新了才返回成功。
对于写操作,一致性级别支持
quorum/one/all
, 默认为quorum
, 即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。
对于读操作,可以设置
replication = sync
(默认),这使得操在主分片和副本分片都完成后才会返回;如果设置
replication = async
时,也可以通过设置搜索请求参数preference为primary来查询主分片,确保文档是最新版本。
6.2:乐观并发控制 - 外部系统
版本号(version)只是其中一个实现方式,我们还可以借助外部系统使用版本控制,一个常见的设置是使用其它数据库作为主要的数据存储
使用 Elasticsearch 做数据检索
这意味着主数据库的所有更改发生时都需要被复制到 Elasticsearch
如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并发问题。
如果你的主数据库已经有了版本号,或一个能作为版本号的字段值比如 timestamp
那么你就可以在 Elasticsearch 中通过增加version_type=external
到查询字符串的方式重用这些相同的版本号
外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同
Elasticsearch 不是检查当前 _version
和请求中指定的版本号是否相同,而是检查当前 _version
是否小于指定的版本号。
如果请求成功,外部的版本号作为文档的新 _version
进行存储。
外部版本号不仅在索引和删除请求是可以指定,而且在创建新文档时也可以指定。
举一个例子:例如要创建一个新的具有外部版本号 5 的博客文章,我们可以按以下方法进行:
PUT /website/blog/2?version=5&version_type=external
{"title": "My first external blog entry","text": "Starting to get the hang of this..."
}
在响应中,我们能看到当前的 _version
版本号是5:
{"_index": "website","_type": "blog","_id": "2","_version": 5,"created": true
}
现在我们更新这个文档,指定一个新的 version 号是 10 :
PUT /website/blog/2?version=10&version_type=external
{"title": "My first external blog entry","text": "This is a piece of cake..."
}
请求成功并将当前 _version 设为 10 :
{"_index": "website","_type": "blog","_id": "2","_version": 10,"created": false
}
三:分片原理(重要)
1:文档搜索 - segment的引入
1.1:不可变的倒排索引
早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘,一旦需要为新的文档建立倒排索引,就需要替换整个倒排索引
即,倒排索引被写入磁盘后是不可改变的,只能整个替换
这样做的优点在于:
-
不需要锁。如果从来不更新索引,就不需要担心多进程同时修改数据的问题。
-
一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
-
写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
这样做的缺点也十分明显:
- 如果需要让一个新的文档可被搜索,就需要重建整个倒排索引,这就对一个倒排索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
1.2:动态更新倒排索引
为了能够在保留倒排索引的不可变性的前提下,实现倒排索引的更新,Elasticsearch采用了补充倒排索引的方法
通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。
在检索时,每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并
这样就可以避免频繁的重建倒排索引而导致的性能损耗了。
1.3:按照段进行搜索 - commit point
Elasticsaerch是基于Lucene开发的,其拥有着按段(segment)搜索的概念,每一个段(segment)本身就是一个倒排索引
除了段之外,还有着提交点(commit point)的概念,在提交点中记录当前所有可用的segment,而新增补充倒排索引,其实就是新增一个段
段和提交点的关系如下图所示:
1.4:分段思想下的写数据
写入数据的大致流程
当一个新的文档被添加到索引时,会经历如下流程(这里只关注段和提交点的使用情况)
- 新文档被添加到内存缓存
- 不时地【默认经过一定时间,或者当内存中数据量达到一定阶段时,再批量提交到磁盘中】,缓存被提交
- 一个新的段(补充的倒排索引)被写入磁盘
- 一个新的包含新段的提交点生成并被写入磁盘
- 磁盘进行同步——所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件
- 新的段被开启,让它包含的文档可以被检索到
- 内存缓存被清空,等待接收新的文档
更新删除数据的大致流程
删除和更新都是写操作,但是由于Elasticsearch中的文档是不可变的,因比不能被删除或者改动以展示其变更
所以ES利用.del
文件标记文档是否被删除,磁盘上的每个段都有一个相应的.del
文件
- 删除操作,文档其实并没有真的被删除,而是在
.del
文件中被标记为deleted状态。该文档依然能匹配查询,但是会在结果中被过滤掉。 - 更新操作,就是将旧的doc标识为deleted状态,然后创建一个新的doc。
查询的大致流程
查询所有段中满足查询条件的数据,然后对每个段里查询的结果集进行合并,得到一个大的结果集
然后将.del文件中记录的被删除的数据剔除后返回给客户端
2:近实时搜索 - os cache的引入
从分段思想下的新增数据流程可以看出,当一个新的文档被写入时,数据还在内存缓存中,此时这条数据是不可查询的
因此Elasticsearch的查询是近实时搜索的
当提交一个新的段到磁盘时,需要使用系统调用fsync来确保数据被物理性地写入磁盘,这样在断电后就不会丢失数据了。
然而fsync的代价很大,如果每次新增/修改数据都使用fsync来将其物理性地写入磁盘,就会导致很大的性能损耗。
在Elasticsearch中使用的是更加轻量化的方式来使得一个文档可以被检索
即,要将fsync从文档被写入到其可被检索的过程中移除掉,以此来提升性能。
为了达到这个目标,在Elasticsearch和磁盘之间,是操作系统的文件系统缓存(OS Cache)
。
在内存索引缓冲区中的文档会被写入到一个新的段中,但是这里新段会被先写入到文件系统缓存(这一步的代价比较低)
稍后再被批量刷新到磁盘(这一步代价比较高)
只要文件已经在文件系统缓存中,就可以像其它文件一样被打开和读取了,即可以被检索【在OS Cache中就可以检索到了】
把内存缓冲区中的数据写入文件系统缓存的过程叫做refresh,该操作,默认情况1s
或者内存缓冲区的数据达到一定数据量时就会执行一次
这也就是为什么我们会说Elasticsearch是近实时搜索了(文档的变化,会在一秒后可见,因此并不是实时的,是近实时的)
当然,Elasticsearch提供了refresh API可供用户手动执行refresh操作,例如发送请求/索引名/_refresh
即可
尽管刷新是比提交轻量很多的操作,它还是会有性能开销。
当写测试的时候,手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。
相反,我们的应用需要意识到 Elasticsearch 的近实时的性质,并接受它的不足
有的场景不需要每秒执行一次refresh(例如添加大量的日志文件到ES中),这该如何满足上述场景的需求呢?
我们可以通过设置索引的refresh_interval
来调整执行refresh操作的时间间隔
{"settings": {"refresh_interval": "30s" }
}
refresh_interval
可以在已存在的索引上进行动态更新。
在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来。
3:持久化变更 - translog的引入
上面说了Elasticsearch通过refresh操作将文档数据从内存缓存中写入文件系统缓存达到轻量化的查询机制
在这个过程中,将fsync系统调用移除了,如果没有用 fsync 把数据从文件系统缓存写入到硬盘(没有flush),就没有了持久化
Elasticsearch为了保证可靠性,就需要确保数据变更被持久化到磁盘,为了实现这个需求,Elasticsearch增加了translog
(事务日志)
来作为补偿机制,防止数据的丢失,在translog中记录了所有还未被持久化到磁盘的数据。
关于translog,需要弄明白下面三个问题:
- 什么时候写入数据到translog中 -> 当写入数据到内存缓存中后,就会追加一份数据到translog中
- 什么时候使用translog中的数据 -> 当Elasticsearch启动时,不仅会根据最新的一个提交点加载已持久化的段,还会根据translog中的数据,将未持久化的数据重新持久化到磁盘上
- 什么时候清理translog中的数据 -> 当文件系统缓存中的数据被flush到磁盘上后,就会删除旧的,并且生成一个新的空白的translog
默认每30分钟或者translog太大(默认为512MB)的时候会执行一次flush操作
通常情况下,自动刷新就足够了。当 Elasticsearch 尝试恢复或重新打开一个索引时,它需要重放 translog 中所有的操作
所以如果日志越短,恢复越快。可以通过index.translog.flush_threshold_size
配置参数来指定translog的最大容量
增加了translog后,Elasticsearch的写流程如下图所示:
虽然translog是用来防止数据丢失,但是也有数据丢失的风险。
可以看到,translog在内存缓存以及磁盘上都有一份,只有当内存中的translog通过fsync系统调用被flush到磁盘上后,才是可靠的。
执行translog的flush操作有两种模式——异步和同步,默认为同步模式
这个模式可以通过参数index.translog.durability
来进行调整
并且可以通过参数index.translog.sync_interval
来控制自动执行flush的时间间隔
#异步模式
index.translog.durability=async
#同步模式
index.translog.durability=request
同步模式 - 每次写请求之后就会执行一次fsync操作 - 获取200响应就是可靠
当处于同步模式时,默认会每次写请求之后就会执行一次fsync操作,这个过程在主分片和复制分片都会发生
这就意味着,在整个请求被fsync到主分片和复制分片的磁盘中的translog之前,客户端都不会得到一个200的响应。
即此模式下,写入请求成功,就表示着已经将本次的数据落盘到磁盘中的translog中了,这就保证了数据的可靠性。
异步模式 - 5秒执行一次fsync操作 - 在五秒之内断电,这部分数据就会丢失
当处于异步模式时,默认会5秒执行一次fsync操作,并且这个动作时异步的
这就意味着,即使你的写请求得到了200的响应,也并不代表着本次请求的数据已经落盘到磁盘中的translog中,即,本次操作并不可靠
es整体架构一览
4:段合并 - merge segement
4.1:段合并的流程
每秒执行的refresh[内存 -> OS cache]操作都会创建一个新的段,经过长时间的积累,索引中会存在大量的段
当段的数量过大时,不仅会占用过多的服务器资源,并且还会影响检索的性能
而前面已经说过,每次搜索时,会查询所有段中满足查询条件的数据,然后对每个段里查询的结果集进行合并,检索越慢。
Elasticsearch采用了段合并的方式来解决段数量过多的问题
在Elasticsearch中有一个后台进程专门负责段的合并(merge segement),它会定期执行段的合并操作。
- 将多个小的段合并成一个新的大的段,在合并时已删除的文档或被更新文档的旧版本不会被写入到新的段中
- 将新的段文件flush[OS cache -> 磁盘]写入磁盘
- 在该提交点[commit point]中标识所有新的段文件,并排除掉旧的和已经被合并的段文件
- 打开新的段文件用于搜索使用
- 等所有的检索请求都从小的段文件转到大的段文件上以后,删除旧的段文件
以上流程对于用户而言是透明的,Elasticsearch会在索引文档以及搜索文档时自动执行。
被合并的段可以是磁盘上已经提交过的索引,也可以在内存中还未提交的段,在合并的过程中,不会打断当前的索引和搜索功能。
4.2:段合并的性能影响
从上面的段合并的流程介绍,我们就可以看出,段合并的流程不仅涉及到段的读取、新的段的生成,还涉及到段的flush操作
因此,如果不对段合并加以控制,将会消耗大量的 I/O 和 CPU 资源,同时也会对搜索性能造成影响
在Elasticsearch中,默认地一次性只能合并十个段,并且段的容量大于5GB时不参与段合并,并且归并线程的默认配速为20MB/S
我们可以通过下面几个参数来对段合并的规则进行调整:
#更改配速为100MB/s
{"persistent" : {"indices.store.throttle.max_bytes_per_sec" : "100mb"}
}
#设置优先被合并的段的大小,默认为2MB
index.merge.policy.floor_segment
#设置一次最多合并的段数量,默认为10个
index.merge.policy.max_merge_at_once
#设置可被合并的段的最大容量,默认为5GB
index.merge.policy.max_merged_segment
4.3:再谈写流程
集群分片中的写流程
- 客户端发送写请求到协调节点
- 协调节点根据routing参数(如果没有指定,则默认是文档的id)进行路由计算,计算出该文档所属的主分片位置。
- 协调节点转发写请求到主分片所在节点。
- 主分片所在节点收到写请求后,就进入了单个节点的写流程。
- 主分片所在节点处理完写请求后,将写请求并行转发到其副本分片所在的所有节点,这些节点收到请求后,会做相同的处理。
- 所有的副本分片所在节点处理完写请求后,会将处理结果返回给主分片所在节点,主分片所在节点再将处理结果返回给协调节点。
- 协调节点返回结果给客户端。
单个节点的写流程
- 将数据写入内存缓存(index buffer)中
- 将数据追加到事务日志(translog)中
- 默认每秒执行一次refresh操作,将内存缓存中的数据refresh到文件系统缓存(OS Cache)中,生成段(segement),并打开该段供用户搜索,同时会清空内存缓存(index buffer)中的数据。
- 默认每次写入数据后通过fsync系统调用将内存中的translog写入(flush)到磁盘中。
- 同步模式是每次写入数据后都会fsync到磁盘
- 异步模式是每5秒fsync到磁盘
- 默认每30分钟或者translog大小超过512M后,就会执行一次flush将文件系统中的数据写入磁盘。
- 生成新的段(new segement)写入磁盘
- 生成一个新的包含新的段的提交点(commit point)写入磁盘
- 删除旧的translog,并生成新的translog
- Elasticsearch会开启归并进程,在后台对中小段进行段合并,减少索引中段的数目,该过程在文件系统缓存和磁盘中都会进行。
4.4:再谈读操作
集群分片中的读流程
-
客户端发读请求到协调节点
-
协调节点根据routing参数(如果没有指定,则默认是文档的id)进行路由计算(详见4.2.7小节),计算出该文档所属的分片位置。
-
使用round-robin随机轮询算法在该文档所属分片中任意选择一个(主分片或者副本分片),将请求转发到该分片所在节点。
-
该节点收到请求后,就进入单个节点的读流程。
-
该节点把查询结果返回给协调节点。
-
协调节点把查询结果返回给客户端。
单个节点的读流程
-
节点接收到读数据请求
-
根据请求中的doc id字段从translog缓存中查询数据,如果查询到数据则直接返回结果。
-
在第2步没有查到结果,从磁盘中的translog查询数据,如果查询到数据则直接返回结果。
-
在第3步没有查到结果,从磁盘中的各个段中查询结果,如果查到数据则直接返回结果。
-
经过前面的步骤,如果都没查到结果,则返回null。
Elasticsearch在读取数据时,会先尝试从translog中获取,再从segement中获取?
面我们讲了对所有文档的写入/修改/删除操作都会先被记录在translog中,然后再通过refresh、flush操作写入segament
因此,translog中会记录着最新的文档数据,所以如果从translog查到了目标数据,直接返回即可
如果没有,再去尝试从segament中获取。
5:搜索流程详解
5.1:四种searchType
这里的搜索流程,指的是search,注意要与上面介绍的读流程进行区分。
读流程是指拿着doc id去通过正排索引查找数据。search流程则与search的流程与searchType相关。
searchType的默认值是 Query then Fetch
searchType有四种,具体如下:
Query And Fetch
向索引的所有分片(shard)都发出查询请求,各分片返回的时候把元素文档(document)和计算后的排名信息一起返回
这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去shard查询一次。
但是各个shard返回的结果的数量之和可能是用户要求的size的n倍。
Query then Fetch(默认)
这种搜索模式分两个步骤。
- 向所有的shard发出请求,各分片只返回”足够“(预估是排序、排名以及分值相关)的信息(不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。
- 去相关的shard取document。这种方式返回的document与用户要求的size是相等的。
简而言之就是 -> 先通过倒排索引拿到doc id,然后再根据doc id通过正排索引查找数据
DFS Query And Fetch
这种方式比Query And Fetch方式多了一个initial scatter phrase步骤,有这一步,可以使评分精确度更高。
DFS Query Then Fetch
这种方式比Query Then Fetch方式多了一个initial scatter phrase步骤,有这一步,可以使评分精确度更高。
5.2:详解Query Then Fetch
Query Then Fetch模式下的搜索流程分为两个阶段 -> Query(查询阶段)和Fetch(获取阶段)
Query阶段
-
协调节点接收search请求后,广播该请求到所有分片上(包括主分片和副本分片)。
-
每个分片独立执行搜索,使用倒排索引进行匹配,根据匹配相关性构建出一个大小为from+size(from和size就是分页时的参数)的优先级排序结果队列(包含文档的id和所有参与排序的字段的值,比如_score)。
-
在该阶段会查询OS Cache中的segament缓存,此时有些数据可能还在Memory中,因此Elasticsearch是近实时搜索。
-
每个分片将其优先级排序结果队列返回给协调节点。
-
协调节点创建一个新的优先级排序结果队列,并对全局结果进行排序,得到一个排序结果列表(包含所有排序的字段值、文档id)。
-
进入Fetch阶段。
Fetch阶段
- 协调节点根据排序结果列表,向相关的分片提交多个 GET 请求。
- 每个分片收到GET请求后,执行上面介绍过的读流程,根据文档id获取详细的文档信息,并返回给协调节点。
- 协调节点返回结果给客户端。
四:面试相关
1:Text和KeyWord的区别(重)
text类型
当一个字段是要被全文搜索的,例如Email,那么这个字段应该使用text类型
设置text类型之后,字段内容会被分析,在生成倒排索引之前,会被分析器分成一个一个词项
text字段类型的字段不用于排序,很少用于聚合
对于text字段由如下注意事项:
- 使用于全文检索,例如match查询
- 文本字段会被分词
- 默认情况下,会创建倒排索引
- 自动映射器会为Text类型创建KeyWord字段
- 不用于排序,很少用于聚合
Keyword类型
KeyWord类型适用于不分词的字段,如姓名,ID,数字等
如果数字类型不用范围查找,那么keyword的性能要高于数值类型
当使用keyword类型进行查询的时候,其字段值会被认为是一个整体,并保留字段值的原始属性
常用于过滤,排序,聚合
GET test_index/_search
{"query": {"match": {"title.keyword": "测试一下"}}
}
对于keyword类型,有如下注意事项:
- 不会对文本分词,会保留字段的原有属性,包括大小写等等
- 仅仅是字段类型,不会对搜索词产生任何的影响
- 一般用于需要精确查找的字段,或者聚合排序的字段
- 通常和term一起使用【DSL】
ignore_above
参数代表其切断长度,默认是256,如果超过长度,字段值会忽略,而不是截断
2:B+树为什么不适合做全文检索
3:ES集群脑裂问题
3.1:脑裂成因
网络问题
集群间的网络延迟导致一些节点访问不到 master,认为 master 挂掉了从而选举出新的 master,并对 master 上的分片和副本标红,分配新的主分片
节点负载
主节点的角色既为 master 又为 data,访问量较大时可能会导致 ES 停止响应造成大面积延迟
此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点
内存回收
data 节点上的 ES 进程占用的内存较大,引发 JVM 的大规模内存回收,造成 ES 进程失去响应
3.2:解决方案
- 减少误判
discovery.zen.ping_timeout
节点状态的响应时间,默认为 3s,可以适当调大- 如果 master 在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。
- 调大参数(如 6s,
discovery.zen.ping_timeout:6
),可适当减少误判
- 选举触发
- 修改参数为1,
discovery.zen.minimum_master_nodes:1
- 该参数是用于控制选举行为发生的最小集群主节点数量。
- 当备选主节点的个数大于等于该参数的值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。
- 官方建议为
(n/2) + 1
,n 为主节点个数(即有资格成为主节点的节点个数)
- 修改参数为1,
- 角色分离
- 即 master 节点与 data 节点分离,限制角色
- 主节点配置为:
node.master: true node.data: false
- 从节点配置为:
node.master: false node.data: true
4:ES的性能问题
分几个方向说几个点:硬件配置优化包括三个因素:CPU、内存和 IO。
CPU
- 大多数 Elasticsearch 部署往往对 CPU 要求不高;
- CPUs 和更多的核数之间选择,选择更多的核数更好。
- 多个内核提供的额外并发远胜过稍微快一点点的时钟频率。
内存
- 配置: 由于 ES 构建基于 lucene,而 lucene 设计强大之处在于 lucene 能够很好的利用操作系统内存来缓存索引数据,以提供快速的查询性能。lucene 的索引文件 segements 是存储在单文件中的,并且不可变,对于 OS 来说,能够很友好地将索引文件保持在 cache 中,以便快速访问;因此,我们很有必要将一半的物理内存留给 lucene;另一半的物理内存留给 ES(JVM heap)。
- 禁止 swap 禁止 swap,一旦允许内存与磁盘的交换,会引起致命的性能问题。可以通过在 elasticsearch.yml 中 bootstrap.memory_lock: true,以保持 JVM 锁定内存,保证 ES 的性能。
- 垃圾回收器: 已知JDK 8附带的HotSpot JVM的早期版本存在一些问题,当启用G1GC收集器时,这些问题可能导致索引损坏。受影响的版本早于JDK 8u40随附的HotSpot版本。如果你使用的JDK8较高版本,或者JDK9+,我推荐你使用G1 GC; 因为我们目前的项目使用的就是G1 GC,运行效果良好,对Heap大对象优化尤为明显。
磁盘
- 在经济压力能承受的范围下,尽量使用固态硬盘(SSD)
linux部署经验
- 64GB 内存的机器是非常理想的,但是 32GB 和 16GB 机器也是很常见的。少于 8GB 会适得其反
- 如果你要在更快的 CPUs 和更多的核心之间选择,选择更多的核心更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率
- 如果你负担得起 SSD,它将远远超出任何旋转介质。基于 SSD 的节点,查询和索引性能都有提升。如果你负担得起,SSD 是一个好的选择
- 即使数据中心们近在咫尺,也要避免集群跨越多个数据中心。绝对要避免集群跨越大的地理距离
- 请确保运行你应用程序的 JVM 和服务器的 JVM 是完全一样的。在 Elasticsearch 的几个地方,使用 Java 的本地序列化
- 通过设置 gateway.recover_after_nodes、gateway.expected_nodes、gateway.recover_after_time 可以在集群重启的时候避免过多的分片交换,这可能会让数据恢复从数个小时缩短为几秒钟
- Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。最好使用单播代替组播
- 不要随意修改垃圾回收器(CMS)和各个线程池的大小
- 把你的内存的(少于)一半给 Lucene(但不要超过 32GB),通过 ES_HEAP_SIZE 环境变量设置
- 内存交换到磁盘对服务器性能来说是致命的。如果内存交换到磁盘上,一个 100 微秒的操作可能变成 10 毫秒。 再想想那么多 10 微秒的操作时延累加起来。 不难看出 swapping 对于性能是多么可怕
- Lucene 使用了大量的文件。同时,Elasticsearch 在节点和 HTTP 客户端之间进行通信也使用了大量的套接字。 所有这一切都需要足够的文件描述符。你应该增加你的文件描述符,设置一个很大的值,如 64000
索引提升的方法
- 使用批量请求并调整其大小:每次批量数据 5MB – 15MB 大是个不错的起始点
- 存储:使用 SSD
- 段和合并:Elasticsearch 默认值是 20MB/s,对机械磁盘应该是个不错的设置。如果你用的是 SSD,可以考虑提高到 100 – 200MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。另外还可以增加
index.translog.flush_threshold_size
设置,从默认的 512MB 到更大一些的值,比如 1GB,这可以在一次清空触发的时候在事务日志里积累出更大的段 - 如果你的搜索结果不需要近实时的准确度,考虑把每个索引的
index.refresh_interval
改到 30s - 如果你在做大批量导入,考虑通过设置
index.number_of_replicas: 0
关闭副本