Elasticsearch:概述

Elasticsearch

应用

适用性

  • 不适合用在那些数据价值不高、对写入性能有要求、数据量大而成本受限的场景中。

缺陷

  • 字段类型无法修改。在需要添加新数据与新字段的时候,如果elasticSearch进行搜索是可能需要重新修改格式。之前的数据需要重新同步,对数据的管理有很多困难。一旦数据格式出现改变,会变得非常麻烦。
    • ES在数据结构灵活度上高于MySQL但远不如MongoDB。
  • 写入性能较低。
    • 自动建立索引使得ES的写入性能也收到了影响,要明显低于MongoDB。
  • 高硬件资源消耗。大数据量下64G内存+SSD基本是标配。
    • 对于同样的数据ES占用的存储空间也要明显大于MongoDB(建那么多索引能不占空间吗?)。

DOC

文档

面向文档

应用中的对象很少只是简单的键值列表,例如MySQL数据库那样,更多的时候它拥有复杂的数据结构,例如包含日期、地理位置、另一个对象或数组。

而数据库是行列组成的表格,如果要将一个对象存储到MySQL当中,则就像是将一个丰富、信息表现力强的对象拆散了放入一个非常大的表格中。你不得不拆散对象以适应表模式(一列对应一个字段),然后在查询时再进行重建。

Elasticsearch是面向文档的,意味着它可以存储整个对象或文档,然而它不仅仅时存储,还会索引每个文档的内容使之可以被搜索。再Elasticsearch中,你可以对文档(并非表结构)进行索引、搜索、排序、过滤。这种理解数据的方式与MySQL完全不同,也是Elasticsearch能够进行复杂的全文搜索的原因之一。

Elasticsearch使用JSON作为文档序列化格式。

程序中大多的实体或对象能够被序列化为包含键值对的JSON对象。通常我们可以认为对象与文档时等价相通的。

  • 对象时应该JSON结构体,类似于HashMap等,内部还可能包含其他对象。
  • 文档在Elasticsearch当中特指最顶层结构或者跟对象序列化成的JSON数据,以唯一ID标识并存储在ES中。

文档元数据

文档不仅仅只有数据,还包含了元数据–关于文档的信息,其中三个必须的元数据节点时:

  • _index:文档存储的地方:
    • 在ES当中index类似于数据库,是存储和索引关联数据的地方。
    • 实际上数据被存储和索引在分片中,索引只是一个把一个或多个分片分组在一起的逻辑空间。
  • _type:文档代表的对象的类:
    • 应用中,使用对象表示一些事物,每个对象都属于一个类,类定义了属性或与对象关联的数据。在关联数据库中,相同类的对象被存在一个表中。
    • ES当中,使用相同type的文档表示相同的事物,因为它们的数据结构也是相同的。每个type都有自己的mapping或结构定义,类型的mapping会告诉ES不同的文档如何被索引。
  • _id:文档的唯一标识:
    • 与index和type组合时,就可以唯一标识一个文档。

Index

在Elasticsearch中,文档归属于一种类型(type),而这些类型存在于索引(index)中,我们可以画一些简单的对比图来类比传统关系型数据库:

1
2
Relational	DB	->	Databases	->	Tables	->	Rows	->	Columns 
Elasticsearch -> Indices -> Types -> Documents -> Fields

Elasticsearch集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型 (types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段 (Fields)(列)。

在Elasticsearch中索引具有不同的涵义

  • 索引(名词):一个索引就像时传统关系数据库中的数据库,它时相关文档存储的地方
  • 索引(动词):索引一个文档,表示把一个文档存储到索引(名词n)中。以便它可以被检索或者查询,很像SQL的insert,但差别是如果文档已经存在,新的文档将覆盖旧的文档
  • 倒排索引:传统数据库为特定列增加一个索引,例如B-Tree索引来加速检索。而Elasticsearch和Lucene使用倒排索引的数据结构达到目的。

默认情况下,文档中的所有字段都会被索引(拥有一个倒排索引),只有这样他们才是可被搜索的。

作用

索引index实际上是一个用来指向一个或多个shard的逻辑命名空间。

一个shard是一个最小级别工作单元,它只是保存了索引中所有数据的一部分。

索引设置

  • number_of_shards。定义一个索引的主分片个数,默认值是5。这个配置在索引创建后不能修改。
  • number_of_replicas。每个主分片的复制分片个数,默认是1。这个配置可以随时在活跃的索引上修改。
  • analysis。配置已经存在的分析器或创建自定义分析器来定制化你的索引。

type和mapping

类型在Elasticsearch中表示一组相似的文档。类型由一个名称(比如user或blogpost)和一个类似数据库表结构的映射组成,描述了文档中可能包含的每个字段的属性,数据类型(比如string,integer或date),和是否这些字段需要被Lucene索引或储存。

Lucene如何处理文档

Lucene中,一个文档由一组简单的键值对组成,一个字段至少需要有一个值,但是任何字段都可以有多个值。类似的,一个单独的字符串可能在分析过程中被转换为多个值。

Lucene不关心这些值是字符串、数字或日期,所有的值都被当成不透明字节。当我们在Lucene中索引一个文档时,每个字段的值都被加到相关字段的倒排索引中。

类型是如何实现的

每个类型有各自的Mapping和文档,保存在index当中。

Lucene没有文档类型的概念,每个文档的类型名被储存在一个叫_type的元数据字段上。当我们搜索一种特殊类型的文档时,Elasticsearch简单的通过_type字段来过滤出这些文档。

Lucene同样没有映射的概念。映射是Elasticsearch将复杂JSON文档映射成Lucene需要的扁平化数据的方式。

  • 即当一个字段声明为了string,则加入索引前数据会被analyzer进行分析。

根对象

映射的最高层即根对象,其可能包含:

  • 一个properties节点。列出文档可能包含的每个字段的映射。
  • 多个元数据字段。每一个都以下划线开头,例如_type等。
  • 设置项。如analyzer等。
  • 其他设置,可以同时应用在根对象和其他object类型的字段上,例如enabled,dynamic和include_in_all。

元数据

_source

ES用JSON字符串表示文档主题保存在_source字段中,_source字段在写入磁盘前会被压缩。

这几乎始终是需要的功能,因为:

  • 搜索结果中能够得到完整的文档,不需要额外去别的数据源查找文档。
  • 如果缺失_source,则部分更新请求不起作用。
  • 当你的映射有变化,而且你需要重新索引数据时,你可以直接在Elasticsearch中操作而不需要重新从别的数据源中取回数据。
  • 你可以从_source中通过get或search请求取回部分字段,而不是整个文档。
  • 样更容易排查错误,因为你可以准确的看到每个文档中包含的内容,而不是只能从一堆ID中猜测他们的内容。

_all

_id

文档的唯一标识由四个元数据字段组成:

  • _id。文档的字符串ID。
  • _type。文档的类型名。
  • _index。文档所在的索引。
  • _uid。_type与_id连接成的type#id。

动态映射

当Elasticsearch处理一个位置的字段时,它通过【动态映射】来确定字段的数据类型且自动将该字段加到类型映射中。

有时这是理想的行为,有时却不是。或许你不知道今后会有哪些字段加到文档中,但是你希望它们能自动被索引。或许你仅仅想忽略它们。特别是当你使用Elasticsearch作为主数据源时,你希望未知字段能抛出一个异常来警示你。

通过dynamic可以控制这些行为,它接受下面几个选项:

  • true。自动添加字段(默认)。
  • false。忽略字段。
  • strict。遇到未知字段抛出异常。

重建索引

虽然你可以给索引添加新的类型,或给类型添加新的字段,但是你不能添加新的分析器或修改已有字段。否则已被索引的数据会变得不正确而你的搜索也不会正常工作。

修改在已存在的数据最简单的方法是重新索引:创建一个新配置好的索引,然后将所有的文档从旧的索引复制到新的上。

数据的基本操作

Search 响应

  • hits。表示匹配到的文档总数。
    • _score。相关性得分。
  • took。搜索花费的毫秒数。
  • shards。参与查询的分片数有多少成功与失败。
    • 分片可能失败,如果一些故障导致主分片与复制分片都故障,那么这个分片的数据无法响应搜索请求。
    • 此时仍然会返回剩余分片的结果。
  • timeout。查询超时与否。
    • 不会停止执行查询,它仅仅告诉你目前顺利返回结果的节点然后关闭连接。在后台,其他分片可能依旧执行查询,尽管结果已经被发送。
    • 使用超时是因为对于你的业务需求来说非常重要,而不是因为你想中断执行长时间运行的查询。

CURD

BULK

读写文档的并发操作

Search API

结构化查询

join

复合查询

全文检索

地理信息查询

控制相关度

Mapping

mapping机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型(String、number、boolean、date等)。

ES的Mapping也称为模式定义,即对数据建模。

Mapping

数据类型差异

当在索引中处理数据时,会有一些东西似乎被破坏了。

假设索引中存在12个tweets,只有应该包含日期2014-09-15,进行查询:

1
2
3
4
GET /search?q=2014 //12个结果
GET /search?q=2014-09-15 //12个结果
GET /search?q=date:2014-09-15 //1个结果
GET /search?q=date:2014 //0个结果

当进行全日期的查询时,得到12个tweets,而针对date字段进行年度查询却什么都不返回。即我们针对_all字段进行查询可全部返回,但是针对date字段却不能返回。

这是因为数据在_all字段的索引方式与在date字段的索引方式不同而导致。

数据类型与字段信息

为了将日期字段处理成日期,将数字字段处理为数字等,ES需要找到每个字段都包含了什么类型,这些类型和字段的信息存储在mapping中。

索引中每个文档都有一个类型(type)。每个类型拥有自己的映射(mapping)或者模式定义(schema definition)。一个映射定义了字段类型,每个字段的数据类型,以及字段被Elasticsearch处理的方式。映射还用于设置关联到类型上的元数据。

支持的简单字段类型

类型 表示的数据类型
String string
Whole number byte、short、integer、long
Floating point float、double
Boolean boolean
Date date

当你索引一个包含新字段的文档,即一个之前没有的字段,ES将使用动态映射猜测字段类型,其来自于JSON基本数据类型:

JSON type Field type
Boolean boolean
Whole number:123 long
Floating point:1123.4 double
String,valid date:”2014-12-15” date
String:”FOO” string

自定义字段映射

某些时候需要自定义一些特殊类型,尤其是字符串字段类型,自定义类型可以帮助:

  • 区分全文字符串字段和准确字符串字段,即分词与不分词。
  • 使用特定语言的分析器。
  • 优化部分匹配字段。
  • 指定自定义日期格式。
  • 等。

对于mapping而言最重要的字段参数是type。

对于string字段,最重要的映射参数是index和analyer。

  • index,控制字符串以何种方式被索引:
    • analyzed,默认值。首先分析这个字符串,然后索引,即进行全文形式的索引。
    • not_analyzed。索引这个字段,使其可以被搜索,但是索引内容与指定值一样,不分析此字段。
    • no。不索引这个字段。
  • analyzer,只当哪一种分析器将在搜索和索引时使用。
    • standard,默认值。
    • whitespace等。

更新mapping

可以在第一次创建索引的时候指定映射的类型。此外,你也可以晚些时候为新类型添加映射(或者为已有的类型更新映射)。

可以向已有映射中增加字段,但你不能修改它。如果一个字段在映射中已经存在,这可能意味着那个字段的数据已经被索引。如果你改变了字段映射,那已经被索引的数据将错误并且不能被正确的搜索到。

复合核心字段类型

JSON还有null值,数组和对象,所有这些Elasticsearch都支持:

多值字段

若想让tag字段包含多个值,我们可以索引一个标签数组来代替单一字符串:

1
{ "tag": ["search", "nosql"]}

对于数组不需要特殊的映射,任何一个字段可以包含0-N个值,同样对于全文字段将被分析并产生多个词。

即数组中的所有值必须为同一类型,若使用字段索引一个数组,ES将使用第一个值的类型来确定这个新字段的类型。

数组是作为多值字段被索引的,它们没有顺序。在搜索阶段你不能指定“第一个值”或者“最后一个值”。倒不如把数组当作一个值集合(bag of values)

空字段

数组可以为空,即等价为0个值,而Lucene无法存储null,因此这个字段被认为是空字段而不被索引。

多层对象

即JSON object,即hashmap。内部对象经常用于在另一个对中嵌入一个实体或对象。

1
2
3
4
5
6
7
8
{
"user":{
"name":{
"full": "Jo sh",
"first": "jo"
}
}
}

内部对象的映射

ES会动态检测新对象的字段,并映射它们为object类型,将每个字段加到properties字段下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"user":{
"type": "object",
"propertiex":{
"name":{
"type": "object",
"properties":{
"full": {"type": "string"},
"first": {"type": "string"}
}
}
}
}
}

内部对象是怎样被索引的

Lucene并不会去了解内部对象,一个Lucene文件包含一个键-值对应的扁平表单,因此ES为了有效索引内部对象,则转换了文件的格式:

1
2
3
4
{
"user.name.full":[jo,sh],
"user.name.first":[jo]
}

对象-数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"followers":[
{
"age": 35,
"name": "Mary White"
},
{
"age": 26,
"name": "Alex Jones"
},
{
"age": 19,
"name": "Lisa Smith"
}
]
}

则此时由于文件被扁平化,值之间的关联被消除了。

1
2
3
4
{				
"followers.age": [19, 26, 35],
"followers.name": [alex,jones,lisa,smith,mary,white]
}

此时需要使用嵌套对象

对数据建模

1
GET /search?q=2014 //12个结果  使用_all,索引类型是string

ES将对字段类型进行猜测,动态生成了字段和类型的映射关系。_all字段是默认字段,其类型为string。

1
GET /search?q=date:2014-09-15 //1个结果 索引类型为date,
  • 因此string与date字段的索引方式是不同的。在ES当中每一种核心数据类型是以不同的方式进行索引的。
  • 最大的差别在于确切值与全文文本间的区别对待。是区分搜索引擎与数据库的根本差异

确切值与全文文本

ES的数据类型分为确切值与全文文本。

  • 确切值是确定的,例如Date与Id等。即“FOO”与“Foo”就不同。
  • 全文文本,是文本化的数据,是一种非结构化数据。
    • 对于全文文本的查询,我们会询问这篇文章与查询的匹配度如何,即对于查询条件,文档的相关性有多高。
    • 即想查询在全文中包含文本的部分。以及针对UK将返回United Kingdom,针对jump能够匹配jumps等。
  • 针对全文查询,ES首先对文本Analyzes,然后使用结果建立一个倒排索引。

对索引建模

对文档建模

Mapping & Setting

倒排索引

ES使用倒排索引结构做快速的全文搜索,倒排索引由在文档中出行的唯一的单词列表以及对于每个单词在文档中的位置组成。

为了建立倒排索引,我们首先切分每个文档的content字段为单独的单词,我们将它们叫做词(terms)或表征(tokens)

假设存在两个文档:

1
2
The quick brown fox jumped over the lazy dog
Quick brown foxes leap over lazy dos in summer

将所有的唯一词放入列表并排序。

1567147497368

当搜索quick brown,则只需要找到每个词在哪个文档出现即可:

1567147531015

此时加入相似度算法,则可以所Doc1具有更高的匹配度。

修复数据

文本Analysis

Analysis用于进行全文文本的分词,以建立供搜索用的反向索引。

由于你只可以找到确实存在于索引中的词,因此索引文本和查询字符串都要标准化成相同的形式。

标准化和标记化的过程叫做分词Analysis。分析的过程:

  • 首先标记化一个文本块为适用于倒排索引单独的词。
  • 标准化这些词为标准形式,提高可搜索性。

这个工作是分析器(analyzer)完成的。一个分析器(analyzer)只是一个包装用于将三个功能放到一个包里。

索引管理

Search运行机制

对于ES,你可以根据ID进行检索,但是其真正的强大在于可以从混乱的数据中找出有意义的信息。

ES不仅会存储文档,也会索引(indexes)文档内容以使得它可以被搜索。每个文档里的字段都会被索引并被查询。在简单查询时,ES可以使用索引的索引。

搜索可以:

  • 类似于age这样的字段上使用结构化查询,join_date这样的字段上进行排序,与SQL的结构化查询一样。
  • 全文检索,使用所有字段来匹配关键字,然后按照关联性排序返回结果。

在ES当中:

  • Mapping。数据在每个字段中的解释说明。
  • Analysis。全文是如何处理的可被搜索的。
  • Query DSL。ES使用的灵活的、强大的查询语言。

分布式搜索方式

搜索是如何在分布式环境中执行的。

一个CRUD操作只处理一个单独的文档。文档的唯一性由_index,_type和routing-value(通常默认是该文档的_id)的组合来确定。这意味着我们可以准确知道集群中的哪个分片持有这个文档。

由于不知道哪个文档会匹配查询(文档可能存放在集群中的任意分片上),所以搜索需要一个更复杂的模型。一个搜索不得不通过查询每一个我们感兴趣的索引的分片副本,来看是否含有任何匹配的文档。

但是,找到所有匹配的文档只完成了这件事的一半。在搜索(search)API返回一页结果前,来自多个分片的结果必须被组合放到一个有序列表中。因此,搜索的执行过程分两个阶段:

  • 查询。
  • 然后取回(query then fetch)。

查询

1567564294197

  • 客户端发送应该search到Node3。
    • Node3首先接受到请求,成为协调节点。即向所有相关的分片广播搜索请求并且把它们的响应整合成一个全局的有序结果集,将结果集返回给客户端。
      • 搜索请求可以被每个分片的原本或任意副本处理,因此当更多的副本时可以提高搜索吞吐量,对于后续请求,协调节点会轮询所有的分片副本以分摊负载。
    • Node3创建了应该长度为from+size的空优先级队列。
  • Node3转发这个搜索请求到索引中每个分片的原本或副本。
    • 每个分片在本地执行这个查询并且将结果存到一个大小为from+size的有序本地优先队列中。
  • 每个分片分会Doc的ID和它优先队列中所有Doc的排序值给协调节点Node3。
    • Node3将这些值合并到自己的优先队列中产生全局排序结果。
    • 类似于归并。

取回

查询阶段辨别出那些满足搜索请求的Doc,但我们仍然要取回那些Doc本身,即取回阶段。

1567566224758

  • 协调节点辨别出哪个Doc需要取回,并向相关分片发出Get请求。
    • 协调节点为每个持有Doc的分片简历多点Get然后发送请求到处理查询阶段的分页副本。
  • 每个分片加载Doc并且根据需要丰富它们,然后将Doc返回协调节点。
    • 加载Doc主体_source,如果需要,还会根据元数据丰富结果和高亮搜索片断。
  • 一旦所有的Doc被取回,则将结果返回给客户端。

深分页

查询然后取回过程虽然支持通过使用from和size参数进行分页,但是它使得所有的分片都要保留一个优先队列,如果是很大的size,或足够大的from,则排序将非常繁重,占用很高的性能。

如果你确实需要从集群里获取大量documents,你可以通过设置搜索类型scan禁用排序,来高效地做这件事。

搜索选项

一些Query String可选参数能够影响搜索过程。

preference

preference参数允许你控制使用哪个分片或哪个节点来处理搜索请求。

使用随机字符串可以避免结果震荡问题

  • 当你按照timestamp字段进行结果排序,并且两个Doc有相应的timestamp,由于搜索请求是在所有有效的分片副本间轮询的,这两个Doc可能在原始分片中是一种顺序,在副本分片中是另一种顺序。
  • 因此用户每次刷新页面,第一次Doc1,Doc2,第二次Doc2,Doc1。
    • 可以使用用户的SessionID设置preference参数。

timeout

通常,协调节点会等待接收所有分片的回答。如果有一个节点遇到问题,它会拖慢整个搜索请求。

timeout告诉协调节点最多等待多久,就可以放弃等待而将已有结果返回。返回部分结果总比什么都没有好。

_shards参数将指出搜索是否超时,所有分片是否成功答复。

routing

反映路由选择。

可以在建立索引时提供一个自定义的routing参数来保证所有相关的document(如属于单个用户的document)被存放在一个单独的分片中。在搜索时,你可以指定一个或多个routing值来限制只搜索那些分片而不是搜索index里的全部分片:

1
GET /search?routing=user_1

在设计非常大的搜索系统时非常有效。

search_type

可以指定搜索类型,虽然query_then_fetch是默认的搜索类型。

  • count。只有query阶段,没有取回阶段。
  • query_and_fetch。将查询与取回合并为一个步骤,是一个内部优化选项。
  • dfs_query_then_fetch和dfs_query_and_fetch。dfs是一个预查询阶段,会从全部相关的分片里取回项目频数来计算全局的项目频数。
  • scan。与scroll滚屏一起使用,可以高效地取回巨大数量的结果,它通过禁用排序来实现的。

扫描与滚屏

可以高效地取回巨大数量的结果,它通过禁用排序来实现的,不需要付出深分页的代价。

scroll

一个滚屏搜索允许我们做一个初始阶段搜索并且持续批量从Elasticsearch里拉取结果直到没有结果剩下。这有点像传统数据库里的cursors(游标)。

滚屏搜索会及时制作快照。这个快照不会包含任何在初始阶段搜索请求后对index做的修改。它通过将旧的数据文件保存在手边,所以可以保护index的样子看起来像搜索开始时的样子。

scan

禁用排序,并传递一个scroll告诉ES应该scroll多久。

分布式特性

集群和节点

节点node是一个运行着地Elasticsearch实例,集群cluster是一组具有相同cluster.name的节点集合,他们协同工作,共享数据并提供故障转移和扩展功能。

修改cluster.name可以通过修改config/目录下的elasticsearch.yml文件,然后重启Elasticsearch来实现。

天然分布式

Elasticsearch可以扩展到上百、千的服务器来处理PB级的数据,Elasticsearch的设计隐藏了分布式本身的复杂性。

Elasticsearch在分布式概念上做了很大程度上的透明化,在教程中你不需要知道任何关于分布式系统、分片、集群发现或者其他大量的分布式概念。所有的节点既可以运行在你的笔记本上,也可以运行在拥有100个节点的集群上,其工作方式是一样的。

Elasticsearch致力于隐藏分布式系统的复杂性。以下这些操作都是在底层自动完成的:

  • 将你的文档分区到不同的容器或者分片(shards)中,它们可以存在于一个或多个节点中。
  • 将分片均匀的分配到各个节点,对索引和搜索做负载均衡。
  • 冗余每一个分片,防止硬件故障造成的数据丢失。
  • 将集群中任意一个节点上的请求路由到相应数据所在的节点。
  • 无论是增加节点,还是移除节点,分片都可以做到无缝的扩展和迁移。

概念

空集群

一个节点就是一个ElasticSearch实例,而一个集群由一个或多个节点组成,它们具有相同的cluster.name,它们协同工作,分析数据和负载。

当加入新的节点或删除一个节点,集群就会感知到并平衡数据。

主节点

集群中一个节点会被选举为主节点,它将临时管理集群级别的一些变更,例如新建或删除索引,增加或移除节点等。

主节点不参与文档级别的变更或搜索,意味着在流量增长时,主节点不会称为集群的瓶颈,且任意节点都可以成为主节点。

作为用户我们可以与集群中的任何节点通信,包括主节点。每一个节点都知道文档存在于哪个节点上,它们可以转发请求到相应的节点,我们访问的节点负责收集各节点返回的数据,最后一起返回给客户端。这一切由ES处理。

Shard

分片就是一个Lucene实例,并且它本身就是一个完整的搜索引擎,我们的文档存储在分片中,并且在分片中被索引,但是我们的应用程序不会直接与shards通信,而是与索引直接通信。

Shard是ES集群中分发数据的关键,把分片类比成数据的容器,文档存储在分片中,然后分片分配到你集群的节点上。当集群扩容或缩小,ES将会自动在你的节点间迁移分片,以使得集群保持平衡。

Shard分类:

  • 主分片。索引中的每个文档属于一个单独的主分片,所以主分片的数量决定了索引最多能够存储多少数据。
    • 理论上能够存储的大小没有限制,限制取决于实际的使用情况,硬件存储的大小、文档的大小和复杂度,如何索引和查询你的文档,以及期望的响应时间。
    • 当index创建后,主分片的数量就确定了。
    • number_of_shards参数。
  • 复制分片。是主分片的副本,防止硬件故障导致数据丢失。
    • 可提供读请求,例如搜索或从其他shard取回文档。
    • index创建后复制分片的数量可随时调整。
    • number_of_replicas参数。

集群健康

集群健康有三种状态:

  • green。所有主要分片和复制分片都可用。
  • yellow。所有主要分片可用,但不是所有复制分片都可用。
  • red。不是所有主要分片都可用。

查询集群信息:GET /_cluster/health,status表示集群的服务状况。

在索引建立后,集群首先是yellow状态,因为所有的复制分片还是没有unassigned的,它们不会被分配到节点上,在同一个节点上保存相同的数据副本是不必要的。

故障转移

单节点上运行有单点故障风险,即没有数据备份。为了防止则启动另一个节点。

可以使用同样的命令行在同样的目录下进行创建,一个节点可以启动多个ES实例。

只要第二个节点与第一个节点具有相同的cluster.name,具体值查看./config/elasticsearch.yml,它就能自动发现并加入第一个节点所在的集群。其原理是进行网络广播。

当第二个节点加入集群,则三个复制分片也将被分配,分别对应三个主分片。文档将首先被存储在主分片中,然后并发复制到对应的复制节点上。此时的集群状态为green。

横向扩展

当启动第三个节点,集群会重新组织。

在原有情况下,两个节点各自拥有3个分片。此时将进行分片的移动,将其中一个主分片和一个复制分片移动到节点3上。

分片本身是一个完整的搜索引擎,它可以使用单一节点的所有资源。主分片或者复制分片都可以处理读请求——搜索或文档检索,所以数据的冗余越多,我们能处理的搜索吞吐量就越大。

当然在相同数量节点上增加更多的复制分片并不能提高性能,因为对硬件资源占用减少。

应对故障

对于这样一个集群:

1567067619776

若杀死Node1,则由于杀死了主节点Master,而一个集群必须拥有主节点才能使得其功能正常,因此各个节点选举了一个新的主节点。

1567067688072

此时,主分片1与2丢失,而索引在丢失主分片时不可用,则此时状态为red。因此新的主节点负责将复制分片升级为主分片,此时集群健康回到yellow。但是不是green,因为复制分片没有全部分配。

如果我们重启了Node1,集群能够重新分配丢失的复制分片,并将只从主分片上复制在故障期间有数据变更的那一部分。

分布式增删改查

考虑数据是如何在集群中分布和获取的相关技术细节。

路由

  • 当你索引一个文档,它被存储在单独一个主分片上。ES是如何知道文档属于哪个分片的呢?
  • 当你创建一个新文档,它是如何知道是应该存储在分片1还是分片2上呢?

进程不能是随机的,因为将来要检索文档,因此根据算法进行决定:

1
2
3
//routing值是一个任意字符串,默认是_id,但也可以自定义。
//这个routing字符串通过哈希函数生产一个数字,然后除以主切片数量得到一个余数
shard = hash(routing) % number_of_primary_shards

因此主分片的数量只能在创建索引时定义且不能修改,如果主分片的数量在未来改变了,所有先前的路由就失效了,文档无法找到。

所有的文档API都接收一个routing参数,用来自定义文档到分片的映射,自定义路由值可以确保所有的相关文档都保存在同一个分片中。

分片交互

假设有3个节点的集群,它包含一个叫做blogs的索引并拥有两个主分片,每个主分片有两个复制分片。

我们能够发送请求给集群中任意一个节点,每个节点都有能力处理任意请求,每个节点都知道任意文档所在的节点,所以也能将请求转发到需要的节点。

接收请求的节点称为请求节点

新建、索引和删除

新建、索引和删除请求都是写操作,它们必须在主分片(Node1)上成功完成才能复制到相关的复制分片上。其步骤是:

  • 客户端给Node1发送新建、索引和删除请求。
  • 节点使用文档_id确定文档属于分片0,因此将请求转发到Node3,分片0位于这个节点上。
  • Node3在主分片上执行请求,如果成功,它转发请求到相应的位于Node1、Node2的复制节点上。
    • 当所有的复制节点报告成功,Node3报告成功到请求的节点,请求的节点再报告给客户端。
    • 即当客户端收到响应时,文档的修改已经应用到了主分片和所有复制分片上。

同步/异步复制

  • 复制的默认值是sync,导致主分片得到复制分片的成功响应后才返回。
    • sync更优,因为允许ES强制反馈传输。
  • 可以设置replication为async,请求在主分片上被执行后就会返回给客户端。它依然会转发请求给复制节点,但你不知道成功与否。
    • async可能会因为在不等待其他分片就绪的情况下发送太多请求而使得ES过载。

可用分片数

  • 默认主分片在尝试写入时需要规定数量或过半的分片可用。这是为了防止数据被写入到错误的网络分区。
    • int ((primary + number_of_replicas) / 2) + 1
    • consistency允许的值为:
      • one,即只有一个主分片。
      • all,所有的主分片和复制分片。
      • quorum,默认,或过半分片。
    • number_of_replicas是在索引中的设置,用于定义复制分片的数量,而不是现在活动的复制节点的数量。

timeout

当分片副本不足时,ES会等待更多的分片出现,默认等待一分钟,如果需要则可设置更早。

新索引默认有1个复制分片,这意味着为了满足quorum的要求需要两个活动的分片。 当然,这个默认设置将阻止我们在单一节点集群中进行操作。为了避开这个问题,规定数量只有在number_of_replicas大于一时才生效。

检索

文档能够从主分片或任意一个复制分片被检索。在主分片或复制分片上检索一个文档必要的顺序步骤:

  • 客户端给Node1发送get请求。
  • 节点使用文档的_id确定文档属于分片0,分片0对应的复制分片在3个节点上都有,此时它转发到Node2。
  • Node2返回文档给Node1然后返回客户端。

可能情况时一个被索引的文档已经存在于主分片上,却没有来得及同步到复制分片上,这时复制分片会报告文档未找到,主分片会成功返回文档。

一旦索引请求成功返回给用户,文档则在主切片和复制分片都是可用的。

批量更新

为什么bulk使用\n,则是因为批量中每个引用的文档属于不同的主分片,每个分片可能被分布于集群中的某个节点上。

意味着批量中的每个操作需要被转发到对应的分片和节点上。

而如果使用JSON,则需要:

  • 解析JSON为数组,包括文档数据,可能非常大。
  • 检查每个请求决定应该到哪个分片上。
  • 为每个分片创建应该请求的数组。
  • 序列化这些数组为内部传输格式。
  • 发送请求到每个分片。

即使可行,也会使得大量RAM承载本质上相同的数据,并需要JVM进行回收。因此ES使用网络缓冲区中一行行读取数据,使用换行符识别并进行解析,然后决定哪些分片来处理这个请求。

因此ES使用最小内存在进行。

深入分片

分片是底层的工作单元。但分片到底是什么,它怎样工作?

  • 为什么搜索是近实时的?
  • 为什么文档的CRUD操作是实时的?
  • ES怎样保证更新持久化,即使断电也不会丢失?
  • 为什么删除文档不会立即释放空间?
  • 什么是refresh,flush,optimize API,以及什么时候你该使用它们?

使文本可以被搜索

为了使得文本可以被全文搜索,一种可行的方案是使用倒排索引。倒排索引包含了出现在所有文档中唯一的词值或词的有序列表,以及每个词所属的文档列表。

倒排索引存储了比包含一个特定term的文档列表多的多的信息,它可能存储包含每个term的文档数量、一个term出现在指定文档中的频次,每个文档中term的顺序,每个文档的长度、平均长度。

这些统计信息使得ES知道哪些term更重要,哪些文档更重要,即相关性。即为了实现倒排索引,必须要找到集合中所有的文档。

在全文检索的早些时候,会为整个文档集合建立一个大索引,并且写入磁盘。只有新的索引准备好了,它就会替代旧的索引,最近的修改才可以被检索。

不可变性

写入磁盘的倒排索引是不可变的,其拥有如下好处:

  • 不需要锁。如果从来不需要更新一个索引,就不必担心多个程序同时尝试修改。
  • 一旦索引被读入文件系统的缓存(译者:在内存),它就一直在那儿,因为不会改变。只要文件系统缓存有足够的空间,大部分的读会直接访问内存而不是磁盘。这有助于性能提升。
  • 在索引的声明周期内,所有的其他缓存都可用。它们不需要在每次数据变化了都重建,因为数据不会变。
  • 写入单个大的倒排索引,可以压缩数据,较少磁盘IO和需要缓存索引的内存大小。

缺点:

  • 不可变,因此如果想建立一个新的文档,则必须重建整个索引

动态索引

是如何在保持不可变好处的同时更新倒排索引?即使用多个索引。

不是重写整个倒排索引,而是增加额外的索引反映最近的变化,每个倒排索引都可以按照顺序查询,从最老的开始,最后把结果聚合。

ES底层依赖的Lucene,引入了per-segment search的概念。一个是有完整功能的倒排索引,但是现在Lucene中的索引指的是段的集合,再加上提交点(commit point,包含所有段的文件)。

1567604324720

新的文档,在被写入磁盘的段前,首先写入内存区的索引缓存,即内存缓冲区有即将提交文档的Lucene索引:

1567669526235

索引 VS 分片

Lucene索引是ES中的分片,ES中的索引是分片的集合。在ES搜索索引时,它发送查询请求给该索引下的所有分片,然后过滤这些结果,聚合成全局的结果。

一个per-segment search如下工作:

  • 新的文档首先写入内存区的索引缓存。
  • 不时,这些buffer被提交。
    • 一个新的段-额外的倒排索引-写入磁盘。
    • 新的提交点写入磁盘,包含新段的名称。
    • 磁盘时fsynced(文件同步),所有写操作等待文件系统缓存同步到磁盘,确保它们可以被物理写入。
  • 新段被打开,它包含的文档可以被检索。
  • 内存的缓存被清除,等待接收新的文档。

1567606295972

当一个请求被接受,所有段依次查询。所有段上的Term统计信息被聚合,确保每个term和文档的相关性被正确计算。通过这种方式,新的文档以较小的代价加入索引。

删除和更新

段是不可变的,即文档既不能从旧的段中移除,旧的段也不能更新以反映文档最新的版本。相反,每一个提交点包括一个.del文件,包含了段上已经被删除的文档。

当一个文档被删除,它实际上只是在.del文件中被标记为删除,依然可以匹配查询,但是最终返回之前会被从结果中删除。

文档的更新操作是类似的:当一个文档被更新,旧版本的文档被标记为删除,新版本的文档在新的段中索引。也许该文档的不同版本都会匹配一个查询,但是更老版本会从结果中删除。

近实时搜索

由于per-segment search机制,索引和搜索一个文档之间是有延迟的,新的文档会在几分钟内被搜索,但这个时间太久。

由于一个新的段到磁盘需要fsync操作,确保段能够被物理地写入磁盘中,即时电源失效也不会丢失数据,但fsync是昂贵的,不能在每个文档被索引时就触发。因此需要一种更轻量级的方式使得新的文档可以被搜索。

在ES与磁盘中间加入缓存,即使用内存。在内存索引缓存中的文档被写入新的段,先将新的段首先写入缓存,之后在一段时间后同步到磁盘。即缓存内容写入了段中,但是uncommited。

Lucene允许新段写入打开,好让它们包括的文档可搜索,而不用执行一次全量提交。这是比提交更轻量的过程,可以经常操作,而不会影响性能:

1567607118204

refresh API

refresh是写入打开一个新段的轻量级过程。默认情况下,每个分片每秒自动刷新一次。因此ES是近实时的搜索,文档的改动一般在1s内可见。

持久化变更

没用fsync同步文件系统缓存到磁盘,我们不能确保电源失效,甚至正常退出应用后,数据的安全。为了ES的可靠性,需要确保变更持久化到磁盘。

全提交同步段到磁盘,写提交点,这会列出所有的已知的段。在重启,或重新打开索引时,ES使用这次提交点决定哪些段属于当前的分片。

当我们通过每秒的刷新获得近实时的搜索,我们依然需要定时地执行全提交确保能从失败中恢复。但是提交之间的文档怎么办?我们也不想丢失它们。

ES增加了事务日志(translog),来记录每次操作。

  • 当一个文档被索引,它被加入到内存缓存,同时加到事务日志。
  • refresh使得分片的进入如下状态,每秒分片都进行refresh。
    • 内存缓冲段的文档写入段中,但没有fsync。
    • 段被打开,使得新的文档可以被搜索。
    • 缓存被清除,但事务日志依然在。
  • 更多的文档加入到缓冲区,重复过程2。
  • 当日志很大的时候,新的日志会创建,会进行一次全提交:
    • 内存缓冲区的所有文档会写入到新段中。
    • 清除缓存。
    • 一个提交点写入硬盘。
    • 文件系统缓存通过fsync操作flush到硬盘。
    • 事务日志被清除。

事务日志记录了没有flush到硬盘的所有操作。当故障重启后,ES会用最近一次提交点从硬盘恢复所有已知的段,并且从日志里恢复所有的操作。

事务日志还用来提供实时的CRUD操作。当你尝试用ID进行CRUD时,它在检索相关段内的文档前会首先检查日志最新的改动。这意味着ES可以实时地获取文档的最新版本。

flush API

在ES中,进行一次提交并删除事务日志的操作叫做flush。分片每30分钟,或事务日志过大会进行一次flush操作。

当你要重启或关闭一个索引,flush该索引是很有用的。当ES尝试恢复或者重新打开一个索引时,它必须重放所有事务日志中的操作,所以日志越小,恢复速度越快。

合并段

通过每秒自动刷新创建新的段,用不了多久段的数量就爆炸了。有太多的段是一个问题。每个段消费文件句柄,内存,cpu资源。更重要的是,每次搜索请求都需要依次检查每个段。段越多,查询越慢。

ES通过后台合并段解决该问题,小段被合并为大段,再合并为更大的段。以两个提交的段和一个未提交的段合并为了一个更大的段所示:

  • 索引过程中,refresh会创建新的段,并打开它。
  • 合并过程会在后台选择一些小的段合并成大的段,这个过程不会中断索引和搜索。
  • 合并大的段会消耗很多IO和CPU,如果不检查会影响到搜索性能。默认情况下,ES会限制合并过程,这样搜索就可以有足够的资源进行。

1567676617810

合并后:

  • 新的段flush到了硬盘。
  • 新的提交点写入新的段,排除旧的段。
  • 新的段打开供搜索。
  • 旧的段被删除。

optimize API

optimize API描述为强制合并段API。它强制分片合并段以达到指定max_num_segments参数。这是为了减少段的数量(通常为1)达到提高搜索性能的目的。

核心

架构

搜索引擎

聚合

ES的聚合(aggregations)允许你在数据上生成复杂的分析统计,类似于Group by但是功能更强大。

例如找到所有职员中最大的共同点(兴趣爱好)是什么:

1
2
3
4
5
6
7
curl -H "Content-Type: application/json" -XGET "http://localhost:9200/megacorp/employee/_search?pretty" -d'{
"aggs":{
"all_interests":{
"terms":{"field": "interests"}
}
}
}

聚合也允许分级汇总。例如,让我们统计每种兴趣下职员的平均年龄。

聚合

Bucket Aggregation

Terms

Data Histogram

Range

Sampler

聚合分析

参考