第05章-分布式特性介绍
# 一、ES 分布式概述
# 1.1 ES 集群介绍
es 支持集群模式,是一个分布式系统,其好处主要有两个:
- 增大系统容量,如内存、磁盘,使得 es 集群可以支持 PB 级的数据
- 提高系统可用性,即使部分节点停止服务,整个集群依然可以正常服务
es 集群由多个 es 实例组成
- 不同集群通过集群名字来区分,可通过
clustername
进行修改,默认为 elasticsearch - 每个 es 实例本质上是一个 JVM 进程,且有自己的名字,通过
node.name
进行修改
Cerebro 是一个可以查看es集群管理工具,地址:https://github.com/lmenezes/cerebro
# 1.2 ES 集群节点
启动一个节点
运行如下命令可以启动一个 es 节点实例
bin/elasticsearch -Eclustername=my_cluster -Enode.name=node1
可以启动多个节点实例构建一个 es 集群
# 1.2.1 节点状态 Cluster State
es 集群相关的数据称为 cluster state
,主要记录如下信息:
- 节点信息,比如节点名称、连接地址等
- 索引信息,比如索引名称、配置等
- ……
# 1.2.2 主节点 Master Node
- 可以修改 cluster state 的节点称为 master节点,一个集群只能有一个
- cluster state 存储在每个节点上,master 维护最新版本并同步给其他节点
- master 节点是通过集群中所有节点选举产生的,可以被选举的节点称为 master-eligible节点,相关配置为:
-node.master:true
# 1.2.3 协调节点 Coordinating Node
- 处理请求的节点即为 coordinating 节点,该节点是所有节点的默认角色,不能取消
- 作用是路由请求到正确的节点处理,比如创建索引的请求到 master 节点
# 1.2.4 数据节点 Data Node
存储数据的节点即为 data 节点,默认节点都是 data 类型,相关配置为:node.data:true
# 二、副本与分片
# 2.1 副本
提高系统可用性,包括以下两个方面
- 服务可用性:2 个节点的情况下,允许其中 1 个节点停止服务
- 数据可用性:引入 副本(Replication) 解决,每个节点上都有完备的数据
# 2.2 分片
如何将数据分布于所有节点上?es 通过引入 分片(Shard) 解决问题
- 分片是 es 支持 PB 级数据的基石
- 分片存储了部分数据,可以分布于任意节点上
- 分片数在索引创建时指定且后续不允许再更改,默认为 5 个
- 分片有主分片和副本分片之分,以实现数据的高可用
- 副本分片的数据由主分片同步,可以有多个,从而提高读取的吞吐量
下图演示的是 3 个节点的集群中 test_index 的分片分布情况,创建时我们指定了 3 个分片和 1 个副本,api 如下所示:
# 2.3 副本与分片的思考
基于上图,思考两个问题:
问:此时增加节点是否能提高 test_index 的数据容量?
答:不能。因为只有 3 个分片,已经分布在 3 台节点上,新增的节点无法利用。
问:此时增加副本数是否能提高 test_index 的读取吞吐量?
答:不能。因为新增的副本也是分布在这 3 个节点上,还是利用了同样的资源。如果要增加吞吐量,还需要新增节点。
总结:分片数的设定很重要,需要提前规划好
- 过小会导致后续无法通过增加节点实现水平扩容
- 过大会导致一个节点上分布过多分片,造成资源浪费,同时会影响查询性能
# 三、集群状态与故障转移
# 3.1 健康状态
通过如下 api 可以查看集群健康状况,包括以下三种:
GET _cluster/health
- green:健康状态,指所有主副分片都正常分配
- yellow:指所有主分片都正常分配,但是有副本分片未正常分配
- red:有主分片未分配(比如磁盘空间不足)
集群处于 red 状态并不意味着不能对外提供服务,是可以正常访问的
# 3.2 故障转移
集群由 3 个节点组成,如下所示,此时集群状态是 Green。
node1 所在机器宕机导致服务终止,此时集群会如何处理?
node 之间会互相 ping 检查节点状态,node2 和 node3 发现 node1 无法响应一段时间后,会发起 master 选举,比如这里选择 node2 为 master 节点。此时由于主分片 P0 下线,集群状态变为 Red。
node2 发现主分片 P0 未分配,将 R0 提升为主分片。此时由于所有主分片都正常分配,集群状态变为 Yellow。
node2 为 P0 和 P1 生成新的副本,集群状态变为 Green。
# 四、文档分布式存储
# 4.1 文档映射算法
文档最终会存储在分片上。如下图所示,Document1 最终存储在分片 P1 上
Document1 是如何存储到分片 P1 的?选择 P1 的依据是什么?
答案:有一个文档到分片的映射算法
目的:使得文档均匀分布在所有分片上,以充分利用资源
算法:是否可以随机选择或者 round-robin 算法?不可取,因为需要维护文档到分片的映射关系,成本巨大。应该根据文档值实时计算对应的分片
映射算法:
es 通过如下的公式计算文档对应的分片
shard = hash(routing) % number_of_primary_shards
- hash 算法保证可以将数据均匀地分散在分片中
- routing 是一个关键参数,默认是文档 id,也可以自行指定
- number_of_primary_shards 是主分片数
- 该算法与主分片数相关,这也是 分片数一旦确定后便不能更改 的原因
# 4.2 文档的读写流程
# 4.2.1 文档创建流程
# 4.2.2 文档读取流程
# 4.2.3 文档批量创建流程
# 4.2.4 文档批量读取流程
# 五、脑裂问题
脑裂问题,英文为 split-brain,是分布式系统中的经典网络问题,如下图所示:
- node2 与 node3 会重新选举 master,比如 node2 成为了新 master,此时会更新 cluster state
- node1 自己组成集群后,也会更新 cluster state
- 同一个集群有两个 master,而且维护不同的 cluster state,网络恢复后无法选择正确的 master
解决方案为仅在可选举 master-eligible 节点数大于等于 quorum 时才可以进行 master 选举
quorum = master-eligible节点数 / 2 + 1
,例如 3 个 master-eligible 节点时, quorum 为 2- 配置
discovery.zen.minimum_master nodes
为 quorum 即可避免脑裂
# 六、shard 详解
# 6.1 倒排索引不可变
倒排索引一旦生成,不能更改,其好处如下:
- 不用考虑并发写文件的问题,杜绝了锁机制带来的性能问题
- 由于文件不再更改,可以充分利用文件系统缓存,只需载入一次,只要内存足够,对该文件的读取都会从内存读取,性能高
- 利于生成缓存数据
- 利于对文件进行压缩存储,节省磁盘和内存存储空间
坏处为需要写入新文档时,必须重新构建倒排索引文件,然后替换老文件后,新文档才能被检索,导致文档实时性差。查询流程如下图:
# 6.2 搜索实时性方案
如何解决文档搜索实时性问题呢?
解决方案是新文档直接生成新的倒排索引文件,查询的时候同时查询所有的倒排文件,然后做结果的汇总计算即可。查询流程如下图:
- Lucene 便是采用了这种方案,它构建的单个倒排索引称为
segment
,合在一起称为Index
,与 ES 中的 Index 概念不同。ES 中的一个Shard
对应一个Lucene Index
。 - Lucene 会有一个专门的文件来记录所有的
segment
信息,称为commit point
# 6.3 文档搜索实时性操作
# 6.3.1 refresh
segment
写入磁盘的过程依然很耗时,可以借助文件系统缓存的特性,先将segment
在缓存中创建并开放查询来进一步提升实时性,该过程在 es 中被称为refresh
。- 在
refresh
之前文档会先存储在一个buffer
中,refresh
时将buffer
中的所有文档清空并生成内存segment
- es 默认每 1 秒执行一次 refresh,refresh 之后文档就可以被搜索到了,因此文档的实时性被提高到 1 秒,这也是 es 被称为 准实时(Near Real Time) 的原因
# 6.3.2 translog
如果在内存中的 segment 还没有写入磁盘前发生了宕机,那么其中的文档就无法恢复了,如何解决这个问题?
- es 引入
translog
机制。写入文档到buffer
时,同时将该操作写入translog
。 translog
文件会即时写入磁盘(fsync
),6.x 默认每个请求都会落盘,可以修改为每 5 秒写一次,这样风险便是丢失 5 秒内的数据,相关配置为index.translog
。- es 启动时会检查
translog
文件,并从中恢复数据。
# 6.3.3 flush
flush 负责将内存中的 segment 写入磁盘,主要做如下的工作:
- 将 translog 写入磁盘,生成 translog file
- 将 index buffer 清空,其中的文档生成一个新的内存 segment,相当于一个 refresh 操作
- 执行 fsync 操作,将内存中的 segment 写入磁盘,生成磁盘 segment
- 更新 commit point,记录新生成的 segment
- 删除旧的 translog file 和内存中的 segment
# 6.4 实时性操作发生时机
refresh 发生的时机主要有如下几种情况:
- 间隔时间达到时,通过
index.settings.refresh interval
来设定,默认是 1 秒 - index.buffer 占满时,其大小通过
indices.memory.index_buffer_size
设置,默认为 jvm heap 的 10%,所有 shard 共享 - flush 发生时也会发生 refresh
flush 发生的时机主要有如下几种情况:
- 间隔时间达到时,默认是 30 分钟,5.x 之前可以通过
index.translog.flush threshold period
修改,之后版本无法修改 - translog 占满时,其大小可以通过
index.translog.flush threshold _size
控制,默认是 512mb,每个 index 有自己的 translog
# 6.5 删除与更新文档
文档搜索实时性—删除与更新文档
segment 一旦生成就不能更改,那么如果你要删除文档该如何操作?
Lucene 专门维护一个
.del
的文件,记录所有已经删除的文档,注意.del
上记录的是文档在 Lucene 内部的 id,在查询结果返回前会过滤掉.del
中的所有文档更新文档如何进行呢?
首先删除文档,然后再创建新文档
# 6.6 Shard 整体结构
ES Index 与 Lucene Index 的术语对照如下所示:
ES 索引分为好几个 Shard,每个 Shard 对应一个 Lucene Index,Lucene Index 由一堆 Segment 组成,还有一个 .del 文件记录已经删除的文档,有一个 Commit Point 文件来维护所有的 Segment 和 .del 文件。
# 6.7 Segment Merging
Segment Merging 操作解决 Segment 文件越来越多的问题。
- 随着 segment 的增多,由于一次查询的 segment 数增多,查询速度会变慢
- es 会定时在后台进行 segment merge 的操作,减少 segment 的数量
- 通过
force_merge
api 可以手动强制做 segment merge 的操作