ES(ElaticSearch)详解(含工作原理、基本知识、常见问题和优化方法)
文章目录
- 一、Lucene 和 ELK 的组成
- 二、ES 配置文件参数解读
- 三、ES 基本知识
- 1、索引(Index):类似于关系型数据库的工作表
- 2、类型(Type):废弃
- 3、文档(Document):类似于关系型数据库中的一条数据
- 4、字段(Field):文档结构的基本组成单元
- 5、映射(Mapping):定义索引中字段的数据类型及其属性
- 6、分片(Shards):大型索引拆分成多个较小部分
- 7、副本(Replicas):副本是指一个分片的复制
- 8、分配(Allocation):将索引的分片和副本合理地分布到集群的各个节点上
- 四、ES 的常见难点
- 1、索引与分片的设计
- 2、文档存放到哪个分片的路由计算
- 3、索引如何在分片上写入、读取和更新
- 4、倒排索引:词项到文档的映射关系的数据结构
- 5、倒排索引被写入磁盘后是不可改变的
- 6、动态更新索引:在保留不变性的前提下实现倒排索引的更新
- 7、近实时搜索:Lucene 允许新段被写入和打开
- 8、持久化变更:保证 ES 的可靠性
- 9、乐观并发控制:保证数据一致性的同时提高系统的并发性能
- 10、ES 集群 Master 选举流程
- 11、集群脑裂:集群被分割成多个无法通信的子集都选出一个自己的 Master 节点
- 12、索引、更新和删除文档的流程:路由计算
- 13、搜索的流程:Query Then Fetch
- 14、在并发情况下,如果保证读写一致
- 五、ES的优化
- 1、硬件选择
- 2、分片策略:合理设置分片数和推迟分片分配
- 3、路由选择
- 4、ES 写入速度优化策略
- 5、内存设置
ElaticSearch
(通常简写为ES
)是一个开源的、高扩展的分布式全文检索引擎。可以做到近乎实时地存储、检索数据,并且本身具有良好的扩展性,可以扩展到上百台服务器,处理PB级别的数据。
ES
基于Apache Lucene
构建,并使用Lucene
作为其核心来实现所有索引和搜索的功能,但目的是通过简单的RESTful API
来隐藏Lucene
的复杂性,从而简化全文搜索。它用于全文搜索、结构化搜索、分析以及将这三者混合使用。提供全文搜索并高亮关键字,以及输入实时搜索(search-asyou-type)和搜索纠错(did-you-mean)等搜索建议功能。
ES
常用于日志分析、全文搜索、安全智能、业务分析和运维智能等场景。
ES
是 Elastic Stack
的核心组件,与Logstash、Beats和Kibana等工具协同工作,共同提供数据收集、存储、分析、可视化和监控等功能,组成 ELK
。
注意:虽然 ES
原本是开源项目,但在21年, Elastic NV
改变了其软件许可策略,不再在 Apache 2.0版本(ALv2)
许可下发布 Elasticsearch
和 Kibana
的新版本。为了确保开源社区和客户继续拥有安全、高质量的完全开源的搜索和分析套件, Elastic
推出了 OpenSearch
项目,该项目是开源 Elasticsearch
和 Kibana
的社区驱动、ALv2许可的分支。
一、Lucene 和 ELK 的组成
Lucene
是一个高性能、可扩展的信息检索工具库
。它是一款纯Java的全文检索引擎工具包
,提供了完整的查询引擎和索引引擎,主要用于实现全文搜索功能。
Lucene
主要是基于倒排索引
的文本检索,通过创建并建立索引器(IndexWriter)来读取需要建立全文索引的文本内容。它提供了一组简单而强大的API,使得索引和搜索过程变得非常方便, 即读入一堆文本文件并将其转换为易于搜索的数据结构
。与 ES
等其他搜索引擎相比,Lucene
更适合于需要自行开发全文检索引擎的场景,例如定制搜索引擎或文本检索等。不过,使用 Lucene
需要手动编写代码,而 ES
等则提供了更丰富的功能和配置,可以更快速地构建出一个搜索引擎。
Lucene
和 ElasticSearch
的关系:ElasticSearch
是基于 Lucene
做了封装和增强,Elasticsearch
基于 Apache Lucene
构建,并使用 Lucene
作为其核心来实现所有索引和搜索的功能,目的是通过简单的 RESTful API
来隐藏 Lucene
的复杂性,从而简化全文搜索。
Elastic Stack
,包括 Elasticsearch
、Kibana
和 Logstash
(也称为 ELK Stack
)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。
ES
是一个基于Lucene
、分布式、通过Restful
方式进行交互的近实时搜索平台框架
。像类似百度、谷歌这种大数据全文搜索引擎的场景都可以使用ES
作为底层支持框架。Logstash
是中央数据流引擎
,用于从不同目标(文件 / 数据存储 / MQ)收集的不同格式数据,经过过滤后支持输出到不同目的地(文件 / MQ / redis / elasticsearch / kafka。Kibana
可以将ES
的数据通过友好的页面展示出来,提供实时分析的功能。
提到 ELK
首先会想到它是一个 日志分析架构技术栈
总称,但实际上它不仅仅适用于日志分析,它还可以支持其它任何数据分析和收集的场景。
ELK 流程:
收集清洗数据(Logstash) ~~~~> 搜索、存储(ElasticSearch) ~~~~> 展示(Kibana)
Google,百度类的网站搜索,它们都是根据网页中的 关键字
生成索引,我们在搜索的时候输入关键字,它们会将该关键字即索引匹配到的所有网页返回。还有常见的项目中应用日志的搜索等等。对于这些非结构化的数据文本,关系型数据库搜索不是能很好的支持。
全文搜索引擎的工作原理:
- 计算机索引程序通过扫描文章中的
每一个词
,对每一个词建立一个索引
,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。
主流的搜索引擎就两款: Elasticsearch
和 Solr
。这两款都是基于 Lucene
搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了服务器安装、部署、管理、集群以外,对于数据的操作 修改、添加、保存、查询等等都十分类似。
二、ES 配置文件参数解读
1、配置文件
# ======================== Elasticsearch Configuration =========================
cluster.name: cluster-es #集群名字
node.name: node-01
# node.master: true
# node.data: true
# node.attr.rack: r1
path.data: /usr/local/elasticsearch/elasticsearch-7.9.3/data
path.logs: /usr/local/elasticsearch/elasticsearch-7.9.3/logs
# bootstrap.memory_lock: true # JVM的内存能swap到磁盘,不能则需要配置为true
# 设置绑定的ip地址,可以是ipv4或ipv6的,默认为0.0.0.0。
network.host: 0.0.0.0 # 这个参数是用来同时设置bind_host和publish_host上面两个参数。
# network.bind_host: 192.168.0.1
# 设置绑定的ip地址,可以是ipv4或ipv6的,默认为0.0.0.0。
# network.publish_host: 192.168.0.1
# 设置其它节点和该节点交互的ip地址,如果不设置它会自动判断,值必须是个真实的ip地址。
http.port: 9200 ## 设置对外服务的http端口,默认为9200。
# 用于指定用于发现其他节点的主机列表。当一个新的节点加入到集群时,它会通过这个配置项来获取其他节点的信息,以便建立与它们的连接。
discovery.seed_hosts: ["0.0.0.0"]
# 集群的一个配置项,用于指定初始的主节点
cluster.initial_master_nodes: ["node-01"]
# gateway.recover_after_nodes: 3 ## 设置集群中N个节点启动时进行数据恢复。
# action.destructive_requires_name: true
http.cors.allow-origin: "*"
http.cors.enabled: true
# 关闭硬盘保护功能
cluster.routing.allocation.disk.threshold_enabled: false
## 常用的配置还有
node.data: true
#指定该节点是否存储索引数据,默认为true。
index.number_of_shards: 5
#设置默认索引分片个数,默认为5片。
index.number_of_replicas: 1
#设置默认索引副本个数,默认为1个副本。
2、配置文件参数解读
三、ES 基本知识
1、索引(Index):类似于关系型数据库的工作表
在 ES
中,索引是存储和组织数据的一种结构,它类似于数据库中的表或电子表格中的工作表。索引由多个文档组成,每个文档又包含多个字段,字段则是文档中存储的实际数据。索引就是一个拥有几分相似特征的文档的集合
。索引由一个名字来标识,必须全部是小写字母
,并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。
1、索引的作用:
- 数据存储:索引是
ES
中存储数据的基本单元,它提供了高效的数据存储和访问能力。 - 搜索和分析:索引不仅用于存储数据,还用于搜索和分析数据。通过使用丰富的查询语法,可以在索引中执行各种复杂的搜索操作,并根据条件过滤和排序结果。
- 聚合功能:
ES
提供了聚合功能,可以对数据进行聚合、分组和计算统计信息,满足实时分析和报告的需求。
2、索引的创建和管理
- 创建:在
ES
中,可以通过RESTful API
创建索引。创建索引时需要指定索引的名称、分片数量、副本数量等参数。 - 映射(Mapping):映射定义了索引中文档的字段类型、是否分词、原始数据是否保存等属性。在创建索引时,可以指定映射规则,以便在后续的数据存储和查询中按照预期的格式处理数据。
- 更新:更新索引通常涉及修改映射规则或增加和删除文档等。
- 删除:删除索引是将索引及其包含的所有数据从
ES
中移除。
3、索引的查询和优化
- 查询方式:支持多种查询方式,包括基于
文档 ID
的精准查询和基于内容关键字
的模糊查询等。通过使用ES
提供的查询DSL(Domain Specific Language)
,可以构建复杂的查询语句,实现各种搜索和分析需求。 - 倒排索引:
ES
使用倒排索引来快速查找文档。倒排索引是将文档中的词条映射到包含该词条的文档列表的一种数据结构,极大地提高了搜索效率。 - 优化策略:为了提高索引的查询性能,可以采取多种优化策略,如合理设置分片数量、副本数量、使用合适的分词器等。此外,还可以通过监控和分析其的性能指标,及时发现并解决潜在的性能问题。
索引是 ES
`中存储和组织数据的基本单元,它提供了高效的数据存储、搜索和分析功能。在一个集群中,可以定义任意多的索引。能搜索的数据必须索引,这样的好处是可以提高查询速度,比如:新华字典前面的目录就是索引的意思,目录可以提高查询速度。
2、类型(Type):废弃
在早期版本中,type
是索引内部的一个关键组成部分,是一个用于在索引内部进行逻辑分类的概念。每个type可以定义自己的字段(field),每个字段都具有自己的类型、属性等信息,允许在单个索引内存储多种类型的文档。
从 ES 7.0
开始,为了简化API模型和统一索引与类型之间的关系,逐步弃用了 type
概念。到了 7.3
及以上版本,已经无法创建新的多类型索引,而现有的多类型索引仍可继续使用。
官方推荐的做法是在 逻辑上将不同类型的文档放入不同的索引中
。即每个索引仅存放一种结构的文档,并通过索引名称来区分不同种类的数据。在 API
调用上,现在使用 _doc
作为文档类型占位符,但这并不表示它是一个真正的类型,而是指代任何文档类型,本质上强调了 索引-文档
这一层级关系。
随着 type
概念的弃用,ES
提供了其他机制来管理不同类型的文档:
- 不同索引:对于不同类型的文档,可以创建不同的索引来存储它们。这样可以通过索引名称来区分不同种类的数据,同时保持每个索引内部的文档结构一致性。
- 字段定义:在创建索引时,可以详细定义每个字段的类型、属性等,以确保文档结构的准确性和一致性。这有助于在查询和过滤数据时提高效率和准确性。
3、文档(Document):类似于关系型数据库中的一条数据
文档是 ES
中的基本数据单位,文档是可被索引的基础信息单元,也就是一条数据。文档以 JSON(Javascript Object Notation)
格式来表示。在一个 index 索引 / type 类型
里面,你可以存储任意多的文档。每个文档都有一个唯一的 ID
,用于标识和检索该文档。索引由多个文档组成,每个文档又包含多个字段,字段则是文档中存储的实际数据。
- 文档是
ES
中存储数据的基本单位,可以对比理解为关系型数据库中的一条数据。 - 文档以
JSON
格式存储,每个文档都是一个JSON
对象,包含了零个或多个字段或键值对。 - 文档的元数据包括索引名(
_index
)、类型名(在7.0及以后版本中,每个索引只能创建一个类型,默认类型为_doc
)、唯一ID(_id
,可以自定义或自动生成)、原始JSON
数据(_source
)等。
1、文档的结构
- 文档具有自我包含性,一篇文档同时包含字段和他们的取值。
- 文档是层次型的,一个字段的取值可以是简单的,也可以包含其他字段和取值,形成嵌套结构。
- 文档拥有灵活的结构,不依赖于预先定义的模式,即并非所有的文档都需要拥有相同的字段。
2、文档的操作
- 创建:通过向
ES
的索引中发送JSON
格式的文档数据来创建文档。 - 搜索:
ES
提供了强大的搜索功能,可以根据指定的查询条件在索引中搜索匹配的文档。 - 更新:虽然
ES
底层没有直接的更新数据操作,但可以通过删除旧文档并添加新文档的方式来实现更新。 - 删除:通过指定文档的
ID
来删除索引中的文档。
3、文档与索引的关系
- 索引是具有相同结构的文档集合,类似于关系数据库中的数据库实例。
- 每个索引都有一个映射(Mapping),它定义了一个索引中的每一个字段类型以及一个索引范围内的设置。
- 文档被存储在索引中,通过索引名可以执行文档的索引、搜索、更新和删除操作。
4、字段(Field):文档结构的基本组成单元
字段是文档结构的基本组成单元,每个文档都包含一个或多个字段。字段用于存储和表示文档中的数据,这些数据可以是文本、数字、日期、地理位置等各种类型。为了有效地索引和搜索这些文档,ES
定义了多种字段类型来表示文档中的不同数据。
- 文本类型(Text):用于全文本值,如博客文章或电子邮件正文。这些字段可以被分析成多个词项,用于全文搜索。不支持聚合操作。
- 关键字类型(Keyword):用于结构化数据,如电子邮件地址、主机名或标签。它们不会被分析,通常用于过滤、排序和聚合。支持精确值搜索。
- 数值类型(Numbers):包括长整型(long)、双精度浮点型(double)等,用于表示数量。根据实际情况选择最小的类型,以提高索引和搜索效率。
- 日期类型(Dates):用于日期值,支持多种日期格式,包括时间戳。Elasticsearch内部会将日期数据转换为UTC,并存储为自纪元以来的毫秒数的长整型整数。
- 地理位置类型(Geo-location):包括地理点(geo_point)和地理形状(geo_shape)。用于存储经纬度坐标或复杂的地理形状,如多边形。支持距离查询和地理空间查询。
- IP类型(IP):用于存储IPv4和IPv6地址。支持IP地址的精确查询和范围查询。
- 二进制类型(Binary):用于存储二进制数据,以Base64编码字符串的形式存储。默认情况下,该类型的字段只存储不索引。
- 对象和关联类型:包括对象(object)、扁平化对象(flattened)、嵌套对象(nested)和连接类型(join)。用于创建复杂的对象模型或表示文档之间的父子关系。
- 范围类型(Range):用于表示范围值,如数字范围、日期范围等。
- 特殊类型:如completion(用于自动完成建议)、search_as_you_type(用于按输入实时完成建议)、token_count(文本中标记的计数)等。
在 ES
中,字段的存储和索引是可以配置的:
- 存储(Store):设置字段是否需要存储原始值,以便在检索时使用。默认情况下,大多数字段类型不存储原始值,但可以通过设置
store
参数为true
来启用存储。 - 索引(Index):设置字段是否可搜索。默认情况下,大多数字段类型是可搜索的,但可以通过设置
index
参数为false
来禁用索引。
多字段与动态映射
- 多字段(Multi-fields):允许为同一个字段指定多种索引方式,以满足不同的搜索需求。例如,可以将一个字符串字段同时映射为
text
字段和keyword
字段,以便同时进行全文搜索和精确值搜索。 - 动态映射(Dynamic Mappings):当向
Elasticsearch
索引中插入新文档时,如果文档中包含未在映射中定义的字段,ES
会根据字段的数据类型自动创建映射。这可以简化映射的管理,但也可能导致意外的字段类型或索引行为。
Elasticsearch中的字段是文档结构的基本组成单元,具有丰富的字段类型以满足不同的数据存储和搜索需求。通过合理配置字段的存储、索引和多字段等属性,可以优化Elasticsearch的索引和搜索性能。
5、映射(Mapping):定义索引中字段的数据类型及其属性
在 ES
中,映射是指定义索引中字段的数据类型及其属性的过程。映射定义了索引中文档的结构,包括每个字段的数据类型、是否被索引、是否被存储、是否包含在全文搜索中等信息。通过映射,ES
能够理解和处理文档中的数据。
1、映射的作用
- 定义字段类型:映射指定了每个字段应该存储的数据类型,如文本(text)、关键词(keyword)、整数(integer)、浮点数(float)、日期(date)等。这有助于
ES
正确解释文档中的数据。 - 配置索引行为:映射可以设置字段是否被索引,如果被索引,还可以指定使用哪种类型的索引方式,例如全文搜索或精确匹配。
- 自定义分析器:对于文本字段,映射可以指定用于索引和查询时的分析器。分析器用于在索引文档和搜索时对文本字段进行分词和标准化,从而影响检索的准确性和性能。
- 多字段支持:映射允许在一个字段上创建多个子字段,每个子字段有不同的用途。例如,一个文本字段可以有一个子字段用于全文搜索,另一个子字段用于排序或聚合。
- 动态映射:当新字段首次出现在文档中时,
ES
可以自动推断并添加这些字段到映射中。这种动态行为可以通过配置来控制,甚至禁止。
2、映射的分类
-
动态映射(
Dynamic Mapping
):- 定义:动态映射是
ES
根据传入文档自动检测并创建映射的过程。 - 优点:方便快捷,适应性强,无需手动定义映射。
- 缺点:可能带来不精确性和风险,因为
ES
可能无法完全准确地推断字段类型和属性。
- 定义:动态映射是
-
静态映射(
Static Mapping
):- 定义:静态映射是在创建索引之前明确地定义映射的过程。
- 优点:精确控制,性能优化,可以手动指定字段类型和属性,以满足特定的业务需求。
- 缺点:配置复杂,灵活性差,需要手动定义和维护映射。
3、映射的示例
以下是一个简单的映射定义示例,展示了如何为一个名为 my_index
的索引定义映射:
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "standard"
},
"author": {
"type": "keyword"
},
"publish_date": {
"type": "date",
"format": "yyyy-MM-dd"
}
}
}
}
在这个例子中:
title
字段是文本类型,并且使用标准分析器进行处理。author
字段是关键词类型,适合于过滤和聚合操作。publish_date
是日期类型,并指定了日期格式。
4、映射的设计原则
- 合理设计映射:根据业务需求和查询场景合理设计映射,优化性能。例如,禁用某些字段的索引可以减少索引大小,加快写入速度;或者将频繁查询的字段标记为
doc_values
以提高读取效率。 - 限制动态映射:在动态映射中,如果每个新插入的文档都引入新字段,每个新字段都添加到索引映射中,随着映射的增长,这可能会成为问题。因此,可以使用映射限制设置来限制字段映射的数量,并防止文档引起映射爆炸。
- 定期评审映射:定期评审和优化映射配置,以适应业务变化和数据特性的变化。
6、分片(Shards):大型索引拆分成多个较小部分
分片是 ES
在集群中分发数据的关键机制。它可以将一个大型索引拆分成多个较小部分,这些部分被称为分片。每个分片都是一个功能完善且独立的索引,能够独立处理读写请求。通过将数据分布在多个分片上,ES
能够实现数据的分布式存储和处理,从而处理大规模的数据量。
一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有 10 亿文档数据的索引占据 1TB 的磁盘空间,而任一节点都可能没有这样大的磁盘空间,或者单个节点处理搜索请求响应太慢。为了解决这个问题,Elasticsearch 提供了将索引划分成多份的能力,每一份就称之为分片
。
当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的 索引
,这个 索引
可以被放置到集群中的任何节点上。
1、类型与分布
- 主分片(Primary Shard):主分片是数据的容器,文档保存在主分片内。在创建索引时,需要指定主分片的数量,
并且这个数量一旦确定就不能修改
。主分片负责处理数据的写入操作,如新建、索引和删除等。 - 副本分片(Replica Shard):副本分片是主分片的一个副本,用于提供数据的冗余和容错性。副本分片可以动态修改数量,以提高系统的可用性和并发处理能力。副本分片同步存储主分片的数据内容,并且可以在主分片出现故障时提供数据恢复服务。
在集群中,主分片和副本分片都不会在同一个节点上,以防止单点故障。ES
会自动管理分片在集群中的分布和平衡,当集群扩容或缩小时,它会自动在节点间迁移分片以保持集群的平衡。
2、作用与优势
- 提升性能:通过将数据分散到多个分片上,允许并行处理查询和写入操作,从而提升了整体性能。允许你在分片之上进行分布式的、并行的操作,进而提高性能和吞吐量。
- 容错性:如果部分分片所在的节点出现故障,其他分片上的数据依然可用,这保证了一定程度的系统可用性。
- 水平扩展:分片机制使得
ES
能够轻松地实现水平扩展,通过增加节点和分片数量来处理更多的数据和请求。
至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由 ES
`管理的,对于作为用户的你来说,这些都是透明的,无需过分关心。
被混淆的概念是,一个 Lucene
索引我们在 ES
称作分片。 一个 ES
索引是分片的集合。 当 ES
在索引中搜索的时候,他发送查询到每一个属于索引的分片(Lucene
索引),然后合并每个分片的结果到一个全局的结果集。
7、副本(Replicas):副本是指一个分片的复制
副本(replica)是指一个分片(shard)的复制。为了提高 ES
的高可用性和搜索吞吐量,会将分片复制一份或多份存储在其他服务器上。这样,即使当前的服务器出现故障,拥有副本的服务器仍然可以提供服务。每个分片可以有多个副本,用于数据冗余和负载均衡。副本可以提供数据的冗余备份,确保数据的高可用性和容错性。
在一个网络 / 云的环境里,失败随时都可能发生,在某个分片 / 节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个 故障转移机制
是非常有用并且是强烈推荐的。为此,ES
允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。
1、副本分片之所以重要,有两个主要原因:
- 提高容错能力:副本分片是主分片的一个备份,可以防止硬件故障导致的数据丢失。当主分片异常时,副本可以被提升为主分片,继续提供服务。因此,复制分片从不与原分片或主要分片置于同一节点上。
- 提高搜索吞吐量:主分片和副本分片都可以处理查询请求(搜索或文档检索)。因此,数据的冗余越多(即副本分片越多),能处理的搜索吞吐量就越大。
2、副本与主分片的关系
- 处理请求的区别:主分片和副本分片都能处理
查询请求
,只有主分片才能处理索引请求
(即添加、更新或删除文档)。 - 存储位置:主分片和对应的副本分片不会存储在同一个节点上,这是为了确保数据的冗余和故障恢复能力。
每个索引可以被分成多个分片。一个索引也可以被复制 0 次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。
分片和复制的数量可以在索引创建的时候指定
。在索引创建之后,你可以在任何时候动态地改变复制的数量,但是你事后不能改变分片的数量。默认情况下,ES
中的每个索引被分为 1 个主分片和 1 个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有 1 个主分片和另外 1 个复制分片(1 个完全拷贝),这样的话每个索引总共就有 2 个分片,我们需要根据索引需要确定分片个数。
3、副本数量的设定
- 默认设置:
ES
默认为一个索引创建5个主分片,并分别为其创建一个副本分片。 - 动态调整:副本分片的数量可以根据需求进行动态调整。例如,可以通过
API
命令来增加或减少副本分片的数量。 - 设定原则:副本数量的设定应根据集群节点的数量和存储空间来决定。如果集群服务器多且有足够的存储空间,可以多设置副本数以提高容错能力和搜索吞吐量;如果集群服务器相对较少且存储空间有限,则可以只设定一份副本以保证容灾。
副本的管理是自动的,但用户可以通过配置来优化副本的使用。例如,可以设置副本分片的数量、监控副本的状态以及处理副本分片故障等。
ES
的副本是提高系统高可用性和搜索吞吐量的重要机制。通过合理配置和管理副本,可以确保集群的稳定性和性能。
8、分配(Allocation):将索引的分片和副本合理地分布到集群的各个节点上
分配涉及到如何将索引的分片(shards)和副本(replicas)合理地分布到集群的各个节点(nodes)上。
1、分片与副本的分配
- 分片:ES通过将索引划分为多个分片来实现分布式存储和搜索。每个分片都是一个功能完善且独立的
索引
,可以放置到集群中的任何节点上。分片包括主分片(primary shard)和副本分片(replica shard),其中主分片负责处理索引请求(如添加、更新或删除文档),而副本分片则作为主分片的备份,可以处理查询请求以提高搜索吞吐量。 - 分配:分配是指将分片(包括主分片和副本分片)分配到集群中各个节点的过程。这个过程是由
ES
的master
节点负责的,它会根据集群的状态、节点的性能和存储空间等因素来做出决策。
2、分配的原则和策略
- 均匀分配:为了最大化利用集群的资源和提高性能,
ES
会尽量将分片均匀分配到各个节点上。这样可以避免某些节点过载而其他节点空闲的情况。 - 副本分离:为了确保数据的冗余和故障恢复能力,
ES
会将主分片和对应的副本分片分配到不同的节点上。这样可以防止因单个节点故障而导致数据丢失。 - 机架感知:在分配分片时,
ES
还会考虑物理硬件配置的因素。例如,通过机架感知(rack awareness)功能,ES
可以将不同的节点分配到不同的物理机架或网络区域中,以提高集群的可靠性和可用性。
3、分配的相关配置和命令
- 自定义属性:通过为节点设置自定义属性(如
node.attr.{attribute}
),可以指定分片分配的规则。 - 索引级配置:在创建索引时,可以通过索引级配置来指定分片的数量和副本的数量。此外,还可以使用索引级配置来控制分片分配的规则,如指定哪些节点可以分配某个索引的分片。
- 集群级配置:通过集群级配置可以修改集群的默认分片分配策略。可以设置是否允许对所有类型的分片进行分片平衡、设置延迟分配策略等。
- API命令:可以使用
API
命令来查看当前集群的分片分布、添加或删除副本分片、重新平衡分片等。
4、分配过程中的注意事项
- 避免分片过多:虽然分片可以提高搜索吞吐量,但过多的分片会增加集群的管理开销和查询延迟。
- 监控集群状态:定期监控集群的状态和性能是确保分配合理性的关键。监控节点的负载、磁盘空间、网络带宽等指标。
- 动态调整:可通过
ES
提供的API
命令和配置选项需要动态调整分片的数量和副本的数量。
四、ES 的常见难点
1、索引与分片的设计
索引与分片的设计如下图所示:
创建一个叫做Car 的索引,有三个节点,一主两从,每一个节点都有三个分片,一主两副,并且主副不在同一个节点中。
- 一个运行中的
ES
实例称为一个节点,而集群是由一个或者多个拥有相同cluster.name
配置的节点组成, 它们共同承担数据和负载的压力。 - 当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
- 当一个节点被选举成为主节点时,它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。
- 主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。
- 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点
。无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 ES
对这一切的管理都是透明的。
从技术上来说,索引能保存无尽的文档。但从实际存储的角度来说,我们应该将比较大的索引切分为相对较小的分片,而且这些分片能存在于多个节点中,这种分布式的架构设计就是产生 ES
高效查询的原因之一。
2、文档存放到哪个分片的路由计算
当索引一个文档的时候,文档会被存储到一个主分片中。 ES
如何知道一个文档应该存放到哪个分片中呢?
这个过程是根据下面这个公式决定的:
shard = hash(routing) % number_of_primary_shards
routing
是一个可变值,默认是文档的 _id
,也可以设置成一个自定义的值。 routing
通过 hash
函数生成一个数字,然后这个数字再除以 number_of_primary_shards
(主分片的数量)后得到余数 。这个分布在 0 ~ number_of_primary_shards-1
之间的余数,就是我们所寻求的文档所在分片的位置。
这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量,因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。
所有的文档 API
( get
、 index
、 delete
、 bulk
、 update
以及 mget
)都接受一个叫做 routing
的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。
3、索引如何在分片上写入、读取和更新
我们假设有一个集群由三个节点组成。 它包含一个叫 emps
的索引,有两个主分片,每个主分片有两个副本分片。相同分片的副本不会放在同一节点。
我们可以发送请求到集群中的任一节点。 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。
在下面的例子中,将所有的请求发送到 Node1
,将其称为 协调节点(coordinating node)
。当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点。
1、索引的写入
新建索引和删除请求都是写操作,必须在主分片上面完成之后才能被复制到相关的副本分片。
上图是3个节点,设置为2个主分片和4个副分片,两个主分片为 P1
和 P0
。
- 客户端向
Node1
发送新建、索引或者删除请求。 - 节点使用文档的
_id
确定文档属于分片0
。请求会被转发到Node3
,因为分片0
的主分片
目前被分配在Node3
上。 Node3
在主分片上面执行请求。如果成功了,它将请求并行转发到Node1
和Node2
的副本分片上。- 如果所有的副本分片都报告成功,
Node3
将向协调节点报告成功,协调节点向客户端报告成功。 - 在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。
2、索引的读取
我们可以从主分片或者从其它任意副本分片检索文档。
- 客户端向
Node 1
发送获取请求。 - 节点使用文档的
_id
来确定文档属于分片0
。分片0
的副本分片存在于所有的三个节点上。 - 在这种情况下,它将请求转发到
Node 2
。 Node 2
将文档返回给Node 1
,然后将文档返回给客户端。
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。
在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
3、索引的更新
- 客户端向
Node 1
发送更新请求。 - 它将请求转发到主分片所在的
Node 3
。 Node 3
从主分片检索文档,修改_source
字段中的JSON
,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会重试步骤 3 ,超过retry_on_conflict
次后放弃。- 如果
Node 3
成功地更新文档,它将新版本的文档并行转发到Node 1
和Node 2
上的副本分片,重新建立索引。一旦所有副本分片都返回成功,Node 3
向协调节点也返回成功,协调节点向客户端返回成功。
当主分片把更改转发到副本分片时,它不会转发更新请求。相反,它转发完整文档的新版本。
请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果 ES
仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。
4、倒排索引:词项到文档的映射关系的数据结构
ES 中的倒排索引是一种核心的数据结构,它极大地优化了搜索性能,使得 ES 能够实现高效的全文搜索功能。
倒排索引是一种从词项(Term)到文档(Document)的映射关系的数据结构。与传统的正排索引(即文档到词项的映射)相反,倒排索引中每个词项都关联着一个或多个包含该词项的文档列表及其位置信息。这种结构使得搜索操作能够迅速定位包含特定关键词的文档,从而大幅提高查询效率。
1、倒排索引的组成
- 词典(Dictionary):包含所有在文档集中出现的关键词。
- 倒排列表:每个关键词都对应一个倒排列表,该列表包含了所有包含该关键词的文档的ID及该关键词在文档中的位置信息(可选)。
2、工作原理
- 文档索引:当一个文档被索引时,
ES
会对文档进行分析(Analyze),将其分解为多个词条(Term)。分析过程包括分词(Tokenization)、词干提取(Stemming)和去除停用词(Stop Word Removal)等步骤。处理后的词条将被添加到倒排索引中。 - 查询处理:当用户发起搜索请求时,
ES
会根据查询条件在倒排索引中查找匹配的文档。具体过程包括解析查询(将用户输入的查询字符串解析为关键词列表)、查找词典(在倒排索引的词典中查找每个关键词,获取对应的倒排列表)、合并结果(根据倒排列表合并结果,生成匹配文档的列表)和计算评分(对匹配的文档进行相关性评分,排序后返回给用户)。
3、倒排索引的优化
- 跳表(Skip List):在倒排列表中引入跳表结构,允许快速跳转到指定位置,加速查询速度。
- 前缀压缩(Prefix Compression):对词典中的相邻词条进行前缀压缩,减少存储空间。
- 块索引(Block Indexing):将倒排列表分成固定大小的块,每个块包含多个文档
ID
。查询时,可以快速定位到包含目标文档ID
的块,从而减少遍历的时间。
4、应用场景
- 文本搜索引擎:倒排索引是构建文本搜索引擎的核心数据结构,可以实现快速、高效和精确的文本匹配和搜索。
- 日志分析:Elasticsearch可以存储并分析大量日志数据,通过倒排索引快速检索特定日志条目。
- 实时数据分析:在实时数据分析场景中,Elasticsearch可以利用倒排索引提供快速的数据检索能力。
- 数据库索引:倒排索引可以用于构建关系型或非关系型数据库的索引,提高读写性能和减少存储空间。
- 网络安全:倒排索引可以用于基于网络流量和日志数据的异常检测和入侵检测,提高网络安全性。
5、倒排索引被写入磁盘后是不可改变的
早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。
倒排索引被写入磁盘后是不可改变的:它永远不会修改。
不变性有重要的价值:
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像
filter
缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。 - 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
当然,一个不变的索引也有不好的地方。主要事实是它是不可变的!你不能修改它。如果你需要让一个新的文档可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
6、动态更新索引:在保留不变性的前提下实现倒排索引的更新
如何在保留不变性的前提下实现倒排索引的更新?
用更多的索引
。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。
Elasticsearch
基于 Lucene
,这个 java
库引入了 按段搜索
的概念。每一段本身都是一个倒排索引, 但索引在 Lucene
中除表示所有段的集合外,还增加了 提交点
:一个列出了所有已知段的文件。
1、按段搜索会以如下流程执行:
- 新文档被收集到内存索引缓存。
- 不时地,缓存被提交。
一个新的段
和一个追加的倒排索引
被写入磁盘。一个新的包含新段名字的提交点
被写入磁盘。- 磁盘进行同步,所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。
- 新的段被开启,让它包含的文档可见以被搜索。
- 内存缓存被清空,等待接收新的文档。
当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。这种方式可以用相对较低的成本将新文档添加到索引。
段是不可改变的
,所以既不能把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del
文件,文件中会列出这些被删除文档的段信息。
当一个文档被 删除
时,它实际上只是在 .del
文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中移除。
文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
7、近实时搜索:Lucene 允许新段被写入和打开
Elasticsearch 是一个基于 Lucene 构建的搜索引擎,它利用 Lucene 的索引和搜索功能来实现高效的文档检索。在 Elasticsearch 中,一个索引(Index)可能分为多个分片(Shard),而每个分片实际上都是一个 Lucene 的索引。Lucene 的索引由多个段(Segment)组成,每个段都是一些倒排索引的集合。
当创建一个新的文档时,它会归属于一个新的段,而不是去修改原来的段
。这是因为 Lucene 的索引是不可变的,一旦创建就不能被修改。因此,每次文档更新或删除时,都会创建一个新的段来反映这些变化,而原来的段则保持不变。
在 Elasticsearch 中,当新文档被索引时,它首先会被写入内存缓冲区(Memory Buffer)和事务日志(Translog)中。默认情况下,Elasticsearch 会 每秒执行一次刷新(Refresh)操作
。在刷新过程中,内存缓冲区中的数据会被写入到一个新的段中,并且这个新段会被打开,使其包含的文档对搜索可见。
这一步操作是轻量级的,因为它不需要将数据从内存刷新到磁盘上。相反,新段首先被写入到文件系统缓存中(这一步代价比较低),稍后才会被刷新到磁盘上(这一步代价比较高)。但是,只要文件已经在系统缓存中,就可以像其他文件一样被打开和读取。
由于 Elasticsearch 每秒都会执行一次刷新操作,因此新索引的文档通常会在一秒内变为对搜索可见。这就是 Elasticsearch 能够实现近实时搜索的原因。
需要注意,虽然文档在一秒内变为可见,但并不意味着它们已经被持久化到磁盘上。为了确保数据的可靠性,ES 还会定期执行刷新(Flush)操作,将内存中的数据全部写入新的段中,并将这些段刷新到磁盘上,同时清空Translog日志。
8、持久化变更:保证 ES 的可靠性
在 Elasticsearch 中,数据是动态变化的,包括文档的添加、更新和删除等操作。为了确保这些变更在系统崩溃或重启后不会丢失,需要将它们持久化到磁盘上。持久化变更的过程包括将内存中的数据刷新到磁盘,并更新相关的索引文件和事务日志。
- 数据变更:当用户在 ES 中执行索引操作(如添加、更新或删除文档)时,这些变更首先会被记录到内存缓冲区和事务日志(Translog)中。
- 刷新(Refresh):ES 会定期(默认为每秒一次)执行刷新操作,将内存缓冲区中的数据生成新的段(Segment)并写入文件系统缓存区。这些新段随后被打开以供搜索查询读取。但需要注意的是,此时数据还没有被持久化到磁盘上。
- 提交(Commit):为了将数据持久化到磁盘上,ES 需要执行提交操作。提交操作会创建一个包含所有当前段列表的
提交点(Commit Point)
,并将其写入磁盘。同时,文件系统缓存中的所有段文件也会通过fsync操作被强制刷新到磁盘上。 - 清空Translog:在提交操作完成后,旧的Translog文件会被清空和删除,以释放磁盘空间。同时,一个新的Translog文件会被创建,用于记录后续的变更操作。
如果没有用 fsync
把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 ES 的可靠性,需要确保数据变化被持久化到磁盘。在动态更新索引,我们说一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。ES 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。
在持久化变更的过程中,事务日志(Translog)记录了 ES 中的每个操作,包括文档的添加、更新和删除等。在发生系统崩溃或重启时,ES 可以使用 Translog 来恢复数据。具体来说,ES 会从磁盘中的最后一个提交点开始恢复已知的段,并重新执行 Translog 中的所有操作,以添加最后一次提交后发生的更改。
在 ES 中,可以通过配置参数来优化持久化变更的性能和可靠性。例如:
refresh_interval
:设置刷新操作的间隔时间。默认情况下为每秒一次,但可以根据实际需求进行调整。translog.durability
:设置Translog的持久化级别。可选值为request
(每次写请求后都执行 fsync)和async
(异步执行fsync,默认为每5秒一次)。根据对性能和可靠性的要求,可以选择适当的持久化级别。translog.sync_interval
:当设置为async
时,此参数指定了 Translog 执行 fsync 操作的间隔时间。
9、乐观并发控制:保证数据一致性的同时提高系统的并发性能
乐观锁的基本思想是在更新数据之前,先检查数据是否被其他用户或线程修改过,如果没有则执行更新操作,如果有则进行相应的冲突处理。它假设在绝大多数情况下,对同一个数据对象操作的冲突是较少的,因此允许多个事务并发执行,并在提交时检查冲突。
ES 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
ES 是分布式的。当文档创建、更新或删除时,新版本的文档必须复制到集群中的其他节点。ES 是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许顺序是乱的。需要一种方法确保文档的旧版本不会覆盖新的版本。
在ES中,乐观锁的实现方式主要依赖于两个关键字段:_seq_no
和_primary_term
,以及内部的_version
字段。
-
_seq_no
和_primary_term
:_seq_no
:索引级别的版本号,索引中所有文档共享一个_seq_no
。每次对文档进行操作时,_seq_no
都会递增,确保较新的操作具有更高的序列号。_primary_term
:一个整数,每当Primary Shard
发生重新分配时(如节点重启、Primary 选举或重新分配等),_primary_term
会递增1。它主要用于恢复数据时处理当多个文档的_seq_no
一样时的冲突,避免Primary Shard
上的数据写入被覆盖。
-
_version
字段:- ES中的每个文档都可以包含一个
_version
字段,用于表示文档的版本号。当对文档进行创建或更新操作时,_version
字段会随之递增。 - 通过版本号,可以追踪文档的变更历史,并可以通过指定特定版本号来获取文档的某个特定状态。
- ES中的每个文档都可以包含一个
1、乐观并发控制的工作流程
-
读取数据:当用户或线程要更新数据时,首先读取数据并获取当前的
_seq_no
、_primary_term
和_version
(如果需要)。 -
执行更新:在执行更新之前,再次检查当前的
_seq_no
和_primary_term
(或_version
,取决于具体的实现方式)是否与之前获取的一致。- 如果一致,则执行更新操作,并将
_seq_no
、_primary_term
(和_version
)进行更新。 - 如果不一致,表示数据已被其他用户或线程修改过,此时可以根据实际需求选择合适的处理方式,例如中止更新、向用户显示冲突信息、尝试自动合并等。
- 如果一致,则执行更新操作,并将
2、乐观并发控制的优缺点
- 不需要显式地锁定数据,提高了并发性能。
- 适用于多读少写的场景,因为在写入时需要进行额外的冲突检测,如果写入频率较低,冲突的概率也会相应降低。
- 在高并发写入场景下,冲突频繁发生时,可能会导致大量的回滚和重试操作,影响系统的性能。
- 不适用于需要保留所有历史版本的场景,因为版本号是单调递增的,无法回溯到更早的版本。
ES的乐观并发控制是一种高效且灵活的并发处理机制,它能够在保证数据一致性的同时提高系统的并发性能。
10、ES 集群 Master 选举流程
Master 选举通常会在以下情况下触发:
- 集群启动时:当 ES 集群首次启动时,需要选举出一个 Master 节点。
- 主节点宕机:如果当前的主节点因为某种原因宕机,集群会触发 Master 选举来选出一个新的主节点。
- 节点失效检测:ES 集群中有专门的节点失效检测机制,包括 NodesFaultDetection(用于检测普通节点是否存活)和 MasterFaultDetection(用于检测Master节点是否存活)。当检测到节点失效时,可能会触发Master选举。
在选举开始之前,集群中的节点会进行一系列准备工作:
- 确认候选主节点数达标:根据
elasticsearch.yml
配置文件中的discovery.zen.minimum_master_nodes
设置,确保集群中有足够数量的候选主节点。 - 获取节点信息:所有节点都会通过
ping
操作来获取当前集群中其他节点的信息,包括节点的ID
、是否具备Master
资格等。
选举流程大致可以分为以下几个步骤:
-
构建
候选者
列表:- 节点在启动时,如果配置了
node.master: true
,则它具备成为Master
节点的资格,会被添加到候选者列表中。 - 通过
ping
操作,节点可以获取当前集群中所有具备Master
资格的节点信息,并构建出一个候选者列表。
- 节点在启动时,如果配置了
-
选择临时
Master
:- 如果当前集群中已经存在活跃的
Master
节点(即active masters
列表不为空),则从这些活跃的Master
节点中选择一个作为临时Master
。选择的标准通常是节点的ID
值最小。 - 如果当前集群中没有活跃的
Master
节点(即active masters
列表为空),则从候选者列表中选择一个作为临时Master
。选择的标准同样是节点的ID
值最小,但会先比较集群状态的版本,选择集群状态版本较高的节点作为临时Master
;如果集群状态版本相同,则再比较节点的ID
值。
- 如果当前集群中已经存在活跃的
-
等待投票:
- 临时
Master
节点会等待其他节点的投票。只有获得超过半数候选者节点的投票,它才能成为真正的Master
节点。 - 如果在默认的超时时间(如30秒)内没有获得足够的投票,则选举失败,集群会重新进行一轮选举。
- 临时
-
发布集群状态:
- 当某个节点成功当选为
Master
节点后,它会发布新的集群状态(cluster state),并确认其他节点的加入请求。 - 其他节点在收到
Master
节点的确认后,会启动节点失效探测器(NodeFaultDetection
和MasterFaultDetection
),并定期向Master
节点发送心跳包以维持连接。
- 当某个节点成功当选为
选举过程中的注意事项:
- 防止脑裂:为了防止在选举过程中出现脑裂(即集群被分割成多个无法通信的子集,每个子集都选出一个自己的
Master
节点),ES
会根据discovery.zen.minimum_master_nodes
参数来设置法定票数。只有当候选主节点数达到或超过这个值时,选举才能继续进行。 - 选举延迟:为了避免因为频繁的主节点切换而导致集群的不稳定,
ES
在检测到主节点失效后,会先等待一段时间(如默认的30秒),然后再触发选举。这样可以确保主节点是因为真正的故障而失效,而不是因为临时的网络问题或负载过重而导致的短暂不可达。
11、集群脑裂:集群被分割成多个无法通信的子集都选出一个自己的 Master 节点
脑裂问题可能的成因:
- 网络问题:集群间的网络延迟导致一些节点访问不到
master
,认为master
挂掉了从而选举出新的master
,并对master
上的分片和副本标红,分配新的主分片 - 节点负载:主节点的角色既为
master
又为data
,访问量较大时可能会导致 ES 停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。 - 内存回收:data 节点上的 ES 进程占用的内存较大,引发 JVM 的大规模内存回收,造成 ES 进程失去响应。
脑裂问题解决方案:
- 减少误判:
discovery.zen.ping_timeout
节点状态的响应时间,默认为 3s,可以适当调大,如果master
在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如 6s,discovery.zen.ping_timeout:6
),可适当减少误判。 - 选举触发:
discovery.zen.minimum_master_nodes: 1
,该参数是用于控制选举行为发生的最小集群主节点数量。当备选主节点的个数大于等于该参数的值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。 - 官方建议为
(n / 2)+1
,n 为主节点个数(即有资格成为主节点的节点个数) - 角色分离:即
master
节点与data
节点分离,限制角色。- 主节点配置为:
node.master: true node.data: false
。 - 从节点配置为:
node.master: false node.data: true
。
- 主节点配置为:
12、索引、更新和删除文档的流程:路由计算
1、协调节点默认使用文档 ID
参与计算(也支持通过 routing
),以便为路由提供合适的分片:
shard = hash(document_id) % (num_of_primary_shards)
2、当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 Memory Buffer
,然后定时写入到 Filesystem Cache
,默认是每隔 1 秒,这个从 Memory Buffer
到 Filesystem Cache
的过程就叫做 refresh
。
3、当然在某些情况下,存在 Momery Buffer
和 Filesystem Cache
的数据可能会丢失,ES 是通过 Translog
的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog
中,当 Filesystem cache
中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush
。
4、在 flush
过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync
将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog
将被删除并开始一个新的 translog
。
5、flush
触发的时机是定时触发(默认 30 分钟)或者 translog
变得太大(默认为 512M)时。
6、删除和更新也都是写操作,但是 ES 中的文档是不可变的,因此不能被删除或者改动以展示其变更。
7、磁盘上的每个段都有一个相应的 .del
文件。当删除请求发送后,文档并没有真的被删除,而在 .del
文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉
。当段合并时,在 .del
文件中被标记为删除的文档将不会被写入新段。
8、在新的文档被创建时,ES 会为该文档指定一个版本号,当执行更新时,旧版本的文档在 .del
文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。
13、搜索的流程:Query Then Fetch
1、搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch。
2、在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size
的优先队列。
3、在搜索的时候是会查询 Filesystem Cache
的,但是有部分数据还在 Memory Buffer
,所以搜索是近实时的。
4、每个分片返回各自优先队列中所有文档的 ID
和排序值给协调节点,它合并这些值到自己的优先队列中来产生一个 全局排序后的结果列表
。
5、接下来就是取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。
6、Query Then Fetch
的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch
增加了一个预查询的处理,询问 Term
和 Document frequency
,这个评分更准确,但是性能会变差。
14、在并发情况下,如果保证读写一致
1、可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;
2、另外对于写操作,一致性级别支持 quorum/one/all
,默认为 quorum
,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。
3、对于读操作,可以设置 replication
为 sync
(默认),这使得操作在主分片和副本分片都完成后才会返回。
4、如果设置 replication
为 async
时,也可以通过设置搜索请求参数 _preference
为 primary
来查询主分片,确保文档是最新版本。
五、ES的优化
1、硬件选择
ES 的基础是 Lucene,所有的索引和文档数据是存储在本地的磁盘中,路径在 ES 的配置文件 ../config/elasticsearch.yml
中配置data
与 logs
。
磁盘在现代服务器上通常都是瓶颈。ES 重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。这里有一些优化磁盘 I/O 的技巧:
- 使用
SSD
(固态硬盘)。 - 使用
RAID0
。条带化 RAID
会提高磁盘 I/O
,代价显然就是当一块硬盘故障时整个就故障了。不要使用镜像
或者奇偶校验 RAID
,因为副本已经提供了这个功能。 - 使用多块硬盘,并允许 ES 通过多个
path.data
目录配置把数据条带化分配到它们上面。 - 不要使用远程挂载的存储,比如
NFS
或者SMB/CIFS
。这个引入的延迟对性能来说完全是背道而驰
的。
2、分片策略:合理设置分片数和推迟分片分配
分片策略之一:合理设置分片数
- 分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。
- 而且索引的分片完成分配后由于索引的路由机制,我们是不能重新修改分片数的。
可能有人会说,我不知道这个索引将来会变得多大,并且过后我也不能更改索引的大小,所以为了保险起见,还是给它设为 1000 个分片吧。但是需要知道的是,一个分片并不是没有代价的。需要了解:
- 一个分片的底层即为一个 Lucene 索引,会消耗一定文件句柄、内存、以及 CPU 运转。
- 每一个搜索请求都需要命中索引中的每一个分片,如果每一个分片都处于不同的节点还好, 但如果多个分片都需要在同一个节点上竞争使用相同的资源就有些糟糕了。
- 用于计算相关度的词项统计信息是基于分片的。如果有许多分片,每一个都只有很少的数据会导致很低的相关度。
一个业务索引具体需要分配多少分片可能需要架构师和技术人员对业务的增长有个预先的判断,横向扩展应当分阶段进行,为下一阶段准备好足够的资源。 只有当你进入到下一个阶段,你才有时间思考需要作出哪些改变来达到这个阶段。
一般来说,我们遵循一些原则:
- 原则1:控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置(一般设置不超过 32G,参考下文的 JVM 设置原则),因此,如果索引的总容量在 500G 左右,那分片大小在 16 个左右即可。当然,最好同时考虑原则 2。
- 原则2:考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。
所以, 一般都设置分片数不超过节点数的 3 倍
。 - 原则3:主分片,副本和节点最大数之间数量,我们分配的时候可以参考以下关系:
节点数 <= 主分片数 *( 副本数 + 1 )
。
分片策略之一:推迟分片分配
对于节点瞬时中断的问题,默认情况,集群 会等待一分钟
来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。通过修改参数 delayed_timeout
,可以延长再均衡的时间,可以全局设置也可以在索引级别进行修改:
3、路由选择
当我们查询文档的时候,ES 是通过公式计算出一个文档应该存放到哪个分片。
shard = hash(routing) % number_of_primary_shards
routing 默认值是 文档的 id
,也可以采用自定义值,比如 用户 id
。
不带 routing 查询
在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为 2 个步骤:
- 分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。
- 聚合:协调节点搜集到每个分片上查询结果,在将查询的结果进行排序,之后给用户返回结果。
带 routing 查询
查询的时候,可以直接根据 routing
信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。
4、ES 写入速度优化策略
ES 的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。实际使用时,我们需要根据公司要求,进行偏向性的优化。
针对于搜索性能要求不高,但是对写入要求较高的场景,我们需要尽可能的选择恰当写优化策略。
综合来说,可以考虑以下几个方面来提升写索引的性能:
- 加大
Translog Flush
,目的是降低Iops
、Writeblock
。 - 增加
Index Refresh
间隔,目的是减少Segment Merge
的次数。 - 调整
Bulk
线程池和队列。 - 优化节点间的任务分布。
- 优化
Lucene
层的索引建立,目的是降低CPU
及IO
。
1、批量数据提交
ES 提供了 Bulk API
支持批量操作,当我们有大量的写任务时,可以使用 Bulk 来进行批量写入。
通用的策略如下:Bulk
默认设置批量提交的数据量不能超过 100M
。数据条数一般是根据文档的大小和服务器性能而定的,但是单次批处理的数据大小应从 5MB~15MB
逐渐增加,当性能没有提升时,把这个数据量作为最大值。
2、优化存储设备
ES 是一种密集使用磁盘的应用,在段合并的时候会频繁操作磁盘,所以对磁盘要求较高,当磁盘速度提升之后,集群的整体性能会大幅度提高。
3、合理使用合并
Lucene 以段的形式存储数据。当有新的数据写入索引时,Lucene 就会自动创建一个新的段。随着数据量的变化,段的数量会越来越多,消耗的多文件句柄数及 CPU 就越多,查询效率就会下降。
由于 Lucene 段合并的计算量庞大,会消耗大量的 I/O,所以 ES 默认采用较保守的策略,让后台定期进行 段合并
。
4、减少 Refresh 的次数
Lucene 在新增数据时,采用了延迟写入的策略,默认情况下索引的 refresh_interval
为1 秒。Lucene 将待写入的数据先写到内存中,超过 1 秒(默认)时就会触发一次 Refresh
,然后 Refresh
会把内存中的的数据刷新到操作系统的文件缓存系统中。
如果我们对搜索的实效性要求不高,可以将 Refresh
周期延长,例如 30 秒。这样还可以有效地减少段刷新次数,但这同时意味着需要消耗更多的 Heap
内存。
5、加大 Flush 设置
Flush
目的是把文件缓存系统中的段持久化到硬盘,当 Translog
的数据量达到 512MB
或者 30 分钟时,会触发一次 Flush
。
index.translog.flush_threshold_size
参数的默认值是 512MB
,我们进行修改。增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以我们需要为操作系统的文件缓存系统留下足够的空间。
6、减少副本的数量
ES 为了保证集群的可用性,提供了 Replicas
(副本)支持,然而每个副本也会执行分析、索引及可能的合并过程,所以 Replicas
的数量会严重影响写索引的效率。
当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越慢。如果需要大批量写入操作,可以先禁止 Replica
复制 , index.number_of_replicas: 0
关闭副本。在写入完成后,Replica
修改回正常的状态。
5、内存设置
ES 默认安装后设置的内存是 1GB
,对于任何一个现实业务来说,这个设置都太小了。如果是通过解压安装的 ES,则在 ES 安装文件中包含一个 jvm.option
文件,添加如下命令来设置 ES 的堆大小。
Xms 表示堆的初始大小,Xmx 表示可分配的最大内存,都是 1GB。
确保 Xmx
和 Xms
的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。
假设你有一个 64G 内存的机器,按照正常思维思考,你可能会认为把 64G 内存都给 ES 比较好,但现实是这样吗, 越大越好?虽然内存对 ES 来说是非常重要的,但是答案是否定的!
ES 堆内存的分配需要满足以下两个原则:
原则1:不要超过物理内存的 50%:Lucene 的设计目的是把底层 OS 里的数据缓存到内存中。
- Lucene 的段是分别存储到单个文件中的,这些文件都是不会变化的,所以很利于缓存,同时操作系统也会把这些段文件缓存起来,以便更快的访问。如果我们设置的堆内存过大,Lucene 可用的内存将会减少,就会严重影响降低 Lucene 的全文本查询性能。
原则2:堆内存的大小最好不要超过 32GB。
- 在 Java 中,所有对象都分配在堆上,然后有一个
Klass Pointer
指针指向它的类元数据。这个指针在 64 位的操作系统上为 64 位,64 位的操作系统可以使用更多的内存(2 ^ 64)
。在 32 位的系统上为 32 位,32 位的操作系统的最大寻址空间为 4GB(2^32)
。但是 64 位的指针意味着更大的浪费,因为你的指针本身大了。浪费内存不算,更糟糕的是,更大的指针在主内存和缓存器(例如LLC
、L1
等)之间移动数据的时候,会占用更多的带宽。
最终我们都会采用31G设置:
-Xms 31g
-Xmx 31g
假设你有个机器有 128 GB 的内存,你可以创建两个节点,每个节点内存分配不超过 32 GB。 也就是说不超过 64 GB 内存给 ES 的堆内存,剩下的超过 64 GB 的内存给 Lucene。