在前面的章节,我们介绍了如何索引和查询数据,不过我们忽略了很多底层的技术细节, 例如文件是如何分布到集群的,又是如何从集群中获取的。 Elasticsearch 本意就是隐藏这些底层细节,让我们好专注在业务开发中,所以其实你不必了解这么深入也无妨。
在这个章节中,我们将深入探索这些核心的技术细节,这能帮助你更好地理解数据如何被存储到这个分布式系统中。
注意
这个章节包含了一些高级话题,上面也提到过,就算你不记住和理解所有的细节仍然能正常使用 Elasticsearch。 如果你有兴趣的话,这个章节可以作为你的课外兴趣读物,扩展你的知识面。
如果你在阅读这个章节的时候感到很吃力,也不用担心。 这个章节仅仅只是用来告诉你 Elasticsearch 是如何工作的, 将来在工作中如果你需要用到这个章节提供的知识,可以再回过头来翻阅。
之前我们说过,原始数据都存在主分片中,副分片只是主分片的一个副本,便于节点管理数据,规避宕机等丢失数据的风险。
问题?Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?
首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:
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 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。我们也会在扩容设计这一章中详细讨论为什么会有这样一种需求。
为了说明目的, 我们假设有一个集群由三个节点组成。 它包含一个叫 blogs 的索引,有两个主分片,每个主分片有两个副本分片。相同分片的副本不会放在同一节点,所以我们的集群看起来像 Figure 8, “有三个节点和一个索引的集群”。
我们可以发送请求到集群中的任一节点。 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 在下面的例子中,将所有的请求发送到 Node 1 ,我们将其称为 协调节点(coordinating node) 。
Tip
当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点。
新建、索引和删除 请求都是 写 操作, 必须在主分片上面完成之后才能被复制到相关的副本分片,如下图所示 Figure 9, “新建、索引和删除单个文档”.
以下是在主副分片和任何副本分片上面 成功新建,索引和删除文档所需要的步骤顺序:
。请求会被转发到
Node 3,因为分片 0 的主分片目前被分配在
Node 3 上。 在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。
有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。这些选项很少使用,因为Elasticsearch已经很快,但是为了完整起见,在这里阐述如下:
一致性
默认情况下,主分片 需要 规定数量(quorum),或大多数的分片 (其中分片副本可以是主分片或者副本分片)在写入操作时可用。这是为了防止将数据写入到网络分区的‘`背面’’。规定的数量定义公式如下:
int( (primary + number_of_replicas) / 2 ) + 1
允许的 一致性 值是 一个 (只是主分片)或者 所有
(主分片和副本分片), 或者默认的规定数量或者大多数的副本分片。
注意 number_of_replicas 是在索引中的设置指定的分片数,不是当前处理活动状态的副本分片数。如果你指定索引应该有三个副本分片,那规定数量计算公式是:
int( (primary + 3 replicas) / 2 ) + 1 = 3
但是如果只启动两个节点,则活动分片副本无法满足规定数量,并且您将无法索引和删除任何文档。
超时 如果没有足够的副本分片会发生什么? Elasticsearch会等待,希望更多的分片出现。默认情况下,它最多等待1分钟。 如果你需要,你可以使用 timeout 参数 使它更早终止: 100 100毫秒,30s 是30秒。
Note
新索引默认有 1 个副本分片,这意味着为满足 规定数量 应该 需要两个活动的分片副本。 但是,这些默认的设置会阻止我们在单一节点上做任何事情。为了避免这个问题,要求只有当 number_of_replicas 大于1的时候,规定数量才会执行。
在增删改的时候,也就是说如果像主例子那样,我们新建了一个索引,规定这个索引的主分片是2个,每个主分片有2个副分片,那么它能保证数据一致性的要求就是它有3 =(2+2)/2 + 1 个活动的副分片,如果我们这个时候只启动了两个节点,也就是这样子的运行:
Node1:R0 P1
Node2:R1 P0
此时只有2个副分片,这个时候无法保证在操作的时候保证主分片的值和所有副分片一致。
即最好启动3个节点。
可以从主分片或者从其它任意副本分片检索检索文档 ,如下图所示 Figure 10, “取回单个文档”.
以下是从主分片或者副本分片检索文档的步骤顺序:
1、客户端向 Node 1 发送获取请求。
2、节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node 2 。(为了读取请求,协调节点在每次请求的时候将选择不同的副本分片来达到负载均衡;通过轮询所有的副本分片。
)
3、Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。
在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
在查询时,也就是说不像增删改操作那样必须到主分片执行,可以轮询访问所有的包含文档的主副分片,如果副分片此时不存在,也会再去访问主分片返回文档。此时如果返回了文档,此文档在副分片也是可查的了。
如 Figure 11, “局部更新文档” 所示,update API 结合了先前说明的读取和写入模式 。
以下是部分更新一个文档的步骤:
update API 还接受在 新建、索引和删除文档 章节中介绍的 routing 、 replication 、 consistency 和 timeout 参数。
基于文档的复制
当主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果Elasticsearch仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。
mget 和 bulk API 的 模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。 它将整个多文档请求分解成 每个分片 的多文档请求,并且将这些请求并行转发到每个参与节点。
协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端,如 Figure 12, “使用 mget 取回多个文档” 所示。
以下是使用单个 mget 请求取回多个文档所需的步骤顺序:
1. 客户端向 Node 1 发送 mget 请求。
2. Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。
可以对 docs 数组中每个文档设置 routing 参数。
bulk API 按如下步骤顺序执行:
bulk API 还可以在整个批量请求的最顶层使用 consistency 参数,以及在每个请求中的元数据中使用 routing 参数。
当我们早些时候在代价较小的批量操作章节了解批量请求时, 您可能会问自己, “为什么 bulk API 需要有换行符的有趣格式,而不是发送包装在 JSON 数组中的请求,例如 mget API?” 。
为了回答这一点,我们需要解释一点背景:在批量请求中引用的每个文档可能属于不同的主分片, 每个文档可能被分配给集群中的任何节点。这意味着批量请求 bulk 中的每个 操作 都需要被转发到正确节点上的正确分片。
如果单个请求被包装在 JSON 数组中,那就意味着我们需要执行以下操作:
• 将 JSON 解析为数组(包括文档数据,可以非常大)
• 查看每个请求以确定应该去哪个分片
• 为每个分片创建一个请求数组
• 将这些数组序列化为内部传输格式
• 将请求发送到每个分片
这是可行的,但需要大量的 RAM 来存储原本相同的数据的副本,并将创建更多的数据结构,Java虚拟机(JVM)将不得不花费时间进行垃圾回收。
相反,Elasticsearch可以直接读取被网络缓冲区接收的原始数据。 它使用换行符字符来识别和解析小的 action/metadata 行来决定哪个分片应该处理每个请求。
这些原始请求会被直接转发到正确的分片。没有冗余的数据复制,没有浪费的数据结构。整个请求尽可能在最小的内存中处理。