0%

ElasticSearch的批处理方法

摘要:使用命令行,实现对 ElasticSearch 数据的批量写入。

开销较小的批处理方式

mget 可以使我们一次取回多个文档同样的方式, bulk API 允许在单个步骤中进行多次 createindexupdatedelete 请求。 如果你需要索引一个数据流比如日志事件,它可以排队和索引数百或数千批次。

bulk 与其他的请求体格式稍有不同,如下所示:

1
2
3
4
5
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...

这种格式类似一个有效的单行 JSON 文档 ,它通过换行符 (\n) 连接到一起,被称为NDJSON。注意两个要点:

  • 每行一定要以换行符 (\n) 结尾, 包括最后一行 。这些换行符被用作一个标记,可以有效分隔行。
  • 这些行不能包含未转义的换行符,因为他们将会对解析造成干扰。这意味着这个 JSON 能使用 pretty 参数打印。

action/metadata 行指定 哪一个文档什么操作

action 必须是以下选项之一:

create

如果文档不存在,那么就创建它。详情请见 创建新文档

index

创建一个新文档或者替换一个现有的文档。详情请见 索引文档更新整个文档

update

部分更新一个文档。详情请见 文档的部分更新

delete

删除一个文档。详情请见 删除文档

metadata 应该指定被索引、创建、更新或者删除的文档的 _index_type_id

例如,一个 delete 请求看起来是这样的:

1
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}

request body 行由文档的 _source 本身组成—文档包含的字段和值。它是 indexcreate 操作所必需的,这是有道理的:你必须提供文档以索引。

它也是 update 操作所必需的,并且应该包含你传递给 update API 的相同请求体: docupsertscript 等等。 删除操作不需要 request body 行。

1
2
{ "create":  { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }

如果不指定 _id ,将会自动生成一个 ID :

1
2
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }

为了把所有的操作组合在一起,一个完整的 bulk 请求 有以下形式:

1
2
3
4
5
6
7
8
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
{ "doc" : {"title" : "My updated blog post"} }

请注意 delete 动作不能有请求体, 它后面跟着的是另外一个操作。

谨记最后一个换行符不要落下。

这个 Elasticsearch 响应包含 items 数组,这个数组的内容是以请求的顺序列出来的每个请求的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"took": 4,
"errors": false,
"items": [
{ "delete": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 2,
"status": 200,
"found": true
}},
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 3,
"status": 201
}},
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "EiwfApScQiiy7TIKFxRCTw",
"_version": 1,
"status": 201
}},
{ "update": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 4,
"status": 200
}}
]
}

每个子请求都是独立执行,因此某个子请求的失败不会对其他子请求的成功与否造成影响。 如果其中任何子请求失败,最顶层的 error 标志被设置为 true ,并且在相应的请求报告出错误明细:

1
2
3
4
5
POST /_bulk
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "Cannot create - it already exists" }
{ "index": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "But we can update it" }

在响应中,我们看到 create 文档 123 失败,因为它已经存在。但是随后的 index 请求,也是对文档 123 操作,就成功了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"took": 3,
"errors": true,
"items": [
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "123",
"status": 409,
"error": "DocumentAlreadyExistsException
[[website][4] [blog][123]:
document already exists]"
}},
{ "index": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 5,
"status": 200
}}
]
}

一个或者多个请求失败。

这个请求的 HTTP 状态码报告为 409 CONFLICT

解释为什么请求失败的错误信息。

第二个请求成功,返回 HTTP 状态码 200 OK

这也意味着 bulk 请求不是原子的: 不能用它来实现事务控制。每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求。

不需要重复指定 index 和 type

也许你正在批量索引日志数据到相同的 indextype 中。 但为每一个文档指定相同的元数据是一种浪费。相反,可以像 mget API 一样,在 bulk 请求的 URL 中接收默认的 /_index 或者 /_index/_type

1
2
3
POST /website/_bulk
{ "index": { "_type": "log" }}
{ "event": "User logged in" }

你仍然可以覆盖元数据行中的 _index_type , 但是它将使用 URL 中的这些元数据值作为默认值:

1
2
3
4
5
POST /website/log/_bulk
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}
{ "title": "Overriding the default type" }

多大是太大了?

整个批量请求都需要由接收到请求的节点加载到内存中,因此该请求越大,其他请求所能获得的内存就越少。 批量请求的大小有一个最佳值,大于这个值,性能将不再提升,甚至会下降。 但是最佳值不是一个固定的值。它完全取决于硬件、文档的大小和复杂度、索引和搜索的负载的整体情况。

幸运的是,很容易找到这个 最佳点 :通过批量索引典型文档,并不断增加批量大小进行尝试。 当性能开始下降,那么你的批量大小就太大了。一个好的办法是开始时将 1,000 到 5,000 个文档作为一个批次, 如果你的文档非常大,那么就减少批量的文档个数。

密切关注你的批量请求的物理大小往往非常有用,一千个 1KB 的文档是完全不同于一千个 1MB 文档所占的物理大小。 一个好的批量大小在开始处理后所占用的物理大小约为 5-15 MB。

此种格式为何更具优势?

您可能会问自己, “为什么 bulk API 需要有换行符的有趣格式,而不是发送包装在 JSON 数组中的请求,例如 mget API?” 。

为了回答这一点,我们需要解释一点背景:在批量请求中引用的每个文档可能属于不同的主分片, 每个文档可能被分配给集群中的任何节点。这意味着批量请求 bulk 中的每个 操作 都需要被转发到正确节点上的正确分片。

如果单个请求被包装在 JSON 数组中,那就意味着我们需要执行以下操作:

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

这是可行的,但需要大量的 RAM 来存储原本相同的数据的副本,并将创建更多的数据结构,Java 虚拟机(JVM)将不得不花费时间进行垃圾回收。

相反,Elasticsearch 可以直接读取被网络缓冲区接收的原始数据。 它使用换行符字符来识别和解析小的 action/metadata 行来决定哪个分片应该处理每个请求。

这些原始请求会被直接转发到正确的分片。没有冗余的数据复制,没有浪费的数据结构。整个请求尽可能在最小的内存中处理。

参考原文地址 www.elastic.co