1. 基本概念回顾
1.1. Node
节点是一个服务器,它是集群的一部分,存储数据,并参与集群的索引和搜索功能
节点有一个名称标识,该名称在缺省情况下是在启动时分配给节点的随机全局惟一标识符(UUID)
这个名称对于管理非常重要,因为你希望识别网络中的哪些服务器与Elasticsearch集群中的哪些节点相对应
默认情况下,每个节点都被设置为连接一个名为elasticsearch的集群,这意味着如果您在网络上启动多个节点,并且假设它们可以发现彼此,那么它们都会自动形成并连接一个名为elasticsearch的集群。
1.2. Index
文档(document)的集合就是索引(Index)
1.3. Type
当你想要在同一个index中存储不同类型的documents时,type用作这个index的一个逻辑分类/分区。比如,在一个索引中,用户数据是一个type,帖子是另一个type。在后续的版本中,一个index将不再允许创建多个types,而且整个types这个概念都将被删除。
(PS:type是index的一个逻辑分类(或者叫分区),在当前的版本中,它仍然用于在一个索引下区分不同类型的数据。但是,不建议这样做,因为在后续的版本中type这个概念将会被移除,也不允许一个索引中有多个类型。)
1.4. Document
一个document就是index中的一条记录,它是JSON格式的
1.5. Shards & Replicas (分片与副本)
索引可能存储大量数据,这些数据可能超过单个节点的硬件限制。例如,一个包含10亿个文档、占用1TB磁盘空间的索引可能不适用于单个节点的磁盘,或者速度太慢,无法单独处理来自单个节点的搜索请求。为了解决这个问题,Elasticsearch提供了将索引细分为多个碎片的功能,每个碎片称之为 shard。
创建索引时,您可以简单地定义所需的分片数量。每个分片本身就是一个功能完整且独立的“index”,可以驻留在集群中的任何节点上。
分片之所以重要,主要有两个原因:
- 允许水平扩容
- 允许分布式存储和并行操作,从而提高性能/吞吐量
shard如何分布以及如何将其文档聚合回搜索请求的机制完全由Elasticsearch管理,对于用户来说是透明的。
副本指的是分片的副本,是shard的复制
复制之所以重要,主要有两个原因:
- 在shard/node失败的时候,它提供高可用性。正因为如此,复制的shard(简称shard的副本)绝不会跟原始shard在同一个节点上
- 它允许扩展搜索量/吞吐量,因为搜索可以并行地在所有副本上执行
(It provides high availability in case a shard/node fails. For this reason, it is important to note that a replica shard is never allocated on the same node as the original/primary shard that it was copied from.)
总而言之,每个索引可以被分成多个碎片
索引还可以被复制0次(即没有副本)或更多次
复制之后,每个索引都将拥有主分片(原始分片)和 副本分片(主分片的副本)
1.6. 小结 & 回顾:
- node是一台服务器,表示集群中的节点
- document表示索引记录
- 一个index中不建议定义多个type
- 一个index可以有多个shard,每个shard可以有0个或多个副本
- original shard (原始shard,或者叫 primary shard)的复制成为副本shard,简称shard
- 主分片和副本决不会在同一个节点上
- 分片的好处主要有两个:第一,突破单台服务器的硬件限制;第二,可以并行操作,从而提高性能和吞吐量;(PS:其实跟kafka差不多)
- 副本的好处主要在于:第一,提供高可用;第二,并行提升性能和吞吐量
- 一个索引包含一个或多个分片,索引记录(即文档)数据存储在这些shard中,且一个文档只会存在于一个分片中
- 每个shard都是一个独立的功能完善的“index”,意思是它可以独立处理索引/搜索请求
(注意:本文中提到的分片指的是主分片(primary shard),而不是副本(replica shard))
2. 读写文档
在Elasticsearch中,每个索引都被划分为分片,每个分片可以有多个副本。这些副本称为复制组,在添加或删除文档时必须保持同步。如果我们做不到这一点,从一个副本中读取的结果将与从另一个副本中读取的结果非常不同。保持碎片副本同步并提供从中读取的服务的过程称为数据复制模型。
Elasticsearch的数据复制模型基于主备份模型,该模型中有一个主分片,以及从主分片那里复制的复制组,这些称之为复制分片。主(服务器)节点作为所有索引操作的主要入口点,它负责验证并确保操作是正确的。一旦主服务器接受了索引操作,主服务器还负责将该操作复制到其他副本。
2.1. Basic write model (基本的写模型)
在Elasticsearch中,每个索引操作首先会通过路由(通常是基于 document ID)解析到一个复制组(PS:这里解析到复制组的意思是定位到索引的哪个分片)。一旦确定了复制组,该操作将在内部转发到该组的当前主分片。主分片负责验证操作并将其转发到其他副本。由于副本可以是下线状态(PS:不在线),因此主分片不需要将该操作复制到所有副本。代替的,Elasticsearch维护应该接收操作的分片副本列表,这个列表称为同步副本,由主节点(PS:master node)维护。顾名思义,这些是一组“良好的”分片副本,它们保证处理了用户已确认的所有索引和删除操作。主服务器负责维护这个不变量(PS:指的是同步副本列表),因此必须将所有操作复制到这个集合中的每个副本。
主分片的基本流程如下:
- 校验输入操作,并且如果结构无效(PS:比如字段类型错误等等)时拒绝该操作
- 在本地执行操作,即索引或删除相关文档。这也将验证字段的内容,并在需要时拒绝
- 将操作转发到当前同步副本集中的每个副本。如果有多个副本,则并行执行此操作
- 一旦所有副本成功地执行了操作并响应了主副本(PS:只的主分片),主副本就向客户端确认请求已成功完成
2.1.1. 失败处理
如果主分片失败的话,那么它所在服务器节点将想主服务器节点(master node)发送一条关于该主分片的消息。索引操作将等待(默认情况下最多1分钟,具体看 Dynamic index settings)主节点(master)将其中一个副本提升为新的主副本。然后将操作转发到新的主分片进行处理。注意,主节点(master)还负责监视节点的健康状况,并可能决定主动降级主副本。当持有主副本的节点因网络问题与集群隔离时,通常会发生这种情况。
在主分片上成功执行操作之后,主分片在副本分片上执行操作时必须处理潜在的故障。这可能是由于副本上的实际故障,或者由于网络问题导致操作无法到达副本(或阻止副本响应)。所有这些最终结果是:作为同步复制集的一部分的副本会丢失即将被确认的操作。为了避免违反不变量,主分片的宿主服务器向主服务器发送一条消息,请求从同步副本集中删除有问题的分片。(PS:跟Kafka的副本同步有点儿像)一旦主服务器确认分片副本的移除后,主分片才会确认这个写请求操作。
在将操作转发到副本时,主分片将使用副本验证它仍然是活的主分片。如果主分片由于网络分区(或长时间GC)而被隔离,它可能会在意识到它已经降级之前继续处理输入的索引操作,然后将操作路由到新的主服务器。
2.2. Basic read model (基本的读模型)
Elasticsearch中的读取可以是非常轻量级的ID查找,也可以是具有复杂聚合(占用大量CPU资源)的大型搜索请求。主备份模型的优点之一是它保持所有分片副本相同,因此,一个同步副本就足以满足读取请求。
当一个节点接收到读请求时,该节点负责将其转发到持有相关分片的节点、整理响应并响应客户端。我们将该节点称为该请求的协调节点。
基本流程如下:
- 解析这个读请求到相关的分片
- 从这个分片的复制组中选择一个活的分片,这个活的分片可以是主分片,也可以是复制分片(副本)。默认情况下,Elasticsearch只是简单地在副本之间进行轮询。
- 发送分片级别的读请求给选中的副本
- 聚合结果并对客户端作出响应
2.2.1. 失败处理
当分片未能响应读取请求时,协调节点将从相同的复制组中选择另一个副本,并将分片级搜索请求发送到该副本。重复失败可能导致没有分片副本可用。在某些情况下,例如_search, Elasticsearch更喜欢快速响应,尽管会得到部分结果,而不是等待问题得到解决(部分结果在响应的_shards头中表示)。
2.2.2. 高效地读
在正常操作下,对每个相关复制组执行一次读操作。只有在失败的条件下,相同分片的多个副本才能执行相同的搜索。
2.2.3. 读未确认
由于先在主分片上写,然后复制请求,因此并发读取可能在确认更改之前就已经看到更改。
2.2.4. 默认两个副本
当只维护数据的两个副本时(number_of_replicas默认是1),该模型可以容错。
2.3. 小结 & 回顾
1、一个索引(index)有多个分片(shard),每个分片(primary shard)有多个副本(replica shard),主分片和它的副本称组成该分片的复制组
2、数据复制模型基于主备份模型
写
1、首先,计算数据在哪个分片上,这个过程称之为路由,通常是根据文档ID来计算的
2、将请求转到相应的节点上,主分片校验请求,然后在本地执行,随后将请求转发到同步副本集中的每个副本上,多个副本是并行执行的,当所有副本都执行完成以后,主副本向客户端作出响应
3、每个分片都有一个同步副本集,它由master节点维护
4、如果主分片操作失败,则该分片的节点立即向master节点发送一条关于该分片的消息,然后master节点将从它的副本中选出一个作为主分片,并且将请求转到新的主分片上执行
5、如果主分片操作成功,副本操作失败时,则改分片的节点会向master节点请求将改副本从同步副本集中移除,待master确认以后主分片就可以想要客户端了
6、如果主分片操作成功,但是由于网络分割,导致主分片与集群的连接断开了,那么在选出新的主分片之前元主分片继续处理请求,一旦选出新的分片后,原主分片不再试主分片,就不能再接收请求了
读
1、解析读请求到相应的分片(PS:路由,即计算数据在哪个分片上)
2、从分片的复制组中选择一个分片(PS:默认选择的算法是在副本之间轮询)
3、给选中的分片发请求
4、获取分片完成请求后的响应结果,并响应客户端
5、接收客户端请求的那个结点负责将请求转发到相应分片结点上,聚合各节点的响应结果,并响应客户端,该结点成为本次请求的协调节点
6、如果分片未能正常响应,协调节点将从相同的复制组中选择另一个副本,并将分片级搜索请求发送到该副本
7、一个读请求只会在一个副本上执行,只有当执行失败的时候,才会换另外一个副本上执行
8、并发读写可能会读到脏数据
3. Index API
下面这个例子,向"twitter"索引中插入一条id为1的文档,并且是在"_doc"类型下
curl -X PUT "localhost:9200/twitter/_doc/1" -H 'Content-Type: application/json' -d' { "user" : "kimchy", "post_date" : "2009-11-15T14:12:12", "message" : "trying out Elasticsearch" } '
响应结果可能是这样的:
{ "_shards" : { "total" : 2, "failed" : 0, "successful" : 2 }, "_index" : "twitter", "_type" : "_doc", "_id" : "1", "_version" : 1, "_seq_no" : 0, "_primary_term" : 1, "result" : "created" }
_shards字段中提供了关于索引操作的复制过程的信息
- total 表明索引操作应在多少分片(主分片和副本分片)上执行
- successful 表示成功执行的复制数量(PS:successful至少是1)
- failed 在索引操作在副本分片上执行失败的情况下,包含复制相关错误的数组
(PS:主分片 primary shard ;副本分片 replica shard )
3.1. 自动创建索引
如果在执行index api之前没有创建索引的话,那么该操作会自动创建索引,并自动创建一个动态类型映射(mapping)。
映射本身非常灵活,而且没有模式。新的字段和对象将自动添加到指定类型的映射定义中。
可以通过将 action.auto_create_index 设置为false来禁用自动创建索引,将 index.mapper.dynamic 设置为false来禁用自动映射。
3.2. 版本控制
每个被索引的文档都有一个版本号。版本号(version)在index API请求的响应中返回。版本号主要用于并发控制。
curl -X PUT "localhost:9200/twitter/_doc/1?version=2" -H 'Content-Type: application/json' -d' { "message" : "elasticsearch now has versioning support, double cool!" } '
上面这个例子,如果ID为1的文档的版本号是2,则更新其message自动为指定的内容,如果版本不是2,则不会执行更新操作,反而会报错。
现在执行失败,是因为当前ID为1的文档版本号是1,因此执行这个请求会失败。如果我们将该请求后面的版本号改成1,则会成功。
(PS:这其实就是CAS,比较并交换)
(PS:乐观锁)
默认情况下,内部版本号从1开始,并且在每次更新和删除的时候版本号都会递增。当然,也可以手动指定。为了可以手动指定版本号,应该将version_type指定为external。这个值必须是一个长整型的数值或者为0。
3.2.1. 版本类型
下面是一些不同的版本类型:
- internal 只索引给定版本号与文档存储的版本号相同的文档。(PS:换言之,只有当给定的版本号与文档存储的版本号相同时,才会索引该文档,这里索引操作指的是更新、删除)
- external 或者 external_gt 只索引那些文档存储的版本号比给定版本号小或者不存在的文档,同时给定的版本号会作为文档的新版本号。(PS:换言之,只有当给定的版本号比文档存储的版本号大或者文档不存在时,才会执行)
- external_gte 只索引给定版本号大于或等于存储文档的版本号的那些文件,如果文档不存在的话这个操作也会成功。(PS:换言之,只有当给定版本号大于或等于存储文档的版本号时,才会执行)
(PS:其实很好理解,无非就是给定的版本号与文档当前版本号的一个比较,internal是相等的时候才执行,external是大于的时候才执行,external_get是大于或等于的时候才执行)
3.3. 操作类型
索引操作也可以接受一个 op_type 参数用于强制创建操作。例如:
curl -X PUT "localhost:9200/twitter/_doc/1?op_type=create" -H 'Content-Type: application/json' -d' { "user" : "kimchy", "post_date" : "2009-11-15T14:12:12", "message" : "trying out Elasticsearch" } '
或者,另一种写法也可以
curl -X PUT "localhost:9200/twitter/_doc/1/_create" -H 'Content-Type: application/json' -d' { "user" : "kimchy", "post_date" : "2009-11-15T14:12:12", "message" : "trying out Elasticsearch" } '
上面的例子,如果文档已经存在,则操作失败
3.4. 自动ID生成
索引操作可以在不指定id的情况下执行。在这种情况下,将自动生成id。此外,op_type将自动设置为create。下面是一个例子(注意这里用POST代替PUT):
curl -X POST "localhost:9200/twitter/_doc/" -H 'Content-Type: application/json' -d' { "user" : "kimchy", "post_date" : "2009-11-15T14:12:12", "message" : "trying out Elasticsearch" } '
返回结果如下:
{ "_shards":{ "total":2, "failed":0, "successful":2 }, "_index":"twitter", "_type":"_doc", "_id":"W0tpsmIBdwcYyG50zbta", "_version":1, "_seq_no":0, "_primary_term":1, "result":"created" }
3.5. 路由
默认情况下,路由(routing)控制是通过文档ID的哈希值来做的。对于显式的控制,可以使用路由参数直接在每个操作的基础上指定输入到路由器使用的哈希函数中的值。例如:
curl -X POST "localhost:9200/twitter/_doc?routing=kimchy" -H 'Content-Type: application/json' -d' { "user" : "kimchy", "post_date" : "2009-11-15T14:12:12", "message" : "trying out Elasticsearch" } '
上面的例子中,_doc类型下的文档究竟被路由到哪个分片(shard)上是基于路由参数提供的值kimchy
3.6. 分布
索引操作根据它的路由定向到主分片,并在包含该分片的实际节点上执行。在主分片完成操作之后,如果需要,更新将被分发到适用的副本。
3.7. 超时
默认情况下,索引操作在主分片上最多等待1分钟,然后失败并以错误进行响应。可以使用timeout参数显式指定它等待的时间。
下面这个例子,设置超时时间是5分钟:
curl -X PUT "localhost:9200/twitter/_doc/1?timeout=5m" -H 'Content-Type: application/json' -d' { "user" : "kimchy", "post_date" : "2009-11-15T14:12:12", "message" : "trying out Elasticsearch" } '
4. Get API
下面这个例子从一个名字叫“twitter”的索引中,类型为“_doc”之下,获取id为0的JSON文档:
curl -X GET "localhost:9200/twitter/_doc/0"
其返回的结果可能是这样的:
{ "_index" : "twitter", "_type" : "_doc", "_id" : "0", "_version" : 1, "found": true, "_source" : { "user" : "kimchy", "date" : "2009-11-15T14:12:12", "likes": 0, "message" : "trying out Elasticsearch" } }
你还可以用HEAD操作单纯的只是检查某个文档是否存在,例如:
curl -X HEAD "localhost:9200/twitter/_doc/0"
默认情况下,Get 操作是实时的。也就是说,如果文档已经被更新,但是索引还没刷新,那么get操作会调用刷新
4.1. Source字段过滤
默认情况下,get操作的返回中包含 _source 字段,你可以手动关闭它。例如:
curl -X GET "localhost:9200/twitter/_doc/0?_source=false"
如果只想显示_source中的某些字段,可以这样简短的表示:
curl -X GET "localhost:9200/twitter/_doc/0?_source=*.id,retweeted"
4.2. 路由
curl -X GET "localhost:9200/twitter/_doc/2?routing=user1"
注意,如果你带了routing参数,而且还路由值还带错了,那么将找不到文档
5. ?refresh
Index , Update ,Delete等操作支持设置 refresh 参数来控制什么时候改变对搜索可见。(PS:意思了,对文档做了更新以后,什么时候这个更新可以被检索的时候看到)
refresh参数的值可以是下列之一:
- 空字符串 或者 true : 操作发生后立即刷新相关的主分片和副本分片(不是整个索引),以便更新后的文档立即出现在搜索结果中。
- wait_for : 在回复之前,等待请求所做的更改被刷新。这并不强制立即刷新,而是等待刷新发生。Elasticsearch自动刷新已更改每个索引的分片。refresh_interval,默认为1秒。
- false(默认) : 不要执行刷新相关操作。此请求所做的更改将在请求返回后的某个时点可见。
下面是一些例子:
curl -X PUT "localhost:9200/test/_doc/1?refresh" -H 'Content-Type: application/json' -d' {"test": "test"} ' curl -X PUT "localhost:9200/test/_doc/2?refresh=true" -H 'Content-Type: application/json' -d' {"test": "test"} '
创建一个文档,并立即刷新
curl -X PUT "localhost:9200/test/_doc/3" -H 'Content-Type: application/json' -d' {"test": "test"} ' curl -X PUT "localhost:9200/test/_doc/4?refresh=false" -H 'Content-Type: application/json' -d' {"test": "test"} '
只是创建一个文档,其它的什么也不做
curl -X PUT "localhost:9200/test/_doc/4?refresh=wait_for" -H 'Content-Type: application/json' -d' {"test": "test"} '
创建一个文档,并等待刷新
6. 参考
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-replication.html