五、分布式特性及分布式搜索的机制
1. 集群分布式模型及选主与脑裂问题
我们知道ES天生就是分布式架构
1.1 分布式特性
ElasticSearch的分布式架构带来的好处- 存储的水平扩容,支持
PB级数据 - 提高系统的可用性,部分节点停止服务,整个集群的服务不受影响
- 存储的水平扩容,支持
ElasticSearch的分布式架构- 不同的集群通过不同的名字来区分,默认名字
elasticsearch - 通过配置文件修改,或者在命令行中
-E cluster.name=geektime进行设定
- 不同的集群通过不同的名字来区分,默认名字
1.2 ElasticSearch集群的节点
- 节点是一个
ElasticSearch的实例- 其本质上就是一个
JAVA进程 - 一台机器上可以运行多个
ElasticSearch进程,但是生产环境一般建议一台机器上就运行一个ElasticSearch实例
- 其本质上就是一个
- 每一个节点都有名字,通过配置文件配置,或者启动时候
-E node.name=geektime指定 - 每一个节点在启动之后,会分配一个
UID,保存在data目录下
1.3 Coordinating Node节点
- 我们会把请求打到节点上,处理请求的节点就叫
Coordinating Node- 路由请求到正确的节点,例如创建索引的请求,需要路由到
Master节点
- 路由请求到正确的节点,例如创建索引的请求,需要路由到
- 所有节点默认都是
Coordinating Node - 通过将其他类型设置成
False,使其成为Dedicated Coordinating Node
1.4 Data Node节点
- 可以保存数据的节点,就叫
Data Node- 节点启动后,默认就是数据节点。可以设置
node.data.false禁止
- 节点启动后,默认就是数据节点。可以设置
Data Node的职责- 保存分片数据。在数据扩展上起到了至关重要的作用(由
Master Node决定如何把分片分发到数据节点上)
- 保存分片数据。在数据扩展上起到了至关重要的作用(由
- 通过增加数据节点
- 可以解决数据水平扩展和解决数据单点问题
1.5 Master Node节点
Master Node的职责- 处理创建、删除索引等请求/决定分片被分配到哪个节点/负责索引的创建与删除
- 维护并且更新
Cluster State
Master Node的最佳实践Master节点非常重要,在部署上需要考虑解决单点的问题- 为一个集群设置多个
Master节点/每个节点只承担Master的单一角色
1.6 Master Eligible Nodes & 选主流程
- 一个集群,支持配置多个
Master Eligible节点。这些节点可以在必要时(如Master节点出现故障,网络故障时)参与选主流程,成为Master节点 - 每个节点启动后,默认就是一个
Master eligible节点- 可以设置
node.master: false禁止
- 可以设置
- 当集群内第一个
Master eligible节点启动时候,它会将自己选举成Master节点
1.6.1 Master Eligible Nodes 节点选主的过程
- 互相
ping对方,Node Id低的会成为被选举的节点 - 其他节点会加入集群,但是不承担
Master节点的角色。一旦发现被选中的主节点丢失,就会选举出新的Master节点
1.7 集群信息
- 集群状态信息(
Cluster State),维护了一个集群中,必要的信息- 所有的节点信息
- 所有的索引和其相关的
Mapping与Setting信息 - 分片的路由信息
- 在每个节点上都保存了集群的状态信息
- 但是,只有
Master节点才能修改集群的状态信息,并负责同步给其他节点- 因为,任意节点都能修改信息会导致
Cluster State信息不一致
- 因为,任意节点都能修改信息会导致
1.8 脑裂问题
Split-Brain,分布式系统的经典问题,当出现网络问题,一个节点和其他节点无法连接Node2和Node3会重新选举MasterNode1自己还是作为Master,组成一个集群,同时更新Cluster State- 导致集群中有 2 个
Master,维护不同的Cluster State。当网络恢复时,无法选择正确恢复
1.8.1 如何避免脑裂问题
- 限定一个选举条件,设置
quorum(仲裁),只有在Master eligible节点数大于quorum(仲裁)时,才能进行选举Quorum = (Master节点总数 / 2) + 1- 当3个
Master eligible时,设置discovery.zen.minimum_master_nodes为2,即可避免脑裂
- 从 7.0 开始,无需这个配置
- 移除
minimum_master_nodes参数,让Elasticsearch自己选择可以形成仲裁的节点 - 典型的主节点选举现在只需要很短的时间就可以完成。集群的伸缩变得更安全、更容易,并且可能造成丢失数据的系统配置选项更少了
- 节点更清楚的记录他们的状态,有助于诊断为什么他们不能加入集群或为什么无法选举出主节点
- 移除
1.9 配置节点类型
一个节点默认情况下是一个Master eligible Node,data Node,ingest Node
| 节点类型 | 配置参数 | 默认值 |
|---|---|---|
master eligible | node.master | true |
data | node.data | true |
ingest | node.ingest | true |
coordinating only | 无 | 设置上面三个参数全部为false |
machine learing | node.ml | true(需要enable-x-pack) |
2. 分片与集群的故障转移
2.1 Primary Shard(主分片) - 提升系统存储容量
- 分片是
ElasticSearch分布式存储的基石- 主分片/副本分片
- 通过主分片,将数据分布在所有节点上
Primary Shard,可以将一份索引的数据,分散在多个Data Node上,实现存储的水平扩展- 主分片(
Primary Shard)数在索引创建时候指定,后续默认不能修改,如要修改,需重建索引
2.2 Replica Shard(副本分片) - 提高数据可用性
- 数据可用性
- 通过引入副本分片(
Replica Shard)提高数据的可用性。一旦主分片丢失,副本分片可以Promote成主分片。副本分片数可以动态调整。整个节点上都有完备的数据。如果不设置副本分片,一旦出现节点硬件故障,就有可能造成数据丢失
- 通过引入副本分片(
- 提升系统的读取性能
- 副本分片由主分片同步。通过支持增加
Replica个数,一定程度可以提高读取的吞吐量
- 副本分片由主分片同步。通过支持增加
2.3 分片数的设定
- 如果规划一个索引的主分片数和副本分片数
- 主分片数过小:例如创建了 1 个
Primary Shard的Index- 如果该索引增长很快,集群无法通过增加节点实现对这个索引的数据扩展
- 主分片数设置过大:导致单个
Shard容量很小,引发一个节点上有过多分片,影响性能 - 副本分片数设置过多,会降低集群整体的写入性能
- 主分片数过小:例如创建了 1 个
2.4 集群节点问题
2.4.1 单节点集群
- 副本无法分片,集群状态为黄色(
Yellow) - 我们可以增加一个数据节点的方式,来让副本分片可以分配到第二个节点上
2.4.2 增加一个数据节点
- 集群状态转为绿色
- 集群具备故障转移能力
- 尝试着将
Replica设置成2和3,查看集群的状况
2.4.3 再增加一个数据节点
- 集群具备故障转移能力
Master节点会决定分片分配到哪个节点- 通过增加节点,提高集群的计算能力
2.4.4 故障转移
- 3个节点共同组成一个集群。包含了一个索引,索引设置了3个
Primary Shard和1个Replica Shard - 节点1是
Master节点,节点意外出现故障。集群重新选举Master节点 Node3上的R0提升成P0,集群变黄R0和R1分配结束之后,集群变绿
2.5 集群健康状态
Green: 健康状态,所有主分片和副本分片都可用Yellow: 亚健康,所有的主分片可用,部分副本分片不可用Red: 不健康状态,部分主分片不可用
3. 分片及其生命周期
3.1 分片的内部原理
- 什么是
ES的分片ES中最小的工作单元/是一个Lucene的Index
- 一些问题:
- 为什么
ES的搜索是近实时的(1秒后被搜索到) ES如何保证在断电时数据不会丢失- 为什么删除文档,并不会立刻释放空间
- 为什么
3.2 倒排索引不可变性
- 倒排索引采用
Immutable Design,一旦生成,不可更改 - 不可变性,带来了的好处如下:
- 无需考虑并发写文件的问题,避免了锁机制带来的性能问题
- 一旦读入内核的文件系统缓存,便留在哪里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能
- 缓存容易生成和维护 / 数据可以被压缩
- 不可变性,带来的挑战:如果需要让一个新的文档可以被搜索到,需要重建整个索引
3.3 Lucene Index
- 在
Lucene中,单个倒排索引文件被称为Segment。Segment是自包含的,不可变更的。多个Segments汇总在一起,称为Lucene的Index,其对应的就是ES中的Shard - 当有新文档写入时,会生成新的
Segment,查询时会同时查询所有的Segment,并且对结果汇总。Lucene中有一个文件,用来记录所有Segments的信息,叫做Commit Point - 删除的文档信息,保存在
.del文件中
3.4 什么是Refresh
ES写入文档时,并不是直接将文档写入Segment,而是先写入一个叫做Index Buffer的缓冲区,再由缓冲区写入Segment
- 将
Index buffer写入Segment的过程叫Refresh。Refresh不执行fsync操作 Refresh频率:默认1s发生一次,可通过index.refresh_interval配置。Refresh后,数据就可以被搜索到了。这也是为什么ElasticSearch被称为近实时搜索- 如果系统有大量的而数据写入,那就会产生很多
Segment Index Buffer被占满时,会触发Refresh,默认值是JVM的10%
3.5 什么是Transaction Log
Segment写入磁盘的过程相对耗时,借助文件系统缓存,Refresh时,先将Segment写入缓存以开放查询- 为了保证数据不会丢失。所以在
Index文档时,同时写Transaction Log,高版本开始,Transaction Log默认落盘。每个分片有一个Transaction Log - 在
ES进行Refresh时,Index Buffer被清空,Transaction log不会清空
3.6 什么是Flush
ES Flush&Lucene Commit- 调用
Refresh,Index Buffer清空并且Refresh - 调用
fsync,将缓存中的Segments写入磁盘 - 清空(删除)
Transaction Log - 默认 30 min 调用一次
Transaction Log满(默认512MB)
- 调用
3.7 Merge操作
随着Flush(上面3.6)操作的进行,Segment中的内容被不断写入磁盘,随着时间的流逝,磁盘上的Segment文件也会越来越多,所以呢我们需要对这些Segment文件做一个定期的处理
ES和Lucene会自动进行Merge操作POST my_index/_forcemerge
Segment很多,需要定期被合并- 减少
Segments/ 删除已经删除的文档
- 减少
4. 剖析分布式查询及相关性算分
4.1 分布式搜索的运行机制
ElasticSearch的搜索,会分两阶段进行- 第一阶段:
Query - 第二阶段:
Fetch
- 第一阶段:
Query-then-Fetch
4.2 Query阶段
- 用户发出搜索请求到
ES节点。节点收到请求后,会以Coordinating(协调节点)节点的身份,在6个主副分片中随机选择3个分片,发送查询请求
这里说的有些问题,并不是随机,看看这篇博客www.elastic.co/guide/cn/el…
- 被选中的分片执行查询,进行排序。然后,每个分片都会返回
From + Size个排序后的文档Id和排序值给Coordinating节点
4.3 Fetch阶段
Coordinating Node会将Query阶段,从每个分片获取的排序后的文档Id列表,重新进行排序。选取From到From + Size个文档的Id- 以
Multi get请求的方式,到相应的分片获取详细的文档数据
4.4 Query Then Fetch潜在的问题
- 性能问题
- 每个分片上需要查的文档个数 =
from + size - 最终协调节点需要处理:
number_of_shard*(from+size) - 深度分页
- 每个分片上需要查的文档个数 =
- 相关性算分
- 每个分片都基于自己的分片上的数据进行相关度计算。这会导致打分偏离的情况,特别是数据量很少时。相关性算分在分片之间是相互独立。当文档总数很少的情况下,如果主分片大于1,主分片数越多,相关性算分会越不准
4.5 解决算分不准的方法
- 数据量不大的时候,可以将主分片数设置为 1
- 当数据量足够大的时候,只要保证文档均匀分散在各个分片上,结果一般就不会出现偏差
- 使用
DFS Query Then Fetch- 搜索的
URL中指定参数_search?search_type=dfs_query_then_fetch - 到每个分片把各个分片的词频和文档频率进行搜索,然后完整的进行一次相关性算分,耗费更加多的
CPU和内存,执行性能低下,一般不建议使用
- 搜索的
5. 排序及Doc Values & Fielddata
默认情况下,ElasticSearch会对算分进行排序,我们可以指定sort参数,来自行指定排序的规则
5.1 排序
ElasticSearch默认采用相关性算分对结果进行降序排序- 可以通过设定
sorting参数,自行设定排序 - 如果不指定
_score,算分为null
5.2 例子
5.2.1 单字段排序
我们来看一个例子
# 单字段排序
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{
"order_date": {
"order": "desc"
}
}
]
}
我们按照订单的时间来降序排序
5.2.2 多字段排序
# 多字段排序
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{
"order_date": {
"order": "desc"
}
},
{
"_doc": {
"order": "asc"
}
},
{
"_score": {
"order": "desc"
}
}
]
}
5.2.3 对Text类型的字段做排序
我们发现报错了,报错提示我们需要开启Fielddata属性
# 打开Text字段的 fielddata
PUT kibana_sample_data_ecommerce/_mapping
{
"properties": {
"customer_full_name": {
"type": "text",
"fielddata": true,
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
5.3 排序的过程
- 排序是针对字段原始内容进行的。倒排索引无法发挥作用;
- 需要用到正排索引。通过文档
Id和字段快速得到字段原始内容 ElasticSearch有两种实现方法FielddataDoc Values(列式存储,对Text类型无效)(这也就是我们对Text类型字段排序时,它让我们开启Fielddata的原因)
5.3.1 Doc Values VS Field Data
| Doc Values | Field data |
|---|---|---|
| 何时创建 | 索引时,和倒排索引一起创建 | 搜索时候动态创建 |
| 创建位置 | 磁盘文件 | JVM Heep |
| 优点 | 避免大量内存占用 | 索引速度快,不占用额外的磁盘空间 |
| 缺点 | 降低索引速度,占用额外磁盘空间 | 文档过多,动态创建开销大,占用过多JVM Heep |
| 缺省值 | ES 2.x 之后 | ES 1.x及之前 |
5.3.2 关闭Doc Values
- 默认启用,可以通过
Mapping设置关闭- 增加索引的速度/减少磁盘空间
- 如果重新打开,需要重建索引
- 什么时候需要关闭
- 明确不需要做排序及聚合分析
6. 分页与遍历:From,Size,Search After & Scroll API
6.1 From/Size
- 默认情况下,查询按照相关度算分排序,返回前10条记录
- 容易理解的分页方案
From: 开始位置Size: 期望获取文档的总数
6.2 分布式系统中深度分页的问题
ES天生就是分布式的。查询信息,但是数据分别保存在多个分片,多台机器上,ES天生就需要满足排序的需要(按照相关性算分)- 当一个查询:
From=990,Size=10- 会在每个分片上先都获取1000个文档。然后,通过
Coordinating Node聚合所有结果。最后再通过排序选取前1000个文档 - 页数越深,占用内存越多。为了避免深度分页带来的内存开销。
ES有一个设定,默认限定到10000个文档Index.max_result_window
- 会在每个分片上先都获取1000个文档。然后,通过
6.3 简单的分页例子
# 简单的分页例子
POST tmdb/_search
{
"from": 0,
"size": 20,
"query": {
"match_all": {}
}
}
- 当最大值超过10000,ES就会报错
6.4 Search After 避免深度分页的问题
我们知道,当我们的页数越深,占用内存就会越多,那如果来避免深度分页的问题呢?
Search After能避免深度分页的性能问题,可以实时获取下一页文档信息- 不支持指定页数(
From) - 只能往下翻
- 不支持指定页数(
- 第一步搜索需要指定
sort,并且保证值是唯一的(可以通过加入_id保证唯一性) - 然后使用上一次,最后一个文档的
sort值进行查询
6.4.1 Search After的例子
1. 数据准备
# Search After
DELETE users
POST users/_doc
{"name": "user1","age":10}
POST users/_doc
{"name": "user2","age":11}
POST users/_doc
{"name": "user3","age":12}
POST users/_doc
{"name": "user4","age":13}
2. 使用Search After查询(第一步搜索需要指定sort,并且保证值是唯一的(可以通过加入_id保证唯一性))
POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"sort": [
{"age": "desc"},
{"_id": "asc"}
]
}
搜索需要指定
sort,并且保证值是唯一的(可以通过加入_id保证唯一性),这里的条件满足
3. 再次使用Search After查询(然后使用上一次,最后一个文档的sort值进行查询)
# 然后使用上一次,最后一个文档的`sort`值进行查询
POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"search_after": [
13,
"ka36BngBc4LlHCXeCOxJ"
],
"sort": [
{"age": "desc"},
{"_id": "asc"}
]
}
然后以此类推,每次填入上一次搜索的
sort值,直到查询结果为空
6.4.2 Search After是如何解决深度分页的问题
- 假定
Size是10 - 当查询 990-1000 时
- 通过唯一排序值定位,将每次要处理的文档数都控制在10
6.5 Scroll API(实际场景用的多)
Scroll API也是ElasticSearch提供的一个对结果进行遍历的API,它会在调用的第一次,指定一个Scroll存活的时间,在这个存活的时间之内,我们可以对查询到的结果进行处理,处理完后,我们再输入上一次查询的Scroll Id来滚动查询后面的数据。
它也有一定的局限性,因为这个请求相当于创建了一个快照,有新的数据写入以后,无法被查到
- 创建一个快照,有新的数据写入以后,无法被查到
- 每次查询后,输入上一次的
Scroll Id
6.5.1 Scroll API的简单Demo
1. 插入3条数据
DELETE users
POST users/_doc
{"name": "user1","age":10}
POST users/_doc
{"name": "user2","age":20}
POST users/_doc
{"name": "user3","age":30}
POST users/_count
2. 基于 Scroll API 创建快照
# 基于 scroll api 创建快照
# DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAFdgWOFN4NEJyd05ScnFmVjRzM3M2dzdvUQ==
POST /users/_search?scroll=5m
{
"size": 1,
"query": {
"match_all": {}
}
}
3. 尝试再写一个文档
POST users/_count
POST users/_doc
{"name": "user4","age":40}
4. 滚动执行Scroll API
POST /_search/scroll
{
"scroll": "1m",
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAFdgWOFN4NEJyd05ScnFmVjRzM3M2dzdvUQ=="
}
将上一步我们保存的
scroll_id写入这个scroll_id中,我们就能拿到下一条数据,这里我们的size是1,我们每次只能拿一个
当我们依次执行,到最后,发现拿不到我们中途插入的数据
6.6 不同的搜索类型和使用场景
Regular- 需要实时获取顶部的部分文档。例如查询最新的订单
Scroll- 需要全部文档,例如导出全部数据
PaginationFrom和Size- 如果需要深度分页,则选用
Search After
7. 处理并发读写操作
7.1 并发控制的必要性
- 两个
Web程序同时更新某个文档,如果缺乏有效的并发,会导致更改的数据丢失 - 悲观并发控制
- 假定有变更冲突的可能。会对资源加锁,防止冲突。例如数据库行锁
- 乐观并发控制
- 假定冲突是不会发生的,不会阻塞正在尝试的操作。如果数据在读写中被修改,更新将会失败。应用程序决定如何解决冲突,例如重试更新,使用新的数据,或者将错误报告给用户
ES采用的是乐观并发控制
7.2 ES的乐观并发控制
ES中的文档是不可变更的。如果你更新一个文档,会将旧文档标记为删除,同时增加一个全新的文档。同时文档的version字段加1- 内部版本控制
if_seq_no + if_primary_term
- 使用外部版本(使用其他数据库作为主要数据存储)
version + version_type = external
学习地址为极客时间《Elasticsearch核心技术与实战》,这只是我做的笔记,仅供参考;