Elasticsearch核心技术与实践六

708 阅读17分钟

五、分布式特性及分布式搜索的机制

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 节点

image

1.7 集群信息

  • 集群状态信息(Cluster State),维护了一个集群中,必要的信息
    • 所有的节点信息
    • 所有的索引和其相关的 MappingSetting 信息
    • 分片的路由信息
  • 在每个节点上都保存了集群的状态信息
  • 但是,只有 Master 节点才能修改集群的状态信息,并负责同步给其他节点
    • 因为,任意节点都能修改信息会导致 Cluster State 信息不一致

1.8 脑裂问题

  • Split-Brain,分布式系统的经典问题,当出现网络问题,一个节点和其他节点无法连接
    • Node2Node3 会重新选举 Master
    • Node1 自己还是作为 Master,组成一个集群,同时更新 Cluster State
    • 导致集群中有 2 个 Master,维护不同的 Cluster State。当网络恢复时,无法选择正确恢复

image

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 eligiblenode.mastertrue
datanode.datatrue
ingestnode.ingesttrue
coordinating only设置上面三个参数全部为false
machine learingnode.mltrue(需要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 ShardIndex
      • 如果该索引增长很快,集群无法通过增加节点实现对这个索引的数据扩展
    • 主分片数设置过大:导致单个Shard容量很小,引发一个节点上有过多分片,影响性能
    • 副本分片数设置过多,会降低集群整体的写入性能

2.4 集群节点问题

2.4.1 单节点集群

image

  • 副本无法分片,集群状态为黄色(Yellow)
  • 我们可以增加一个数据节点的方式,来让副本分片可以分配到第二个节点上

2.4.2 增加一个数据节点

image

  • 集群状态转为绿色
  • 集群具备故障转移能力
  • 尝试着将Replica设置成2和3,查看集群的状况

2.4.3 再增加一个数据节点

image

  • 集群具备故障转移能力
  • Master 节点会决定分片分配到哪个节点
  • 通过增加节点,提高集群的计算能力

2.4.4 故障转移

image

  • 3个节点共同组成一个集群。包含了一个索引,索引设置了3个Primary Shard和1个Replica Shard
  • 节点1是Master节点,节点意外出现故障。集群重新选举Master节点
  • Node3上的R0提升成P0,集群变黄
  • R0R1分配结束之后,集群变绿

2.5 集群健康状态

image

  • Green: 健康状态,所有主分片和副本分片都可用
  • Yellow: 亚健康,所有的主分片可用,部分副本分片不可用
  • Red: 不健康状态,部分主分片不可用

3. 分片及其生命周期

3.1 分片的内部原理

  • 什么是ES的分片
    • ES中最小的工作单元/是一个 LuceneIndex
  • 一些问题:
    • 为什么ES的搜索是近实时的(1秒后被搜索到)
    • ES如何保证在断电时数据不会丢失
    • 为什么删除文档,并不会立刻释放空间

3.2 倒排索引不可变性

  • 倒排索引采用Immutable Design,一旦生成,不可更改
  • 不可变性,带来了的好处如下:
    • 无需考虑并发写文件的问题,避免了锁机制带来的性能问题
    • 一旦读入内核的文件系统缓存,便留在哪里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能
    • 缓存容易生成和维护 / 数据可以被压缩
  • 不可变性,带来的挑战:如果需要让一个新的文档可以被搜索到,需要重建整个索引

www.zhulou.net/post/8005.h…

3.3 Lucene Index

image

  • Lucene中,单个倒排索引文件被称为SegmentSegment是自包含的,不可变更的。多个Segments汇总在一起,称为LuceneIndex,其对应的就是ES中的Shard
  • 当有新文档写入时,会生成新的Segment,查询时会同时查询所有的Segment,并且对结果汇总。Lucene中有一个文件,用来记录所有Segments的信息,叫做Commit Point
  • 删除的文档信息,保存在.del文件中

3.4 什么是Refresh

image

ES写入文档时,并不是直接将文档写入Segment,而是先写入一个叫做Index Buffer的缓冲区,再由缓冲区写入Segment

  • Index buffer写入Segment的过程叫RefreshRefresh不执行fsync操作
  • Refresh频率:默认1s发生一次,可通过index.refresh_interval配置。Refresh后,数据就可以被搜索到了。这也是为什么ElasticSearch被称为近实时搜索
  • 如果系统有大量的而数据写入,那就会产生很多Segment
  • Index Buffer被占满时,会触发Refresh,默认值是JVM的10%

3.5 什么是Transaction Log

image

  • Segment写入磁盘的过程相对耗时,借助文件系统缓存,Refresh时,先将Segment写入缓存以开放查询
  • 为了保证数据不会丢失。所以在Index文档时,同时写Transaction Log,高版本开始,Transaction Log默认落盘。每个分片有一个Transaction Log
  • ES进行Refresh时,Index Buffer被清空,Transaction log不会清空

3.6 什么是Flush

image

  • 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文件做一个定期的处理

  • ESLucene会自动进行Merge操作
    • POST my_index/_forcemerge
  • Segment很多,需要定期被合并
    • 减少Segments/ 删除已经删除的文档

4. 剖析分布式查询及相关性算分

4.1 分布式搜索的运行机制

  • ElasticSearch的搜索,会分两阶段进行
    • 第一阶段:Query
    • 第二阶段:Fetch
  • Query-then-Fetch

4.2 Query阶段

image

  • 用户发出搜索请求到ES节点。节点收到请求后,会以Coordinating(协调节点)节点的身份,在6个主副分片中随机选择3个分片,发送查询请求

这里说的有些问题,并不是随机,看看这篇博客www.elastic.co/guide/cn/el…

  • 被选中的分片执行查询,进行排序。然后,每个分片都会返回From + Size个排序后的文档Id和排序值给Coordinating节点

4.3 Fetch阶段

image

  • Coordinating Node会将Query阶段,从每个分片获取的排序后的文档Id列表,重新进行排序。选取FromFrom + 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 排序

image

  • 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"
            }
        }
    ]
}

我们按照订单的时间来降序排序

image

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"
            }
        }
    ]
}

image

5.2.3 对Text类型的字段做排序

image

我们发现报错了,报错提示我们需要开启Fielddata属性

# 打开Text字段的 fielddata
PUT kibana_sample_data_ecommerce/_mapping
{
    "properties": {
        "customer_full_name": {
            "type": "text",
            "fielddata": true,
            "fields": {
                "keyword": {
                    "type": "keyword",
                    "ignore_above": 256
                }
            }
        }
    }
}

image

image

5.3 排序的过程

  • 排序是针对字段原始内容进行的。倒排索引无法发挥作用;
  • 需要用到正排索引。通过文档Id和字段快速得到字段原始内容
  • ElasticSearch有两种实现方法
    • Fielddata
    • Doc Values(列式存储,对Text类型无效)(这也就是我们对Text类型字段排序时,它让我们开启Fielddata的原因)

5.3.1 Doc Values VS Field Data

Doc ValuesField data
何时创建索引时,和倒排索引一起创建搜索时候动态创建
创建位置磁盘文件JVM Heep
优点避免大量内存占用索引速度快,不占用额外的磁盘空间
缺点降低索引速度,占用额外磁盘空间文档过多,动态创建开销大,占用过多JVM Heep
缺省值ES 2.x 之后ES 1.x及之前

5.3.2 关闭Doc Values

image

  • 默认启用,可以通过Mapping设置关闭
    • 增加索引的速度/减少磁盘空间
  • 如果重新打开,需要重建索引
  • 什么时候需要关闭
    • 明确不需要做排序及聚合分析

6. 分页与遍历:From,Size,Search After & Scroll API

6.1 From/Size

image

  • 默认情况下,查询按照相关度算分排序,返回前10条记录
  • 容易理解的分页方案
    • From: 开始位置
    • Size: 期望获取文档的总数

6.2 分布式系统中深度分页的问题

image

  • ES天生就是分布式的。查询信息,但是数据分别保存在多个分片,多台机器上,ES天生就需要满足排序的需要(按照相关性算分)
  • 当一个查询:From=990,Size=10
    • 会在每个分片上先都获取1000个文档。然后,通过Coordinating Node聚合所有结果。最后再通过排序选取前1000个文档
    • 页数越深,占用内存越多。为了避免深度分页带来的内存开销。ES有一个设定,默认限定到10000个文档
      • Index.max_result_window

6.3 简单的分页例子

# 简单的分页例子
POST tmdb/_search
{
  "from": 0,
  "size": 20,
  "query": {
    "match_all": {}
  }
}
  • 当最大值超过10000,ES就会报错

6.4 Search After 避免深度分页的问题

我们知道,当我们的页数越深,占用内存就会越多,那如果来避免深度分页的问题呢?

image

  • 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保证唯一性),这里的条件满足

image

3. 再次使用Search After查询(然后使用上一次,最后一个文档的sort值进行查询)
# 然后使用上一次,最后一个文档的`sort`值进行查询
POST users/_search
{
  "size": 1,
  "query": {
    "match_all": {}
  },
  "search_after": [
          13,
          "ka36BngBc4LlHCXeCOxJ"
  ],
  "sort": [
    {"age": "desc"},
    {"_id": "asc"}
  ]
}

image

然后以此类推,每次填入上一次搜索的sort值,直到查询结果为空

6.4.2 Search After是如何解决深度分页的问题

image

  • 假定 Size 是10
  • 当查询 990-1000 时
  • 通过唯一排序值定位,将每次要处理的文档数都控制在10

6.5 Scroll API(实际场景用的多)

image

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

image

2. 基于 Scroll API 创建快照
# 基于 scroll api 创建快照
# DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAFdgWOFN4NEJyd05ScnFmVjRzM3M2dzdvUQ==
POST /users/_search?scroll=5m
{
  "size": 1,
  "query": {
    "match_all": {}
  }
}

image

3. 尝试再写一个文档
POST users/_count

POST users/_doc
{"name": "user4","age":40}

image

4. 滚动执行Scroll API
POST /_search/scroll
{
  "scroll": "1m",
  "scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAFdgWOFN4NEJyd05ScnFmVjRzM3M2dzdvUQ=="
}

将上一步我们保存的scroll_id写入这个scroll_id中,我们就能拿到下一条数据,这里我们的size是1,我们每次只能拿一个

当我们依次执行,到最后,发现拿不到我们中途插入的数据

6.6 不同的搜索类型和使用场景

  • Regular
    • 需要实时获取顶部的部分文档。例如查询最新的订单
  • Scroll
    • 需要全部文档,例如导出全部数据
  • Pagination
    • FromSize
    • 如果需要深度分页,则选用Search After

7. 处理并发读写操作

7.1 并发控制的必要性

image

  • 两个Web程序同时更新某个文档,如果缺乏有效的并发,会导致更改的数据丢失
  • 悲观并发控制
    • 假定有变更冲突的可能。会对资源加锁,防止冲突。例如数据库行锁
  • 乐观并发控制
    • 假定冲突是不会发生的,不会阻塞正在尝试的操作。如果数据在读写中被修改,更新将会失败。应用程序决定如何解决冲突,例如重试更新,使用新的数据,或者将错误报告给用户
    • ES采用的是乐观并发控制

7.2 ES的乐观并发控制

image

  • ES中的文档是不可变更的。如果你更新一个文档,会将旧文档标记为删除,同时增加一个全新的文档。同时文档的version字段加1
  • 内部版本控制
    • if_seq_no + if_primary_term
  • 使用外部版本(使用其他数据库作为主要数据存储)
    • version + version_type = external

学习地址为极客时间《Elasticsearch核心技术与实战》,这只是我做的笔记,仅供参考;