简介 #
ElasticSearch 是一个开源的分布式搜索引擎,上手容易,开箱即用,具有以下特点
- 自身带有分布式协调管理功能,不依赖其他组件(它的竞品 Solr 依赖 Zookeeper)
- 注重核心功能(高级功能通过插件形式提供)
- 实时查询效果好(建立索引速度快)
Elasticsearch是一个基于 Apache Lucene (TM)的开源搜索引擎。使用 Java 编写并使用 Lucene 来建立索引并实现搜索功能,但是它的目的是通过简单连贯的 RESTful API 让全文搜索变得简单并隐藏 Lucene 的复杂性。但是 es 不仅仅是 Lucene 和全文搜索引擎,它还提供:
- 分布式的实时文件存储,每个字段都被索引并可被搜索
- 实时分析的分布式搜索引擎
- 可以扩展到上百台服务器,处理 PB 级结构化或非结构化数据
Elasticsearch 的特点是它提供了一个极速的搜索体验。这源于它的高速(speed)。相比较其它的一些大数据引擎,Elasticsearch 可以实现秒级的搜索,但是对于它们来说,可能需要数小时或更长才能完成。Elasticsearch 的 cluster 是一种分布式的部署,极易扩展(scale)。这样很容易使它处理 petabytes 的数据库容量。最重要的是 Elasticsearch 是它搜索的结果可以按照分数进行排序,它能提供我们最相关的搜索结果(relevance)。我们可以针对自己的业务场景进行 relevance 定制
此外 ES 使用 json 存储数据,对开发者友好,而且提供容易上手的 Restful API
不适应的场景 #
处理关系数据集 #
与 MySQL 等数据库不同,Elasticsearch 并非旨在处理关系数据。 Elasticsearch 允许你在数据中建立简单的关系,例如父子关系和嵌套关系,但会降低性能(分别在搜索时间和索引时间)。必须对 Elasticsearch 上的数据进行非规范化(在文档中复制或添加冗余字段,以避免必须加入数据)以改进搜索和 索引/更新性能。
执行 ACID 事务 #
es 中单个请求支持 ACID 属性,但是不存在事务的概念,es 没有提供 ACID 事务
- Atomictiy 是通过发送写入请求来实现的,该请求将在所有活动分片上成功或失败。请求无法部分成功。
- 通过写入主分片来实现 Consistency。数据复制在返回成功响应之前同步发生。这意味着在写入请求之后所有分片上的所有读取请求都将看到相同的响应。
- 提供 Isolation,因为可以成功处理并发写入或更新(即删除和写入)而不受任何干扰。
- 实现了 Durability,因为一旦将文档写入 Elasticsearch,它就会持续存在,即使在系统发生故障的情况下也是如此。 Elasticsearch 上的写入不会立即持久化到磁盘上的 Lucene 段,因为 Lucene 提交是相对昂贵的操作。相反,文档被写入事务日志(称为 translog)并定期刷新到磁盘中。如果一个节点在数据刷新之前崩溃了,translog 中的操作将在启动时恢复到 Lucene 索引中。
整个 Elastic 生态被称为 ELK,包含三部分
- Kibana(提供监控、日志分析、性能检测、控制台)
- Elastic Search(分布式搜索引擎)
- Logstash(数据处理管道,负责手机、加工、过滤数据)
基础概念 #
集群、节点、分片 #
Cluster #
即 集群,ElasticSearch 有一个活多个节点组成,可以通过其集群名称来进行标识。通常这个 Cluster 的名字是可以在 Elasticsearch 里的配置文件中设置的。在默认的情况下,如我们的 Elasticsearch 已经开始运行,那么它会自动生成一个叫做 “elasticsearch” 的集群。我们可以在 config/elasticsearch.yml 里定制我们的集群的名字。
可以通过 GET _cluster/state
来获取整个集群状态
Node #
单个 ES 实例,在大多数环境中,每个节点都在单独的 box 或虚拟机上运行,一个集群由一个或多个 node 组成。实际部署中,大多数情况需要一个 server 上运行一个 node。
根据 node 的作用,可以分为如下的几种:
- master-eligible:可以参加选主流程,成为 Master 节点。一旦成为 Master node,它可以管理整个 cluster 的设置及变化:创建,更新,删除 index;添加或删除 node;为 node 分配 shard 及应用的集群设置等。master 节点角色通常不是非常占用资源,并且可以共同位于在较小集群中运行其他角色的节点上。
每个节点上都保存了集群的状态,只有 Master 节点才能修改集群的状态信息
集群状态(Cluster State),维护了一个集群中必要的信息
- 所有的节点信息
- 所有的索引和其相关的 Mapping 和 Setting 信息
- 分片的路由信息
- data:数据 node。 可以保存数据的节点,负责保存分片数据。
- ingest: 数据接入(比如 pipepline)
- machine learning 负责跑寄去学习的 Job,用来做异常检测
- coordinating node: 严格来说,这个不是一个种类的节点。它甚至可以是上面的任何一种节点。这种节点通常是接受客户端的 HTTP 请求的。针对大的集群而言,通常的部署时使用一些专用的节点来接受客户端的请求。这样的节点可以不配置上面的任何角色
作用:
- 很容易被 kill 或从集群中摘除。因为它不是 master 服务器,不产于集群功能,不包含数据,不会因故障而发生数据重定位、复制
- 可以防止用户的错误查询而导致集群不稳定。如执行聚合过大(如日期直方图范围为若干年,间隔为 10s)。ES 新版中存在断路器(circuit breaker)结构。总是存在一些边界 case 导致不稳定,但是协调节点不是主节点,它的过载不会对集群稳定性造成影响
- 如果协调器或客户端节点嵌入到应用程序中,则数据的往返次数会减少,从而加快应用程序的速度。
- 可以添加它们以平衡搜索和聚合吞吐量,而不会在集群中产生更改和数据重定位。
一般来说,一个 node 可以具有上面的一种或几种功能。我们可以在命令行或者 Elasticsearch 的配置文件(config/elasticsearch.yml)来定义:
Node类型 | 配置参数 | 默认值 |
---|---|---|
master-eligible | node.master | TRUE |
data | node.data | TRUE |
ingest | node.ingest | TRUE |
machine learning | node.ml | true (除了OSS发布版) |
如果没有配置上述参数,则可以个任务这个 node 是 coordination node。此时它可以接受外部请求,并转发到相应节点处理。针对 master node,有时我们需要设置 cluster.remote.connect: false,这样它不可以作为 CCS/CCR 用途。
实际使用中,请求可以被发送到 data/ingest/coordination 节点,但是不能发送到 master 节点
在整个 Elastic 架构中,data node 和 cluster 的关系表述如下:
在 Elastic Stack 7.9 之后,有了新的改进。Elasticsearch:Node roles 介绍 - 7.9 之后版本。
shard #
ES 是一个分布式搜索引擎,因此索引通常会拆分为分布在多个节点上的被称为分片(shard)的元素。ES 会自动管理这些分片的排列,并根据需要重新平衡分片。
试想一个问题,一个索引可以存储超过单个节点硬件限制的大量数据,比如,一个具有 10 亿文档的索引占据 1TB 的磁盘空间,而任一节点都没有这样大的磁盘空间;或者单个节点处理搜索请求,响应太慢。为了解决这个问题,Elasticsearch 提供了将索引划分成多份的能力,这些份就叫做分片(shard)。当你创建一个索引的时候,你可以指定你想要的分片(shard)的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。
分片之所以重要,主要有两方面的原因:
- 允许你水平分割/扩展你的内容容量
- 允许你在分片(潜在地,位于多个节点上)之上进行分布式的、并行的操作,进而提高性能/吞吐量
分片存在两种类型:
主分片(Primary shard):
- 每个文档都存储在一个 Primary shard。索引文档时,它首先在 Primary shard上编制索引,然后在此分片的所有副本上(replica)编制索引。
- 索引可以包含一个或多个主分片。 此数字确定索引相对于索引数据大小的可伸缩性。 用以解决数据水平扩展问题,通过主分片,可以将数据分不到集群内的所有节点上。创建索引后,无法更改索引中的主分片数。
- Primary 可以同时进行读和写操作
- 只有在成功更新副本分片后才会确认索引请求,从而确保跨 Elasticsearch 集群的读取一致性。
副本(Replica shard):
- 每个主分片可以具有零个或多个副本。 replica 是主分片的副本。replica 只能是只读的,不可以进行写入操作。
- 用以解决数据高可用的问题,是主分片的拷贝。replica 分片可以独立于主分片响应搜索(或读取)请求。由于主分片和副本分片被分配到不同的节点(为索引提供更多计算资源),因此可以通过添加 replica 来实现读取可扩展性。
- 实现故障转移。如果一个索引的 primary shard 一旦被丢失(有宕机或者网络连接问题),那么相应的 replica shard 会被自动提升为新的 primary shard。即使你失去了一个 node,那么副本分片还是拥有所有的数据。永远不会在与其主分片相同的节点上启动副本分片。
Primary 及 replica shards 一直是分配在不同的 node 上的,这样既提供了冗余度,也同时提供了可扩展行。
默认情况下,每个主分片都有一个副本,但可以在现有索引上动态更改副本数。我们可以通过如下的方法来动态修改副本数:
PUT my_index/_settings
{
"number_of_replicas": 2
}
分片的设定 #
- 分片数设置过小
- 导致后续无法增加节点,实现水平扩展;
- 单个分片数据量太大,导致数据重新分配耗时
- 分片数设置过大(7.0 开始默认主分片设置为 1,解决了 over-sharding 的问题)
- 影响搜索结果的相关性打分,影响统计结果准确性;
- 单个节点上过多的分片,导致资源浪费,同时影响性能
通常一个 shard 可以存储许多文档。在实际的使用中,增加副本 shard 的数量,可以提高搜索的速度,这是因为更多的 shard 可以帮我们进行同时进行搜索。但是副本 shard 数量的增多,也会影响数据写入的速度。在很多的情况下,在大批量数据写入的时候,我们甚至可以把 replica 的数量设置为 0。(如何提升 ES 数据写入速度
增加 primary shard 的数量可以提高数据的写入速度,这是因为有更多的 shard 可以帮我们同时写入数据。 oversharding 是 Elasticsearch 用户经常会遇到的一个问题。许多小的 shard 会消耗很多的资源,这是因为每一个 shard 其实对应的是一个 Lucene 的 index。一个 shard 通常可以存储几十个 G 的数据。如果你需要更多的 shard,你可以:
- 创建更多的索引从而使得它容易扩展,比如针对一些时序数据,我们可以为它们每天或者每个星期创建新的索引
- Split index API 把一个大的索引分拆成更多分片。
我们可以使用 auto_expand_replica 这个配置来让 Elasticsearch 自动决定有多少个 replica。当我们有一个节点时,通过这个设置,我们可能会得到 0 个 replica 从而保证整个集群的健康状态。
如上图,建议 50g 为索引的大小以获得最好的性能。为了最大限度地提高索引/搜索性能,分片应尽可能均匀分布在节点之间,以利用底层节点资源。 每个分片应保存 30 GB 到 50 GB 的数据,具体取决于数据类型及其使用方式。例如,高性能搜索用例可以受益于整体较小的分片以运行快速搜索和聚合请求,而日志记录用例可能适合稍大的分片以在集群中存储更多数据。(Elasticsearch:我的 Elasticsearch 集群中应该有多少个分片?)
为每个索引设置以及查询相应的 shard size
# 一旦设置好 primary shard 的数量,我们就不可以修改了。
# 这是因为 Elasticsearch 会依据每个 document 的 id 及 primary shard 的数量来把相应的 document 分配到相应的 shard 中。
# 如果这个数量以后修改的话,那么每次搜索的时候,可能会找不到相应的 shard。
curl -XPUT http://localhost:9200/another_user?pretty -H 'Content-Type: application/json' -d '
{
"settings" : {
"index.number_of_shards" : 2,
"index.number_of_replicas" : 1
}
}'
# 查询索引设置
curl -XGET http://localhost:9200/twitter/_settings?pretty
# 响应
{
"twitter" : {
"settings" : {
"index" : {
"creation_date" : "1565618906830",
"number_of_shards" : "1",
"number_of_replicas" : "1",
"uuid" : "rwgT8ppWR3aiXKsMHaSx-w",
"version" : {
"created" : "7030099"
},
"provided_name" : "twitter"
}
}
}
}
集群健康状况 #
可以通过以下接口获得一个索引的健康情况
http://localhost:9200/_cat/indices/twitter
Green
- 每个索引的每个 shard 都有 replica,而且主分片与副本都正常分配,如果一个 node 挂掉,另外一个 node 的 replica 将会起作用,不会有数据的丢失
Yellow
- 主分片全部正常分配,有副本分片未能正常分配(如,replica shard 和 primary shard 处在同一个 node 中,如果 node 损坏,整个数据库会丢失
- 该状态在开发阶段较常见,此时通常启动单个 ES 服务器
Red
- 有主分片未能分配,有的 shard 及其相应的 replica 已经不能正常访问。
- 例如,当服务器的磁盘容量超过 85% 时,去创建一个新的索引
通过如下的命令来查看集群的健康状态:
GET _cluster/health
响应值字段
字段名称 | 作用 |
---|---|
cluster_name | 集群的名称 |
timeout | 布尔值,指示 REST API 是否达到调用中设置的超时 |
number_of_nodes | 表示集群中的节点数 |
number_of_data_nodes | 表示可以存储数据的节点数 |
active_primary_shards | 显示活跃主分片的数量; 主分片是负责写操作。 |
active_shards | 显示活跃分片的数量; 这些分片可用于搜索。 |
relocating_shards | 显示了从一个节点重新定位或迁移到另一个节点的分片数量——这主要是由于集群节点平衡。 |
initializing_shards | 显示处于初始化状态的分片数量。 初始化过程在分片启动时完成。 它是激活之前的瞬态状态,由几个步骤组成; 最重要的步骤如下: 1. 如果其translog 太旧或需要新副本,则从主副本复制分片数据。 2. 检查 Lucene 索引。 3. 根据需要处理事务日志 |
unassigned_shards | 显示未分配给节点的分片数量。 这通常是由于设置的副本数大于节点数。 在启动过程中,尚未初始化或正在初始化的分片将被计入此处 |
delay_unassigned_shards | 这显示将分配的分片数量,但它们的节点配置为延迟分配。 你可以在 Delaying allocation when a node leaves 中找到有关延迟分片分配的更多信息 |
number_of_pending_tasks | 这是集群级别的待处理任务的数量,例如集群状态的更新、索引的创建和分片重定位。 它应该很少是 0 以外的任何值 |
number_of_in_flight_fetch | 这是必须在分片中执行的集群更新的数量。 由于集群更新是异步的,这个数字会跟踪有多少更新仍然需要在分片中执行 |
task_max_waiting_in_queue_millis | 这是一些集群任务在队列中等待的最长时间。 它应该很少是 0 以外的任何值。如果该值与 0 不同,则意味着存在某种集群资源饱和或类似问题 |
active_shards_percent_as_number | 这是集群所需的活动分片的百分比。 在生产环境中,它应该很少与 100% 不同——除了一些重定位和分片初始化 |
索引、文档、RestAPI #
文档 #
Elasticsearch 是面向文档的,文档是所有可搜索数据的最小单位(对应到关系型数据库的一条记录)
document 相比较于关系数据库,它相应于其中每个 record。文档会被序列化成 json 格式,保存在 Elasticsearch 中
- json 对象由字段组成
- 每个字段都有对应的字段类型(字符串、数值、布尔、日期、二进制、范围类型)
当文档被 es 索引时,存储在 _source 字段中。每个文档都会附加系统字段
- unique ID,索引范围的唯一标识符,存储在
_id
字段中,可以指定或自动生成 - 文档的索引名称由
_index
字段指示
{
"_index" : "parties",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"name" : "Elasticsearch Denver",
"organizer" : "Lee",
"location" : "Denver, Colorado, USA"
}
}
元数据,用于标注文档的相关信息
- _index 文档所属的索引名
- _type 文档所属的类型
- _id 文档唯一 ID
- _source 文档的原始 json 数据
- _all 整合所有字段内容到该字段,已被废除
- _version 文档的版本信息
- _scorce 相关性打分
Type #
类型是文档的逻辑容器,类似于表是行的容器。 你将具有不同结构(模式)的文档放在不同类型中。 例如,你可以使用一种类型来定义聚合组,并在人们聚集时为事件定义另一种类型。 每种类型的字段定义称为映射。 例如,name 将映射为字符串,但 location 下的 geolocation 字段将映射为特殊的 geo_point 类型。 每种字段的处理方式都不同。 例如,你在名称字段中搜索单词,然后按位置搜索组以查找位于你居住地附近的组。
- 在 7.0 之前,一个 Index 可以设置多个 Types
- 6.0 开始 Type 已经被 Deprecated,7.0 开始,一个索引只能创建一个 Type,即 _doc。在 8.0 的版本中,type 将被彻底删除
schema-less #
很多人认为 Elasticsearch 是 schema-less 的。大家都甚至认为 Elasticsearch 中的数据库是不需要 mapping 的。其实这是一个错误的概念。schema-less 在 Elasticsearch 中正确的理解是,我们不需要事先定义一个类型关系数据库中的 table 才使用数据库。在 Elasticsearch 中,我们可以不开始定义一个 mapping,而直接写入到我们指定的 index 中。这个 index 的 mapping 是动态生成的 (当然我们也可以禁止这种行为)。其中的数据项的每一个数据类型是动态识别的。比如时间,字符串等,虽然有些的数据类型还是需要我们手动调整,比如 geo_point 等地理位置数据。另外,它还有一个含义,同一个 type,我们在以后的数据输入中,可能增加新的数据项,从而生产新的 mapping。这个也是动态调整的。
Elasticsearch 具有 schema-less 的能力,这意味着无需显式指定如何处理文档中可能出现的每个不同字段即可对文档建立索引。 启用动态映射后,Elasticsearch 自动检测并向索引添加新字段。 这种默认行为使索引和浏览数据变得容易-只需开始建立索引文档,Elasticsearch 就会检测布尔值,浮点数和整数值,日期和字符串并将其映射到适当的 Elasticsearch 数据类型。
索引 #
(对应到关系型数据库的表)
Index 索引时文档的容器,是一类文档的结合
- index 体现了逻辑空间的概念:每个索引都有自己的 Mapping 定义,用于定义包含的文档的字段名和字段类型
- Shard 体现了物理空间的概念:索引中的数据分散在 Shard 上
索引的 Mapping 定义文档字段的类型,Setting 定义不同的数据分布
Mapping 对应关系型数据库的 scheme,表定义
一个 index 是一个逻辑命名空间,它映射到一个或多个主分片,并且可以具有零个或多个副本分片。每当一个文档进来后,根据文档的 id 会自动进行 hash 计算,并存放于计算出来的 shard 实例中,这样的结果可以使得所有的 shard 都比较有均衡的存储,而不至于有的 shard 很忙。
shard_num = hash(_routing) % num_primary_shards
默认的情况下,上面的 _routing 既是文档的 _id。如果有 routing 的参与,那么这些文档可能只存放于一个特定的 shard,这样的好处是对于一些情况,我们可以很快地综合我们所需要的结果而不需要跨 node 去得到请求。比如针对 join 的数据类型。
从上面的公式我们也可以看出来,shard 数目是不可以动态修改的,否则之后也找不到相应的 shard 号码了。但是 replica 的数目是可以动态修改的。
倒排索引 #
索引的结构有很多种,都是为了在某些场景中起到最大化查询速度的作用。倒排索引即索引的一种,本质上是为了快速检索我们存储的数据。
Elasticsearch 是建立在全文搜索引擎库 Lucene 基础上的搜索引擎,它隐藏了 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。Elasticsearch 的倒排索引,其实就是 Lucene 的倒排索引。
倒排索引区别于正排索引,对与正排索引来说,行为是:
- 文档 id -> 文档内容 -> 单词 而倒排索引索引的行为则是:
- 单词 -> 文档 id
倒排索引包含两部分:单词词典(Term Dictionary)、倒排列表(Posting List)
单词词典(Term Dictionary) #
记录所有文档的单词,记录单词到倒排列表的关联关系。单词词典一般比较大,可以通过 B+ 树或哈希拉链法实现
lucene 采用了跳表的数据结构(Term Ditionary),采用二分查找的方式提高搜索效率,对关键词进行查询。同时在跳表的基础上,增加了一层字典树(Term Index),存储单词前缀,解决了 磁盘 IO 过慢的问题。通过字典树找到单词的所在块,再通过跳表进行二分查找,找到对应的单词,以及对应的文档列表
另外,为了进一步节省内存,Lucene 还用了 FST(Finite State Transducers)对 Term Index 做进一步压缩,term index 在内存中是以 FST(finite state transducers)的形式保存的,其特点是非常节省内存。Term dictionary 在磁盘上是以分 block 的方式保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。这样 term dictionary 可以比 b-tree 更节约磁盘空间。
倒排列表(Posting List)记录了单词对应的文档结合,由倒排索引项组成 #
倒排索引项(Posting)
- 文档 ID
- 词频 TF 该单词在文档中出现的次数,用于相关性评分
- 位置 Position 单词在文档中分词的位置,用于语句搜索(phrase query)
- 偏移 Offset 记录单词的开始结束位置,实现高亮显示
压缩: 尽可能降低每个数据占用的空间,同时又能让信息不失真,能够还原回来
- 增量编码
数据只记录元素与元素之间的增量,如
[73, 300, 302, 332, 343, 372]
->[73, 227, 2, 30, 11, 29]
- 分割成块 Lucene 里每个块是 256 个文档 ID,这样可以保证每个块,增量编码后,每个元素都不会超过 256(1 byte),另外还方便进行后面求交并集的跳表运算。 假设每个块是 3 个文档的 ID,则记录为 `[73, 227, 2], [30, 11, 29]
- 按需分配空间 对于第一个块,[73, 227, 2],最大元素是227,需要 8 bits,所以就给每个元素都分配 8 bits的空间。 但是对于第二个块,[30, 11, 29],最大的元素才30,只需要 5 bits,所有给每个元素只分配 5 bits 的空间足矣。
以上三个步骤,共同组成了一项编码技术,Frame Of Reference(FOR):
快速求交并集
当有多个查询条件时,如查询 “a” 相关词语、发布时间为近一个月、作者为 “b” 的文档,此时需要根据三个字段,查询三个倒排索引,对三个倒排索引在内存中做一个交集。
假设下面三个数组,求交集
[64, 300, 303, 343]
[73, 300, 302, 303, 343, 372]
[303, 311, 333, 343]
当文档数量少于 4096 时,用 Integer 数组。对于 Integer,使用 Skip List 做合并计算。有最短的 posting list 开始遍历,由小到大遍历
当文档数量大于 4096 时,使用 Roaring Bitmaps,0 标识不存在,1 标识存在,此时每个文档 id 只需要 1 bit,假设存在 100M 个文档,此时需要 100M bits = 100M * 1/8 bytes = 12.5Mb
,比用 Inter 数组的 200Mb 节省了大量的空间。同时进行位运算,求交集,运算更快。
FOR 是压缩数据,减少磁盘占用。从磁盘取出来数据进行解压,经过 Roaring bitmaps 处理后,缓存到内存中,Lucene 称之为 filer cache
Analysis 与 Analyzer #
Analysis 文本分析,将全文本转换一系列单词(term/token) 的过程
Analysis 通过 Analyzer 实现
Analyzer 组成 #
Character Filters
- 针对原始文本处理,例如去除 html
Tokenizer
- 按照规则切分为单词
Token Filter
- 将切分的单词进行加工,小写,删除 stop
ES 为什么比 mysql 快 #
Mysql 只有 term dictionary 这一层,是以 b+-tree 的方式存储在磁盘上的。检索一个 term 需要若干次的 random access 的磁盘操作。而 Lucene 在 term dictionary 的基础上添加了 term index 来加速检索,term index 以树的形式缓存在内存中。从 term index 查到对应的 term dictionary 的 block 位置之后,再去磁盘上找 term,大大减少了磁盘的 random access 次数。