Elasticsearch基本概念与核心原理

本文基于Elasticsearch7.x版本.

什么是Elasticsearch

Elasticsearch是一个分布式的搜索引擎和数据分析引擎, 提供了全文检索, 同义词处理, 相关度排名, 复杂数据分析, 海量数据的近实时处理等功能, Elasticsearch作为传统数据库的一个补充, 提供了很多数据库所不不能提供的功能.

Elasticsearch的核心概念

Elasticsearch的概念和其他分布式中间件差不多, 可以对照着理解.(实际上Redis, Kafka, Zookeeper, Elasticsearch等这些中间件里的分布式概念殊途同归, 可以相互对照一下)

(1) Near Realtime(NRT)

近实时. 有两个意思, 一个是从写入数据到数据可以被搜索到有一个小延迟(1秒), 另一个是基于ES执行搜索和分析可以达到秒级.

(2) Cluster

集群. 包含多个节点, 每个节点属于哪个集群是通过一个配置(cluster.name)来决定的.

(3) Node

节点. 一个ES进程就是一个节点, 当启动多个ES进程后就组成了一个ES集群, 当然一个节点也可以组成一个ES集群.

(4) Index

索引. 一类具有相似结构的文档数据的集合,比如用户信息索引, 订单索引, 商品索引等.

(5) Type

类型. 每个索引里都可以有一个或多个type, type是index中的一个逻辑数据分类, 一个type下的document, 都有相同的field.

注意: Elasticsearch7.x以后的版本type都默认为_doc, 不要再手动指定了.

(6) Document和field

文档和属性. document是ES中的最小数据单元, 一个document可以是一条用户数据, 一条商品数据, 一条订单数据, 通常用JSON数据结构表示. 每个index下的type中, 都可以去存储多个document. 一个document里面有多个field, 每个field就是一个数据字段.

(7) Shard

分片. 单台机器无法存储大量数据, ES可以将一个索引中的数据切分为多个shard, 分布在多台服务器上存储. 有了shard就可以横向扩展(增加服务器), 存储更多数据, 让搜索和分析等操作分布到多台服务器上去执行, 提升吞吐量和性能. 每个shard都是一个lucene index.

(8) Replica

副本. 任何一个服务器随时可能故障或宕机, 此时shard可能就会丢失, 因此可以为每个shard创建多个replica. replica可以在shard故障时提供备用服务, 保证数据不丢失, 多个replica还可以提升搜索操作的吞吐量和性能.

Elasticsearch分布式特性

基本概念

(1) Coordinating Node

处理请求的节点, 叫 Coordinating Node. 具体功能是路由请求到正确的节点, 例如创建/删除索引的请求, 需要路由到 Master 节点. 所有节点默认都是Coordinating Node.

创建和删除索引需要将请求发送给Master节点, 搜索和聚合Coordinating节点自己处理.

(2) Data Node

可以保存数据的节点, 叫做 Data Node. 节点启动后, 默认就是数据节点. 可以设置 node.data: false 禁止.

(3) Master Node

  • 负责索引的创建与删除.
  • 决定分片被分配到哪个节点.
  • 负责主分片与副本分片的身份切换.
  • 维护并且更新 Cluster State.

(4) Master Eligible Nodes

可以选举为Master节点的候选节点. 默认情况下, 所有节点都是Master Eligible Nodes, 可以设置 node.master: false 禁止.

(5) Cluster State

集群状态信息, 维护了⼀个集群中必要的信息.

  • 所有的节点信息.
  • 所有的索引及其相关的 Mapping 与 Setting 信息.
  • 分片的路由信息.

每个节点上都保存了集群的状态信息, 但是只有 Master 节点才能修改集群的状态信息, 并负责同步给其他节点. 因为任意节点都能修改信息会导致 Cluster State 信息的不⼀致.

Master节点选举算法

Elasticsearch没有使用ZooKeeper来进行Master选举, 而是自己实现了一套选举算法.

Master 节点会周期性地向集群中的其他节点发送ping, 检查节点是否仍然活跃. 其他节点也会周期性地检查 Master 节点是否活跃.

(1) 选举时机

当有以下情况发生时, 会触发Master选举.

  • 集群启动时(或新节点加入集群).
  • Master节点监测到有节点断开连接.
  • 节点监测到Master节点断开连接.

(2) 选举算法

  • 查找当前活跃的Master节点.
  • 如果存在活跃的Master节点, 选择其中nodeId最小的那个作为Master节点. (脑裂问题可能导致多个Master节点)
  • 如果不存在活跃的Master节点, 则获取集群中活跃的Master Eligible Nodes.
  • 如果集群中活跃的候选节点数超过一半(正常情况下候选节点数的一半), 则选择clusterStateVersion(集群状态版本)最新的那个作为Master节点. 如果clusterStateVersion相同, 则选择nodeId最小的那个作为Master节点.
  • 如果如果集群中活跃的候选节点数没有超过一半, 则无法选举.

(3) 脑裂问题

Split-Brain是分布式系统的经典网络问题.

由于网络原因(如机房之间网络连接断开), 导致原本集群中的部分节点掉出集群, 这样集群就被分为了两部分. 两个部分的节点会分别触发Master节点选举, 最后导致原本的一个集群变成了两个集群, 当网络恢复正常后, 两个集群合并会导致数据冲突等各种问题.

在这里插入图片描述

解决办法就是"过半选举机制", 即要有超过一半的Master Eligible Nodes选举某个节点, 这个节点才能当选为Master节点. 注意这个一半是指网络正常时集群中的一半, 而不是分裂后的一半.

使用ZooKeeper实现Master选举算法

  • 所有的Master Eligible Nodes尝试在ZK上创建一个指定路径的临时节点.
  • 第一个创建成功的候选节点成为Master节点.
  • 其他候选节点去创建时会收到“节点已存在”的异常, 这样就知道Master节点已存在.
  • 其他候选节点监听Master节点.
  • 一旦ZooKeeper失去Master节点的连接, 就会删除对应的临时节点, 同时通过监听器通知其他节点.
  • 重复以上步骤.

Kafka就是使用ZooKeeper实现的Master选举算法.

Elasticsearch的高可用机制

分片与副本

一个索引包含多个分片, 每个分片都是一个lucene实例, 具备完整的建立索引和处理请求的能力. 当集群中节点的数目发生变化时, 分片会自动在节点中负载均衡.

每个分片(Primary Shard)都有一个副本分片(Replica Shard), 副本分片负责容错以及承担读请求负载. 主分片和副本分片的默认数量都是1, 但主分片一旦确定就不可修改, 副本分片可以随时修改. 且处于容错的目的, 主分片不能它自己的副本分片放在同一个节点上, 所以最小的高可用配置是2台服务器.

文档到分片的路由算法

ES默认使用的是Hash路由算法: shard = hash(routing) % number_of_primary_shards.

默认的routing值是文档id, 也可以自行指定routing值. 比如我们将MySQL表里的数据导入到ES中, 就可以指定MySQL表的某个字段作为routing值.

post index_name/_doc/id?routing=user_id

集群健康状态

GET _cluster/health

{
  "cluster_name" : "elasticsearch",
  "status" : "yellow",
  "timed_out" : false,
  "number_of_nodes" : 1,
  "number_of_data_nodes" : 1,
  "active_primary_shards" : 12,
  "active_shards" : 12,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 13,
  "delayed_unassigned_shards" : 0,
  "number_of_pending_tasks" : 0,
  "number_of_in_flight_fetch" : 0,
  "task_max_waiting_in_queue_millis" : 0,
  "active_shards_percent_as_number" : 48.0
}
  • green
    健康状态, 所有的主分片和副本分片都可用.
  • yellow
    亚健康, 所有的主分片可用, 部分副本分片不可用.
  • red
    不健康状态, 部分主分片不可用.

Document的写入原理

(1) Document的写入过程

  • Document写入Index Buffer, 同时将写命令记录到Transaction Log.
  • 每隔1秒或Index Buffer空间被占满后, Index Buffer中的数据被写入新的Segment中, 并进入OS Cache, 这个过程叫Refresh. (此时倒排索引已创建, 存在OS Cache中, 数据可被搜索)
  • 重复前面两个步骤.
  • 每隔30分钟或Transaction Log占满后, 先进行Refresh操作, 然后将OS Cache中的Segment刷入磁盘, 这个过程叫Flush.
  • 删除旧Transaction Log, 创建一个新的Transaction Log.

看到这个写入过程是不是感觉和MySQL很像? 其实我们常用数据库和中间件的数据写入过程都差不多.

(2) 为什么Elasticsearch是近实时的?

从上文Document的写入过程中可以得知, 倒排索引在Elasticsearch进行Refresh操作后才会被创建, 而Refresh操作的执行频率是1秒, 这就是Elasticsearch是近实时的原因.

(3) Lucene Index

Elasticsearch是基于Lucene实现的, 每一个Shard都是一个Lucene Index. Lucene Index 由多个Segment组成, 数据存储在Segment中, Segment的中的每个字段(Field)都有一个倒排索引, 倒排索引不可变.

Lucene Index还有两个重要的文件Commit Point文件和".del"文件, Commit Point文件用来记录所有的Segment信息, ".del"文件用来记录删除文档的信息.

(4) 倒排索引设计为不可变的原因?

  • 解决了并发问题.
  • 数据压缩, 减少磁盘IO.
  • 加载进文件系统缓存后, 只要缓存空间足够就会一直存在.

(5) 什么是Transaction Log?

出于性能考虑, 并不是每一个写操作都会立即持久化到磁盘, 而是先写入缓冲, 然后再批量写入磁盘. 但这样会有数据丢失的风险, 如果在写入磁盘之前机器宕机了, 那缓冲中的所有数据都将会丢失. 所以Elasticsearch引入了Transaction Log, 在将数据写入缓冲的同时, 将写命令记录到Transaction Log中.

Transaction Log是默认落盘的(旧版本是每隔5s落盘一次), 使用的是顺序写, 性能很高.

关于顺序写的详细介绍: https://blog.csdn.net/litianxiang_kaola/article/details/103632124

(6) Segment Merge

从上文的Document的写入过程中可以发现, 每次进行Refresh操作时都会创建一个新的Segment, 这会导致资源浪费, 所以Elasticsearch在后台自动执行Segment Merge, 将小的Segment合并成大的Segment, 将大的Segment合并成更大的Segment, 同时在这个过程中清除那些已删除的文档.

ES并发控制

ES并发控制使用的是乐观版本控制, 即乐观锁.

什么是乐观锁?

总是假设最好的情况, 即在并发情况下永远不会发生并发冲突(不存在同时有两个线程进行写操作), 所以在这种假设下, 不会对线程加锁, 那它是如何实现的呢?

乐观锁和悲观锁介绍

ES乐观版本控制

ES中的文档创建时会生成三个数值version, seq_no和primary_term.

  • seq_no和primary_term用于内部版本控制.
    seq_no和primary_term要与文档的相同.
  • version用于外部版本控制.
    外部的version要大于文档的version.

ES中的文档是不可变更的, 当修改一个文档时, ES会将其标记为删除, 然后创建一个全新的文档. 新文档的primary_term和旧文档相同, version和seq_no会加1.

内部版本控制

Elasticsearch内部执行的版本控制, seq_no和primary_term要与文档的相同.

(1) 添加数据

PUT /student/_doc/1
{
  "name": "tyshawn",
  "age": 17
}

返回结果:

{
  "_index" : "student",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

(2) 修改数据—版本冲突

POST /student/_doc/1?if_seq_no=1&if_primary_term=1
{
  "name": "tyshawn",
  "age": 20
}

结果:

{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, required seqNo [1], primary term [1]. current document has seqNo [0] and primary term [1]",
        "index_uuid": "oSyR6rjGQtSpuibGxLXpUg",
        "shard": "0",
        "index": "student"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, required seqNo [1], primary term [1]. current document has seqNo [0] and primary term [1]",
    "index_uuid": "oSyR6rjGQtSpuibGxLXpUg",
    "shard": "0",
    "index": "student"
  },
  "status": 409
}

(3) 修改数据—正常

POST /student/_doc/1?if_seq_no=0&if_primary_term=1
{
  "name": "tyshawn",
  "age": 20
}

结果:

{
  "_index" : "student",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}

外部版本控制

Elasticsearch的版本控制也可以使用外部的版本号, 但外部版本号要大于文档的版本号才能执行成功.

(1) 新增数据

PUT /student/_doc/1
{
  "name": "tyshawn",
  "age": 17
}

结果:

{
  "_index" : "student",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

(2) 修改数据—版本冲突

POST /student/_doc/1?version=1&version_type=external
{
  "name": "tyshawn",
  "age": 20
}

结果:

{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, current version [1] is higher or equal to the one provided [1]",
        "index_uuid": "3rEpuPkyTg-cC_jPdIiI6A",
        "shard": "0",
        "index": "student"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, current version [1] is higher or equal to the one provided [1]",
    "index_uuid": "3rEpuPkyTg-cC_jPdIiI6A",
    "shard": "0",
    "index": "student"
  },
  "status": 409
}

(3) 修改数据—正常

POST /student/_doc/1?version=2&version_type=external
{
  "name": "tyshawn",
  "age": 20
}

结果:

{
  "_index" : "student",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值