ELK技术栈 - Elasticsearch 学习笔记(三)

217 阅读14分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

将文档路由到从库中

分片 = hash(routing) % 主分片数量

routing 值可以是任何的字符串, 默认是文档的 _id ,但也可以设置成一个自定义的值。 routing 字符串被传递到一个哈希函数以生成一个数字,然后除以索引的主分片的数量 得到 余数 remainder. 余数将总是在 0 到 主分片数量 - 1 之间, 它告诉了我们用以存放 一个特定 文档的分片编号。

解释了为什么主分片的数量只能在索引创建时设置、而且不能修改。 如果主分片的数量一 旦在日后进行了修改,所有之前的路由值都会无效,文档再也无法被找到。

所有文档 APIs ( get , index , delete , bulk , update 和 mget ) 都可以接受 routing 参数

主从库之间是如何通信的

为了便于说明,假设我们有由3个节点的集群。 它 包含一个名为blogs的索引,它有两个主分片。 每个主要分片都有 两个副本。 永远不会将同一分片的副本分配给同一节点,因此我们的群集也是如此 看起来像<>。

[[img-distrib]] .A cluster with three nodes and one index image::images/04-01_index.png["A cluster with three nodes and one index"]

我们可以将请求发送到集群中的任何节点。 每个节点都完全有能力服务任何要求。 每个节点都知道集群中每个文档的位置,因此也可以将请求直接转发到所需节点。 在下面的例子中,我们将发送所有的对节点1的请求,我们将其称为请求节点。

提示:发送请求时,最好通过循环遍历所有节点集群,以分散负载。

创建,索引和删除请求是写操作,必须成功

下面我们列出了成功创建,索引或删除a所需的步骤顺序 主要和任何副本分片上的文档,如<>中所述:

  1. 客户端向Node_1发送创建,索引或删除请求。
  2. 节点使用文档的_id来确定文档属于分片 0。它将请求转发到节点3,其中当前是碎片0的主副本 分配。
  3. 节点3在主分片上执行请求。如果成功,它会转发 并行请求节点1和节点2上的副本分片。一旦所有的复制品 分片报告成功,节点3向报告的请求节点报告成功 对客户的成功。

注意:

  1. 复制的默认值是sync。 这会导致主分片等待在返回之前来自副本分片的成功响应。
  2. 如果将复制设置为异步,则会立即将成功返回给客户端
  3. 请求已在主分片上执行。 它仍然会将请求转发给复制品,但你不知道复制品是否成功。
  4. 建议使用默认的同步复制,因为可以重载Elasticsearch通过发送太多请求而不等待他们的

默认情况下,主分片需要法定数量或大多数分片副本(分片所在的位置)复制可以是主要或副本碎片)甚至在尝试写入之前可用操作。这是为了防止将数据写入网络分区的“错误一侧”。一个 仲裁定义为:

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

允许的一致性值是一个(只是主要分片),所有(主要的)和所有副本)或默认仲裁或大多数碎片副本。请注意,number_of_replicas是索引设置中指定的副本数,不是当前活动的副本数量。如果您指定了索引应该有3个副本,然后是法定人数:

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

默认情况下,新索引具有1个分片,这意味着应该有两个活动分片副本为了满足法定人数的需要而需要。 但是,这些默认设置会阻止我们对单节点集群做任何有用的事情。 为了避免这个问题,要求

a quorum is only enforced when number_of_replicas is greater than 1 .

仅当number_of_replicas大于1时才会强制执行仲裁

获取一个文档

下面我们列出从主服务器或副本服务器检索文档的步骤顺序 碎片,如<>所示:

  1. 客户端向节点1发送get请求。
  2. 节点使用文档的_id来确定文档属于分片0。所有三个节点上都存在shard 0的副本。在这个场合,它转发了请求节点2。
  3. 节点2将文档返回到节点1,节点1将文档返回给客户端。

对于读取请求,请求节点将在每个请求上选择不同的分片副本为了平衡负载 - 它循环遍历所有碎片副本。

文档可能已在主分片上编入索引但尚未编入索引复制到副本分片。在这种情况下,副本可能会报告文档没有存在,而主要成功返回文档。

更新文档中的一部分

下面我们列出用于对文档执行部分更新的步骤序列,如 在<>中描述:

1.客户端向Node_1发送更新请求。

2.它将请求转发到节点3,在节点3中分配主分片。

3.节点3从主分片中检索文档,更改中的JSON_source字段,并尝试在主分片上重新索引文档。如果是文件已被另一个进程更改,它重试第3步放弃之前retry_on_conflict次。

4.如果节点3已成功更新文档,则转发新文档与节点1和节点2上的副本分片并行的文档版本重建索引。一旦所有副本分片报告成功,节点3就会报告成功请求节点,向客户端报告成功。更新API还接受路由,复制,一致性和超时 <>中解释的参数

NOTE

当主分片将更改转发到其副本分片时,它不会转发更新请求。 相反,它转发整个文档的新版本。 请记住这些更改将异步转发到副本分片,并且无法保证他们将按照他们发送的顺序到达。 如果Elasticsearch只转发了更改,可能会以错误的顺序应用更改,从而导致损坏文献。

多文档模式

mget和批量API的模式与单个文档的模式类似。不同之处在于请求节点知道每个文档在哪个分片中存在。它将多文档请求分解为每个分片的多文档请求,以及将这些并行转发到每个参与节点。

一旦它从每个节点接收到答案,它就会将他们的响应整理成一个响应,它返回给客户端。

[[img-distrib-mget]] .Retrieving multiple documents with mget image::images/04- 05_mget.png["Retrieving multiple documents with mget"]

下面我们列出了使用单个文档检索多个文档所需的步骤顺序

mget请求,如<>所示:

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

2.节点1为每个分片构建一个多重获取请求,并将这些请求并行转发托管每个所需主要或副本分片的节点。一旦所有回复都是收到后,节点1构建响应并将其返回给客户端。可以为docs数组中的每个文档和首选项设置路由参数可以为顶级mget请求设置参数。

下面我们列出执行多个create,index所需的步骤序列,

在单个批量请求中删除和更新请求,如<>中所述:

1.客户端向Node_1发送批量请求。

2.节点1为每个分片构建一个批量请求,并将这些请求并行转发给托管每个节点的节点都涉及主分片。

3.主分片一个接一个地连续执行每个动作。作为每个动作成功后,主要将新文档(或删除)转发到其副本分片中并行,然后继续下一个动作。一旦所有副本分片报告所有人都成功动作,节点向请求节点报告成功,该节点整理响应并将它们返回给客户端。批量API还接受顶级的复制和一致性参数

对于整个批量请求,以及每个请求的元数据中的路由参数。

Why the funny format?

当我们在<>之前了解批量请求时,您可能会问自己:为什么批量API是否需要带有换行符的有趣格式,而不仅仅是发送包含在JSON数组中的请求,比如mget` API?''

要回答这个问题,我们需要解释一下背景:

批量请求中引用的每个文档可以属于不同的主分片,其中的一部分可以分配给集群中的任何节点。这意味着每一个动作,批量请求内部需要转发到正确节点上的正确分片。如果单个请求被包装在JSON数组中,那就意味着我们会这样做

需要:

  1. 将JSON解析为数组(包括文档数据,可能非常大)
  2. 查看每个请求以确定它应该去哪个分片
  3. 为每个分片创建一组请求
  4. 将这些数组序列化为内部传输格式
  5. 将请求发送到每个分片

它可以工作,但需要大量的RAM来保存基本相同数据的副本,并且会创建更多的数据结构,JVM必须花费时间垃圾 收集。

相反,Elasticsearch会进入原始请求所在的网络缓冲区已收到并直接读取数据。它使用换行符来识别和只解析小动作/元数据行,以决定哪个分片应该处理每个分片请求。

这些原始请求将直接转发到正确的分片。没有多余的复制数据,没有浪费的数据结构。整个请求过程在中处理最小的内存量。

搜索

  • 类似于 年龄 、 性别 、 加入日期 等结构化数据,类似于在SQL中进行查询。
  • 全文搜索,查找整个文档中匹配关键字的内容,并根据相关性
  • 或者结合两者。

虽然很多搜索操作是安装好Elasticsearch就可以用的,但是想发挥它的潜力,你需要明白以 下内容:

名字说明
映射 (Mapping)每个字段中的数据如何被解释
统计 (Analysis)可搜索的全文是如何被处理的
查询 (Query DSL)Elasticsearch使用的灵活强的查询语言

空白搜索

搜索API最常用的一种形式就是空白搜索,也就是不加任何查询条件的,只是返回集群中所有 文档的搜索。

GET /_search

返回内容如下(有删减):

{
    "hits" : {
    "total" : 14,
    "hits" : [
        {
            "_index": "us",
            "_type": "tweet",
            "_id": "7",
            "_score": 1,
            "_source": {
                "date": "2014-09-17",
                "name": "John Smith",
                "tweet": "The Query DSL is really powerful and flexible",
                "user_id": 2
        	}
        },
        ... 9 个结果被隐藏 ...
        ],
        "max_score" : 1
        },
            "took" : 4,
            "_shards" : {
                "failed" : 0,
                "successful" : 10,
                "total" : 10
        },
        "timed_out" : false
}

hits

返回内容中最重要的内容就是 hits ,它指明了匹配查询的文档的 总数 , hits 数组里则会包 含前十个匹配文档——也就是搜索结果。

hits 数组中的每一条结果都包含了文档的 _index , _type 以及 _id 信息,以及 _source 字 段。这也就意味着你可以直接从搜索结果中获取到整个文档的内容。这与其他搜索引擎只返 回给你文档编号,还需要自己去获取文档是截然不同的。

每一个元素还拥有一个 _score 字段。这个是相关性评分,这个数值表示当前文档与查询的匹 配程度。通常来说,搜索结果会先返回最匹配的文档,也就是说它们会按照 _score 由高至低 进行排列。在这个例子中,我们并没有声明任何查询,因此 _score 就都会返回 1

max_score 数值会显示所有匹配文档中的 _score 的最大值。

took

took 数值告诉我们执行这次搜索请求所耗费的时间有多少毫秒。

shards

_shards 告诉了我们参与查询分片的总数,以及有多少 successfulfailed 。通常情况下 我们是不会得到失败的反馈,但是有的时候它会发生。如果我们的服务器突然出现了重大事 故,然后我们丢失了同一个分片中主从两个版本的数据。在查询请求中,无法提供可用的备 份。这种情况下,Elasticsearch就会返回`failed提示,但是它还会继续返回剩下的内容。

timeout

timed_out 数值告诉了我们查询是否超时。通常,搜索请求不会超时。如果相比完整的结果 你更需要的是快速的响应时间,这是你可以指定 timeout 值,例如 10 、 "10ms" (10毫秒) 或者 "1s" (1秒钟):

GET /_search?timeout=10ms

Elasticsearch会尽可能地返回你指定时间内它所查到的内容

Timeout并不是终止者

这里应该强调一下 timeout 并不会终止查询,它只是会在你指定的时间内返回当时已经查询 到的数据,然后关闭连接。在后台,其他的查询可能会依旧继续,尽管查询结果已经被返回 了。 使用超时是因为你要保障你的品质,并不是因为你需要终止你的查询。

多索引,多类型

当我们没有特别指定一个索引或者类型的时候,我们将会搜索整个集群中的所有文档。 Elasticsearch会把搜索请求转发给集群中的每一个主从分片,然后按照结果的相关性得到前 十名,并将它们返回给我们。

URL说明
/_search搜索所有的索引和类型
/gb/_search搜索索引 gb 中的所有类型
/gb,us/_search搜索索引 gb 以及 us 中的所有类型
/g*,u*/_search搜索所有以 g 或 u 开头的索引中的所有类型
/gb/user/_search搜索索引 gb 中类型 user 内的所有文档
/gb,us/user,tweet/_search搜索索引 gb 和 索引 us 中类型 user 以及类型 tweet 内的所有文档
/_all/user,tweet/_search搜索所有索引中类型为 user 以及 tweet 内的所有文档

搜索一个拥有五个主分片的索引与搜索五个都只拥有一个主分片是完全一样的。

分页

与SQL使用 LIMIT 来控制单“页”数量类似,Elasticsearch使用的是 from 以及 size 两个参 数:

参数说明
size每次返回多少个结果,默认值为 10
from忽略最初的几条结果,默认值为 0

假设每页显示5条结果,那么1至3页的请求就是:

GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10

当心不要一次请求过多或者页码过大的结果。它们会在返回前排序。一个请求会经过多个分 片。每个分片都会生成自己的排序结果。然后再进行集中整理,以确保最终结果的正确性。

分布式系统中的大页码页面 为了说明白为什么页码过大的请求会产生问题,我们就先预想一下我们在搜索一个拥有5个主 分片的索引。当我们请求第一页搜索的时候,每个分片产生自己前十名,然后将它们返回给 请求节点,然后这个节点会将50条结果重新排序以产生最终的前十名。 现在想想一下我们想获得第1,000页,也就是第10,001到第10,010条结果,与之前同理,每一 个分片都会先产生自己的前10,010名,然后请求节点统一处理这50,050条结果,然后再丢弃 掉其中的50,040条!

现在你应该明白了,在分布式系统中,大页码请求所消耗的系统资源是呈指数式增长的。这 也是为什么网络搜索引擎不会提供超过1,000条搜索结果的原因。

精简 搜索

搜索的API分为两种:其一是通过参数来传递查询的“精简版”查询语句(query string),还有 一种是通过JSON来传达丰富的查询的完整版请求体(request body),这种搜索语言被称为 查询DSL。

查询语句在行命令中运行点对点查询的时候非常实用。比如我想要查询所有 tweet 类型中, 所有 tweet 字段为 "elasticsearch" 的文档:

GET /_all/tweet/_search?q=tweet:elasticsearch

下一个查询是想要寻找 name 字段为 "john" 且 tweet 字段为 "mary" 的文档,实际的查询就 是:

+name:john +tweet:mary

但是经过百分号编码(percent encoding)处理后,会让它看起来稍显神秘:

GET /_search?q=%2Bname%3Ajohn+%2Btweet%3Amary

前缀 "+" 表示必须要满足我们的查询匹配条件,而前缀 "-" 则表示绝对不能匹配条件。没 有 + 或者 - 的表示可选条件。匹配的越多,文档的相关性就越大。

字段 _all

下面这条简单的搜索将会返回所有包含 "mary" 字符的文档:

GET /_search?q=mary

在之前的例子中,我们搜索 tweet 或者 name 中的文字。然而,搜索的结果显示 "mary" 在三 个不同的字段中:

  • 用户的名字为"Mary"

  • 6个"Mary"发送的推文

  • 1个"@mary“

那么Elasticsearch是如何找到三个不同字段中的内容呢?

当我们在索引一个文档的时候,Elasticsearch会将所有字段的数值都汇总到一个大的字符串 ,并将它索引成一个特殊的字段 _all :

{
    "tweet": "However did I manage before Elasticsearch?",
    "date": "2014-09-14",
    "name": "Mary Jones",
    "user_id": 1
}

就好像我们已经添加了一个叫做 _all 的字段:

"However did I manage before Elasticsearch? 2014-09-14 Mary Jones 1"

除非指定了字段名,不然查询语句就会搜索字段 _all

TIP: 在你刚开始创建程序的时候你可能会经常使用 _all 这个字段。但是慢慢的,你可能就会 在请求中指定字段。当字段 _all 已经没有使用价值的时候,那就可以将它关掉。之后的《字 段all》一节中将会有介绍

更加复杂的查询

再实现一个查询:

  • 字段 name 包含 "mary" 或 "john"
  • date 大于 2014-09-10
  • _all 字段中包含 "aggregations" 或 "geo"
+name:(mary john) +date:>2014-09-10 +(aggregations geo)

最终处理完的语句可读性可能很差:

?q=%2Bname%3A(mary+john)+%2Bdate%3A%3E2014-09-10+%2B(aggregations+geo)

最后要提一句,任何用户都可以通过查询语句来访问臃肿的查询,或许会得到一些私人的信息,或许会通过大量的运算将你的集群压垮!

TIP 出于以上原因,我们不建议你将查询语句直接暴露给用户,除非是你信任的可以访问数据与 集群的权限用户。

映射与统计

当我们在进行搜索的事情,我们会发现有一些奇怪的事情。比如有一些内容似乎是被打破 了:在我们的索引中有12条推文,中有一个包含了 2014-09-15 这个日期,但是看看下面的查 询结果中的总数量:

GET /_search?q=2014 # 12 results
GET /_search?q=2014-09-15 # 12 results !
GET /_search?q=date:2014-09-15 # 1 result
GET /_search?q=date:2014 # 0 results !

为什么我们使用字段 _all 搜索全年就会返回所有推文,而使用字段 date 搜索年份却没有结 果呢?为什么使用两者所得到的结果是不同的?

推测大概是因为我们的数据在 _all 和 date 在索引时没有被相同处理。我们来看看 Elasticsearch是如何处理我们的文档结构的。我们可以对 gb 的 tweet 使用mapping请求:

GET /gb/_mapping/tweet
{
    "gb": {
        "mappings": {
            "tweet": {
                "properties": {
                    "date": {
                    	"type": "date",
                    	"format": "dateOptionalTime"
                    },
                    "name": {
                    	"type": "string"
                    },
                    "tweet": {
                    	"type": "string"
                    },
                    "user_id": {
                    	"type": "long"
                    }
                }
            }
        }
    }
}

Elasticsearch会根据系统自动判断字段类型并生成一个映射。返回结果告诉我们 date 字段被 识别成了 date 类型。 _all 没有出现是因为他是默认字段,但是我们知道字段 _all 实际上 是 string 类型的。

所以类型为 date 的字段和类型为 string 的字段的索引方式是不同的。