ES四| 分布式增删查改

免责声明:本专栏的内容都是在读了《Elasticsearch权威指南( 中文版)》书籍后整理的读后感及部分内容笔记,具体内容请查阅原著内容。

路由文档到分片

当你索引一个文档, 它被存储在单独一个主分片上。进程不能是随机的, 因为我们将来要检索文档。 事实上, 它根据一个简单的算法决定:

shard = hash(routing) % number_of_primary_shards

routing 值是一个任意字符串, 它默认是 _id 但也可以自定义。 这个 routing 字符串通过哈希函数生成一个数字, 然后除以主切片的数量得到一个余数(remainder), 余数的范围永远是 0 到 number_of_primary_shards - 1 , 这个数字就是特定文档所在的分片。这也解释了为什么主分片的数量只能在创建索引时定义且不能修改: 如果主分片的数量在未来改变了, 所有先前的路由值就失效了, 文档也就永远找不到了。所有的文档API( get 、 index 、 delete 、 bulk 、 update 、 mget ) 都接收一个 routing 参数, 它用来自定义文档到分片的映射。 自定义路由值可以确保所有相关文档——例如属于同一个人的文档——被保存在同一分片上。

主分片和复制分片如何交互

我们假设有三个节点的集群。 它包含一个叫做 bblogs 的索引并拥有两个主分片。 每个主分片有两个复制分片。 相同的分片不会放在同一个节点上, 所以我们的集群是这样的:

我们能够发送请求给集群中任意一个节点。 每个节点都有能力处理任意请求。 每个节点都知道任意文档所在的节点, 所以也可以将请求转发到需要的节点。 下面的例子中, 我们将发送所有请求给 Node 1 , 这个节点我们将会称之为请求节点(requesting node)。

新建、 索引和删除文档

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

下面我们罗列在主分片和复制分片上成功新建、 索引或删除一个文档必要的顺序步骤:

1. 客户端给 Node 1 发送新建、 索引或删除请求。

2. 节点使用文档的 _id 确定文档属于分片 0 。 它转发请求到 Node 3 , 分片 0 位于这个节点上。

3. Node 3 在主分片上执行请求, 如果成功, 它转发请求到相应的位于 Node 1 和 Node 2 的复制节点上。 当所有的复制节点报告成功, Node 3 报告成功到请求的节点, 请求的节点再报告给客户端。

客户端接收到成功响应的时候, 文档的修改已经被应用于主分片和所有的复制分片。 修改生效了。

此外,有很多可选的请求参数允许你更改这一过程。

  • replication

复制默认的值是 sync 。 这将导致主分片得到复制分片的成功响应后才返回。如果你设置 replication 为 async , 请求在主分片上被执行后就会返回给客户端。 它依旧会转发请求给复制节点, 但你将不知道复制节点成功与否。上面的这个选项不建议使用。 默认的 sync 复制允许Elasticsearch强制反馈传输。 async 复制可能会因为在不等待其它分片就绪的情况下发送过多的请求而使Elasticsearch过载。

  • consistency

默认主分片在尝试写入时需要规定数量(quorum)或过半的分片( 可以是主节点或复制节点)可用。 这是防止数据被写入到错的网络分区。 规定的数量计算公式如下:

int( (primary + number_of_replicas) / 2 ) + 1

consistency 允许的值为 one ( 只有一个主分片) , all ( 所有主分片和复制分片) 或者默认的 quorum 或过半分片。注意 number_of_replicas 是在索引中的的设置, 用来定义复制分片的数量, 而不是现在活动的复制节点的数量。 如果你定义了索引有3个复制节点, 那规定数量是:

int( (primary + 3 replicas) / 2 ) + 1 = 3

但如果你只有2个节点, 那你的活动分片不够规定数量, 也就不能索引或删除任何文档。

  • timeout

当分片副本不足时会怎样? Elasticsearch会等待更多的分片出现。 默认等待一分钟。 如果需要, 你可以设置 timeout 参数让它终止的更早: 100 表示100毫秒, 30s 表示30秒。

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

检索文档

文档能够从主分片或任意一个复制分片被检索。

下面我们罗列在主分片或复制分片上检索一个文档必要的顺序步骤:

1. 客户端给 Node 1 发送get请求。

2. 节点使用文档的 _id 确定文档属于分片 0 。 分片 0 对应的复制分片在三个节点上都有。 此时, 它转发请求到 Node 2 。

3. Node 2 返回文档(document)给 Node 1 然后返回给客户端。

对于读请求, 为了平衡负载, 请求节点会为每个请求选择不同的分片——它会循环所有分片副本。可能的情况是, 一个被索引的文档已经存在于主分片上却还没来得及同步到复制分片上。 这时复制分片会报告文档未找到, 主分片会成功返回文档。 一旦索引请求成功返回给用户, 文档则在主分片和复制分片都是可用的。

文档局部更新

使用 update API, 我们可以使用一个请求来实现局部更新, 例如增加数量的操作。我们也说过文档是不可变的——它们不能被更改, 只能被替换。 update API必须遵循相同的规则。 表面看来, 我们似乎是局部更新了文档的位置, 内部却是像我们之前说的一样简单的使用 update API处理相同的检索-修改-重建索引流程, 我们也减少了其他进程可能导致冲突的修改。最简单的 update 请求表单接受一个局部文档参数 doc , 它会合并到现有文档中——对象合并在一起, 存在的标量字段被覆盖, 新字段被添加。 举个例子, 我们可以使用以下请求为博客添加一个 tags 字段和一个 views 字段:

POST /website/blog/1/_update
{
    "doc" : {
        "tags" : [ "testing" ],
            "views": 0
    }
}

如果请求成功, 我们将看到类似 index 请求的响应结果:

{
    "_index" : "website",
    "_id" : "1",
    "_type" : "blog",
    "_version" : 3
}

检索文档文档显示被更新的 _source 字段:

{
    "_index": "website",
    "_type": "blog",
    "_id": "1",
    "_version": 3,
    "found": true,
    "_source": {
        "title": "My first blog entry",
        "text": "Starting to get the hang of this...",
        "tags": [ "testing" ], <1>
        "views": 0 <1>  #我们新添加的字段已经被添加到 _source 字段中
    }
}

update API 结合了之前提到的读和写的模式。

下面我们罗列执行局部更新必要的顺序步骤:

1. 客户端给 Node 1 发送更新请求。

2. 它转发请求到主分片所在节点 Node 3 。

3. Node 3 从主分片检索出文档, 修改 _source 字段的JSON, 然后在主分片上重建索引。如果有其他进程修改了文档, 它以 retry_on_conflict 设置的次数重复步骤3, 都未成功则放弃。

4. 如果 Node 3 成功更新文档, 它同时转发文档的新版本到 Node 1 和 Node 2 上的复制节点以重建索引。 当所有复制节点报告成功, Node 3 返回成功给请求节点, 然后返回给客户端。

基于文档的复制
当主分片转发更改给复制分片时, 并不是转发更新请求, 而是转发整个文档的新版本。记住这些修改转发到复制节点是异步的, 它们并不能保证到达的顺序与发送相同。 如果Elasticsearch转发的仅仅是修改请求, 修改的顺序可能是错误的, 那得到的就是个损坏的文档。

更新可能不存在的文档

想象我们要在Elasticsearch中存储浏览量计数器。 每当有用户访问页面, 我们增加这个页面的浏览量。 但如果这是个新页面, 我们并不确定这个计数器存在与否。 当我们试图更新一个不存在的文档, 更新将失败。在这种情况下, 我们可以使用 upsert 参数定义文档来使其不存在时被创建。

POST /website/pageviews/1/_update
{
    "script" : "ctx._source.views+=1",
    "upsert": {
        "views": 1
    }
}

第一次执行这个请求, upsert 值被索引为一个新文档, 初始化 views 字段为 1 .接下来文档已经存在, 所以 script 被更新代替, 增加 views 数量。

更新和冲突

这这一节的介绍中, 我们介绍了如何在检索(retrieve)和重建索引(reindex)中保持更小的窗口, 如何减少冲突性变更发生的概率, 不过这些无法被完全避免, 像一个其他进程在 update 进行重建索引时修改了文档这种情况依旧可能发生。

为了避免丢失数据, update API在检索(retrieve)阶段检索文档的当前 _version , 然后在重建索引(reindex)阶段通过 index 请求提交。 如果其他进程在检索(retrieve)和重建索引(reindex)阶段修改了文档, _version 将不能被匹配, 然后更新失败。

对于多用户的局部更新, 文档被修改了并不要紧。 例如, 两个进程都要增加页面浏览量, 增加的顺序我们并不关心——如果冲突发生, 我们唯一要做的仅仅是重新尝试更新既可。这些可以通过 retry_on_conflict 参数设置重试次数来自动完成, 这样 update 操作将会在发生错误前重试——这个值默认为0。

POST /website/pageviews/1/_update?retry_on_conflict=5 <1>
{
    "script" : "ctx._source.views+=1",
    "upsert": {
        "views": 0
    }
}

这适用于像增加计数这种顺序无关的操作, 但是还有一种顺序非常重要的情况。 例如 indexAPI, 使用“保留最后更新(last-write-wins)”的 update API, 但它依旧接受一个 version 参数以允许你使用乐观并发控制(optimistic concurrency control)来指定你要更细文档的版本。

多文档模式

mget 和 bulk API与单独的文档类似。 差别是请求节点知道每个文档所在的分片。 它把多文档请求拆成每个分片的对文档请求, 然后转发每个参与的节点。一旦接收到每个节点的应答, 然后整理这些响应组合为一个单独的响应, 最后返回给客户端。

下面我们将罗列通过一个 mget 请求检索多个文档的顺序步骤:

1. 客户端向 Node 1 发送 mget 请求。

2. Node 1 为每个分片构建一个多条数据检索请求, 然后转发到这些请求所需的主分片或复制分片上。 当所有回复被接收, Node 1 构建响应并返回给客户端。

routing 参数可以被 docs 中的每个文档设置。

下面我们将罗列使用一个 bulk 执行多个 create 、 index 、 delete 和 update 请求的顺序步骤:

1. 客户端向 Node 1 发送 bulk 请求。

2. Node 1 为每个分片构建批量请求, 然后转发到这些请求所需的主分片上。

3. 主分片一个接一个的按序执行操作。 当一个操作执行完, 主分片转发新文档( 或者删除部分) 给对应的复制节点, 然后执行下一个操作。 一旦所有复制节点报告所有操作已成功完成, 节点就报告success给请求节点, 后者(请求节点)整理响应并返回给客户端。

bulk API还可以在最上层使用 replication 和 consistency 参数, routing 参数则在每个请求的元数据中使用。

总结

我们学习了将数据放入索引然后检索它们的所有方法,本文主要关于数据是如何在集群中分布和获取的相关技术细节。知道数据在Elasticsearch如何分布它就会很好的工作。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值