五、分布式特性及分布式搜索的机制
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
会重新选举Master
Node1
自己还是作为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
有两种实现方法Fielddata
Doc 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
- 需要全部文档,例如导出全部数据
Pagination
From
和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核心技术与实战》,这只是我做的笔记,仅供参考;