0%

分布式系统实例之 elasticsearch集群之概述

[TOC]

概述

基本概念

elasticsearch是什么

官网:Elasticsearch 是一个实时的分布式搜索分析引擎,它能让你以前所未有的速度和规模,去探索你的数据。 它被用作全文检索、结构化搜索、分析以及这三个功能的组合:

  • 使用 java 语言开发的一套开源的全文搜索引擎
  • 用于搜索、日志管理、安全分析、指标分析、业务分析、应用性能监控等多个领域
  • 底层基于 Lucene 开源库开发,提供 restAPI,可以被任何语言调用
  • 支持分布式部署,可水平扩展

集群(cluster)和节点(node)

集群(Cluster)

  • Elasticsearch 集群部署使其可以随时可用和并按需扩容,并保证数据的安全性
  • 通过启动参数 cluster.name 修改集群名称,默认名称为 elasticsearch

节点(Node)

  • 一个节点是一个 Java 进程实例,一台机器可以运行多个实例,一般情况下一台机器只允许一个节点
  • 一个集群有一个或者多个节点
  • 通过启动参数 node.name 定义节点名称
  • 每个节点都保存了集群的状态信息,只有 Master 节点可以修改集群的状态信息
  • 集群状态信息包括:所有节点信息、索引、Mapping、Settings、分片路由等信息

三种类型的节点

Master-eligible 节点:
1
2
3
- 每个节点启动,默认自己是一个 Master-eligible 节点
- 可以通过启动参数 node.master: false 禁止当前启动节点是 Master-eligible 节点
- 所有 Master-eligible 都可以参与选主流程,成为 Master 节点
Data 节点:
1
2
3
- 保存分片数据的节点
- 在数据扩展上起了很大的作用
- 通过启动参数 node.data 设置
Coordinating 节点
1
2
- 接收客户端请求,将请求分发到合适的节点,最终再对结果进行汇集
- 每个节点默认都是 Coordinating 节点

如何启动节点

1
2
3
bin/elasticsearch -E node.name=node1 -E cluster.name=myEs -d
bin/elasticsearch -E node.name=node2 -E cluster.name=myEs -d
bin/elasticsearch -E node.name=node3 -E cluster.name=myEs -d

索引(Index)-> table

  • 一个集群下面可以新建多个索引,索引体现了逻辑空间概念
  • 索引是一类相似文档的集合,是文档的容器,类比关系型数据库中的一张表的 Schema 的概念
  • 每个索引有自己的 Mapping 用于定义文档的字段名和字段类型
  • 每个索引有自己的 Settings 用于定义不同的数据分布,也就是索引使用分片的情况

类型(type)

Elasticsearch 7.x 版本已废弃 type 的概念,默认所有 index 只具备 _doc 一个类型,因此该概念可以不用深究。

为什么现在要移除type?

  • 在关系型数据库中table是独立的(独立存储),但es中同一个index中不同type是存储在同一个索引中的(lucene的索引文件),因此不同type中相同名字的字段的定义(mapping)必须一致。
  • 不同类型的“记录”存储在同一个index中,会影响lucene的压缩性能。

分片(Shard)

  • 分片是物理空间概念,索引中的数据都分布在分片上
  • 一个分片就是运行的一个 Lucene 的实例
  • 为了支持更大量的数据,索引一般会按某个维度分成多个部分,每个部分就是一个分片,分片被节点(Node)管理。一个节点(Node)一般会管理多个分片,这些分片可能是属于同一份索引

分片有两种,主分片和副本分片。

副本分片(分片副本)

副本(Replica):同一个分片(Shard)的备份数据,一个分片可能会有0个或多个副本,这些副本中的数据保证强一致或最终一致。

设置分片

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
curl -X DELETE 'localhost:9200/accounts'
# number_of_shards 分片数,不可修改, number_of_replicas 副本数,可以修改
curl -X PUT -H "Content-Type:application/json" 'localhost:9200/accounts' --data '{
"settings": {
"number_of_shards" : 2,
"number_of_replicas" : 0
}
}'

curl "localhost:9200/accounts?pretty"

# 修改副本数
curl -X PUT -H "Content-Type:application/json" 'localhost:9200/accounts/_settings' --data '{
"number_of_replicas" : 1
}'

# 再查一次可以看到有两个分片,一个副本
curl "localhost:9200/accounts?pretty"

curl "localhost:9200/accounts/_cat/shards?pretty"

# 现在的状况
# 拥有两个主分片,加上每个主分片的一个副本,总共给予我们四个分片


# 尝试修改分片,会失败
curl -X PUT -H "Content-Type:application/json" 'localhost:9200/accounts/_settings' --data '{
"number_of_shards" : 3
}'

文档(Document)

  • 文档是所有可搜索数据的最小单位,类似关系数据库中某张表中的一行记录
  • 文档会被序列化成 JSON 格式,JSON 对象由字段组成
  • 每个字段都有对应的字段类型,类型可以自己指定,也可以使用 ElasticSearch 自动推算
  • JSON 文档支持数组和嵌套
  • 每个文档都有一个唯一性 ID,可以自己指定,也可以系统自动生成

一个文档主要的元信息

1
2
3
4
5
6
1. _index: 文档所属的索引名
2. _type: 文档所属的类型名
3. _id: 文档的唯一ID
4. _source: 文档存储的 Json 数据
5. _version:文档的版本信息
6. _score: 相关性打分

Mapping (索引映射)

见 elasticesearch 索引mapping

Index Template (索引模板)

见 elasticesearch 索引模板

安装

windows

1
2
3
4
wget https://mirrors.huaweicloud.com/elasticsearch/7.7.0/elasticsearch-7.7.0-windows-x86_64.zip
unzip elasticsearch-5.5.1.zip
cd elasticsearch-5.5.1/
elasticsearch

基本操作

集群信息查询

1
curl -X GET 'http://localhost:9200/_cat/indices?v'

新建和删除 Index

1
2
3
4
# 新建
curl -X PUT 'localhost:9200/weather'
# 删除
curl -X DELETE 'localhost:9200/weather'

index 操作

按理说默认是 type, 但是这里使用 type = person

新增

向指定的 /Index/Type 发送 PUT 请求,就可以在 Index 里面新增一条记录。新增记录的时候,也可以不指定 Id,这时要改成 POST 请求。

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
curl -X PUT -H "Content-Type:application/json" 'localhost:9200/accounts/person/1' --data '{
"user": "zhangsan",
"title": "engineer",
"desc": "task DBA",
"num": 4
}'

# id 随机
curl -X POST -H "Content-Type:application/json" 'localhost:9200/accounts/person' --data '{
"user": "li 4",
"title": "engineer",
"desc": "task HK",
"num": 6
}'

curl -X POST -H "Content-Type:application/json" 'localhost:9200/articles/_doc' --data '{
"title": "how to make millions",
"tag": "starred",
"date": "2014-01-01"
}'

# 批量写数据
for i in `seq 0 10`; do \
curl -X POST -H "Content-Type:application/json" 'localhost:9200/accounts/person' --data '{
"user": "li 4",
"title": "engineer",
"desc": "task HK",
"num": 6
}'
done

返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index":"accounts",
"_type":"person",
"_id":"1", // 由于传入了id, 所以这里id按我们传入的给
"_version":1,
"result":"created",
"_shards":{
"total":2,
"successful":1,
"failed":0
},
"_seq_no":0,
"_primary_term":1
}

查询

1
2
3
4
5
# 查询, 向/Index/Type/Id发出 GET 请求,就可以查看这条记录。
curl 'localhost:9200/accounts/person/1'

# 可以通过自定义唯一id(dy_order_id)来查询
curl 'localhost:9200/accounts/person/dy_order_id'

返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index":"accounts",
"_type":"person",
"_id":"1",
"_version":1,
"_seq_no":0,
"_primary_term":1,
"found":true,
"_source":{
"user":"zhangsan",
"title":"engineer",
"desc":"task DBA"
}
}

删除记录

1
2
# 删除记录就是发出 DELETE 请求。
curl -X DELETE 'localhost:9200/accounts/person/1'

返回

1
{"_index":"accounts","_type":"person","_id":"1","_version":2,"result":"deleted","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":3,"_primary_term":1}

更新记录

1
2
3
4
5
6
7
curl -X PUT -H "Content-Type:application/json" 'localhost:9200/accounts/person/1' -d '
{
"user" : "zhang 3",
"title" : "engineer",
"desc" : "DBA",
"num": 4
}'

返回

1
{"_index":"accounts","_type":"person","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":4,"_primary_term":1}

基本高级查询

返回所有记录

1
2
3
curl 'localhost:9200/accounts/person/_search'

curl 'localhost:9200/share_2022_5_53/_doc/_search'

查询和过滤的区别

当使用于 过滤情况 时,查询被设置成一个“不评分”或者“过滤”查询。即,这个查询只是简单的问一个问题:“这篇文档是否匹配?”。回答也是非常的简单,yes 或者 no ,二者必居其一。当使用于 查询情况 时,查询就变成了一个“评分”的查询。和不评分的查询类似,也要去判断这个文档是否匹配,同时它还需要判断这个文档匹配的有 多好(匹配程度如何)。

过滤查询(Filtering queries)只是简单的检查包含或者排除,这就使得计算起来非常快。考虑到至少有一个过滤查询(filtering query)的结果是 “稀少的”(很少匹配的文档),并且经常使用不评分查询(non-scoring queries),结果会被缓存到内存中以便快速读取,所以有各种各样的手段来优化查询结果。

相反,评分查询(scoring queries)不仅仅要找出匹配的文档,还要计算每个匹配文档的相关性,计算相关性使得它们比不评分查询费力的多。同时,查询结果并不缓存。

多亏倒排索引(inverted index),一个简单的评分查询在匹配少量文档时可能与一个涵盖百万文档的filter表现的一样好,甚至会更好。但是在一般情况下,一个filter 会比一个评分的query性能更优异,并且每次都表现的很稳定。

如何选择查询与过滤

通常的规则是,使用查询(query)语句来进行 全文 搜索或者其它任何需要影响 相关性得分 的搜索。除此以外的情况都使用过滤(filters)。

bool 查询和过滤的区别

bool 过滤的子句使用 term, range 等过滤语法。 bool 查询使用 match, multi_match等子句

过滤

term 过滤

term主要用于精确匹配哪些值,比如数字,日期,布尔值或 not_analyzed 的字符串(未经切词的文本数据类型):

1
2
3
curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{"query": {"term": {"num": 4}}}'

curl -H "Content-Type:application/json" "http://10.23.111.70:9200/personal_orders/_doc/_search" --data '{"query": {"term": {"SellerID": 1901625824}}}'

terms 过滤

terms 跟 term 有点类似,但 terms 允许指定多个匹配条件。 如果某个字段指定了多个值,那么文档

1
curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{"query": {"terms": {"num": [3, 4]}}}'

range 过滤

1
curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{"query": {"range": {"num": {"gte": 4}}}}'

exists 和 missing 过滤

exists 和 missing 过滤可以用于查找文档中是否包含指定字段或没有某个字段,类似于SQL语句中的IS_NULL条件.

1
2
3
curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{"exists": {"field": "title"}}'

curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{"exists": {"field": "title1"}}'

bool 过滤(should, must, must_not)

bool 过滤器将多个小查询组合成一个大查询,查询语法有如下特点:

  1. 子查询可以任意顺序出现
  2. 可以嵌套多个查询,包括bool 查询也可以
  3. 如果bool查询中没有must条件,should中必须至少满足一条才会返回结果。

bool 过滤器包括四个操作符,mustmust_notshouldfilter,这四个都是数组,数组里面是对应的判断条件

  • must: 过滤子句,必须匹配。贡献算分,相当于 and。
  • must_not:过滤子句,必须不能匹配,但不贡献算分,相当于 not。
  • should: 选择性匹配,至少满足一条。贡献算分,相当于 or。
  • filter: 过滤子句,必须匹配,但不贡献算分。 相当于 and, 和 must 非常像,但是不计算得分

官方例子

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
POST _search
{
"query": {
"bool" : {
"must" : [
"term" : { "user" : "kimchy" }
],
"filter": [
"term" : { "tag" : "tech" }
],
"must_not" : [
"range" : {
"age" : { "gte" : 10, "lte" : 20 }
}
],
"should" : [
{ "term" : { "tag" : "wow" } },
{ "term" : { "tag" : "elasticsearch" } }
],
"minimum_should_match" : 1,
"boost" : 1.0
}
}
}

作者:学就完事了
链接:https://juejin.cn/post/6871109774566653965
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

查询

match 查询

match查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。

如果你使用 match 查询一个全文本字段,它会在真正查询之前用分析器先分析match一下查询字符:

如果用match下指定了一个确切值,在遇到数字,日期,布尔值或者not_analyzed 的字符串时,它将为你搜索你给定的值:

做精确匹配搜索时,你最好用过滤语句,因为过滤语句可以缓存数据。

1
2
3
4
curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_s
earch' --data '{"query": {"match": {"title": "engineer"}}}'

curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{"query": {"match": {"desc": "HK"}}}'

multi_match 查询

multi_match查询允许你做match查询的基础上同时搜索多个字段,在多个字段中同时查一个:

1
2
curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{
"query": {"multi_match": {"query": "engineer","fields": ["title", "desc"]}}}'

嵌套查询(内部对象和嵌套对象)

nested 查询

object新增(object 索引)
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
curl -X DELETE 'localhost:9200/my_index'
curl -H "Content-Type:application/json" "localhost:9200/my_index/blogpost/1" --data '{
"title": "Nest eggs",
"body": "Making your money work...",
"tags": [ "cash", "shares" ],
"comments": [
{
"name": "John Smith",
"comment": "Great article",
"age": 28,
"stars": 4,
"date": "2014-09-01"
},
{
"name": "Alice White",
"comment": "More like this please",
"age": 31,
"stars": 5,
"date": "2014-10-22"
}
]
}'

curl "localhost:9200/my_index/_mapping?pretty"
# 如果我们依赖字段自动映射,那么 comments 字段会自动映射为 object 类型。
object查询失败
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 没有查到,符合预期,需要使用 nested, 正如我们在 对象数组 中讨论的一样,
# 出现上面这种问题的原因是 JSON 格式的文档被处理成如下的扁平式键值对的结构。

# "comments.comment": [ article, great, like, more, please, this ],
# "comments.age": [ 28, 31 ],
# "comments.stars": [ 4, 5 ],
# "comments.date": [ 2014-09-01, 2014-10-22 ]

# 注意这里并不是说查到了一个name 为 alice 并且 age 为 28的 documnet,
# 而是commnets 中只要有任意两个 name 和 age 匹配就可以。
curl -H "Content-Type:application/json" "localhost:9200/my_index/_search?pretty" --data '{
"query": {
"bool": {
"must": [
{ "match": { "comments.name": "Alice" }},
{ "match": { "comments.age": 28 }}
]
}
}
}'

# 在内部对象下,尝试nested 查询, nested object under path [comments] is not of nested type, 不对哦
curl -H "Content-Type:application/json" "localhost:9200/my_index/_search?pretty" --data '{
"query": {
"bool": {
"must": [
{
"match": {
"title": "eggs"
}
},
{
"nested": {
"query": {
"bool": {
"must": [
{
"match": {
"comments.name": "john"
}
},
{
"match": {
"comments.age": 28
}
}
]
}
},
"path": "comments"
}
}
]
}
}
}'

虽然 object 类型 (参见 内部对象) 在存储 单一对象 时非常有用,但对于对象数组的搜索而言,毫无用处。**嵌套对象 ** 就是来解决这个问题的。将 comments 字段类型设置为 nested 而不是 object 后,每一个嵌套对象都会被索引为一个 隐藏的独立文档 ,举例如下:

nested 查询成功
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
curl -X DELETE 'localhost:9200/my_index'
curl -X PUT 'localhost:9200/my_index'
curl -XPUT -H "Content-Type:application/json" "localhost:9200/my_index/_mapping?pretty" --data '{"properties": {"comments": {"type": "nested", "properties": {"date": {"type": "date"}, "comment": {"type": "text"}, "stars": {"type": "short"}, "age": {"type": "short"}, "name": {"type": "text"}}}}}'


curl "localhost:9200/my_index/_mapping?pretty"
# "type" : "nested", 这样 tpye 为 nested

curl -H "Content-Type:application/json" "localhost:9200/my_index/_doc/1" --data '{
"title": "Nest eggs",
"body": "Making your money work...",
"tags": [ "cash", "shares" ],
"comments": [
{
"name": "John Smith",
"comment": "Great article",
"age": 28,
"stars": 4,
"date": "2014-09-01"
},
{
"name": "Alice White",
"comment": "More like this please",
"age": 31,
"stars": 5,
"date": "2014-10-22"
}
]
}'

# nested 查询
curl -H "Content-Type:application/json" "localhost:9200/my_index/_search?pretty" --data '{"query": {"bool": {"must": [{"match": {"title": "eggs"}}, {"nested": {"query": {"bool": {"must": [{"match": {"comments.name": "john"}}, {"match": {"comments.age": 28}}]}}, "path": "comments"}}]}}}'

参考

https://www.elastic.co/guide/en/elasticsearch/reference/7.9/nested.html

分页查询

在 Elasticsearch 中,搜索一般包括两个阶段,query 和 fetch 阶段,可以简单的理解,query 阶段确定要取哪些doc(doc_id),fetch 阶段取出具体的 doc。

具体查询见下方 kibana 查询。

Query 阶段

  1. Client 发送一次搜索请求,node1 接收到请求,然后,node1 创建一个大小为 from + size 的优先级队列用来存结果,我们管 node1 叫 coordinating node。
  2. coordinating node将请求广播到涉及到的 shards,每个 shard 在内部执行搜索请求,然后,将结果存到内部的大小同样为 from + size 的优先级队列里,可以把优先级队列理解为一个包含 top N 结果的列表。
  3. 每个 shard 把暂存在自身优先级队列里的数据返回给 coordinating node,coordinating node 拿到各个 shards 返回的结果后对结果进行一次合并,产生一个全局的优先级队列,存到自身的优先级队列里。

在上面的例子中,coordinating node 拿到 (from + size) * 6 条数据,然后合并并排序后选择前面的 from + size 条数据存到优先级队列,以便 fetch 阶段使用。另外,各个分片返回给 coordinating node 的数据用于选出前 from + size 条数据,所以,只需要返回唯一标记 doc 的 _id 以及用于排序的 _score 即可,这样也可以保证返回的数据量足够小。

coordinating node 计算好自己的优先级队列后,query 阶段结束,进入 fetch 阶段。

为什么是 from + size 大小的数据量,而不是单纯 size 大小的数据量,这是因为在一个分片里得分很低的文档,在和另一个分片中得分较高的文档比,甚至可能得分更高, 所以需要每个分片都将前 from + size 多的 doc_id 取出来。这实际上也就导致深翻页的问题。

Fetch 阶段

  1. coordinating node 发送 GET 请求到相关shards。
  2. shard 根据 doc 的 _id 取到数据详情,然后返回给 coordinating node。
  3. coordinating node 返回数据给 Client。

coordinating node 的优先级队列里有 from + size 个 _doc _id,但是,在 fetch 阶段,并不需要取回所有数据,在上面的例子中,前100条数据是不需要取的,只需要取优先级队列里的第101到110条数据即可

需要取的数据可能在不同分片,也可能在同一分片,coordinating node 使用 multi-get 来避免多次去同一分片取数据,从而提高性能。

深度分页的问题

Elasticsearch 的这种方式提供了分页的功能,同时,也有相应的限制。举个例子,一个索引,有10亿数据,分10个 shards,然后,一个搜索请求,from=1,000,000,size=100,这时候,会带来严重的性能问题:

CPU、内存和IO消耗容易理解,网络带宽问题稍难理解一点。在 query 阶段,每个shards需要返回 1,000,100 条数据给 coordinating node,而 coordinating node 需要接收 10 * 1,000,100 条数据,即使每条数据只有 _doc _id 和 _score,这数据量也很大了,而且,这才一个查询请求,那如果再乘以100呢?

在另一方面,我们意识到,这种深度分页的请求并不合理,因为我们是很少人为的看很后面的请求的,在很多的业务场景中,都直接限制分页,比如只能看前100页

这种深度分页确实存在,比如,业务上有遍历数据的需要,比如,有1千万粉丝的微信大V,要给所有粉丝群发消息,或者给某省粉丝群发,这时候就需要取得所有符合条件的粉丝,而最容易想到的就是利用 from + size 来实现,不过,这个是不现实的,这时,可以采用 Elasticsearch 提供的 scroll 方式来实现遍历。

深翻页的解决之道(游标查询scroll)

可以把 scroll 理解为关系型数据库里的 cursor,因此,scroll 并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发。

可以把 scroll 分为初始化和遍历两步,初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照,在遍历时,从这个快照里取数据,也就是说,在初始化后对索引插入、删除、更新数据都不能影响遍历结果

为了使用 scroll,初始搜索请求应该在查询中指定 scroll 参数,这可以告诉 Elasticsearch 需要保持搜索的上下文环境多久

初始化

1
2
# 保持游标查询窗口一分钟。
curl -X GET -H "Content-Type:application/json" "http://10.23.111.70:9200/trade_orders/_doc/_search?scroll=1m" --data '{"query": { "match_all": {}}}'

初始化时需要像普通 search 一样,指明 index 和 type (当然,search 是可以不指明 index 和 type 的),然后,加上参数 scroll,表示暂存搜索结果的时间,其它就像一个普通的search请求一样。

初始化返回一个 _scroll_id,_scroll_id 用来下次取数据用。

scroll=1m

启用游标查询可以通过在查询的时候设置参数 scroll 的值为我们期望的游标查询的过期时间。 游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。 这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。 设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。

遍历

注意遍历时的 url ,不需要 index 和 type 并且是 _search/scroll

1
curl -X GET -H "Content-Type:application/json" "http://10.23.111.70:9200/_search/scroll?scroll=1m" --data '{"scroll_id":"DnF1ZXJ5VGhlbkZldGNoAwAAAAAAADvxFnhpZlhVQllnVG42dklaUThoNWFXSmcAAAAAAANLwhZaUHo0aGJROVNscTJhc2JtdEo0dkpnAAAAAAADlx0WWEFrS3VVSFhTeG1mWEMtUVFfeENwUQ=="}'

Scroll-Scan

Elasticsearch 提供了 Scroll-Scan 方式进一步提高遍历性能。还是上面的例子,微信大V要给粉丝群发这种后台任务,是不需要关注顺序的,只要能遍历所有数据即可,这时候,就可以用Scroll-Scan。

Scroll-Scan 的遍历与普通 Scroll 一样,初始化存在一点差别。

search_type=scan已经被废弃了, 可以使用下面的方式

1
curl -X GET -H "Content-Type:application/json" "http://10.23.111.70:9200/trade_orders/_doc/_search?scroll=1m" --data '{"sort": ["_doc"]}'

Kibana 查询

每一个GET都可以直接查询

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
36
37
38
39
40
41
42
43
44
45
46
GET _search
{
"query": {
"match_all": {}
}
}

# from size 分页查询
GET trade_orders/_doc/_search
{
"query": { "match_all": {}},
"from": 11,
"size": 5
}

# 初始化 游标查询 scroll, 过期时间1分钟,关键字 _doc 是最有效的排序顺序。
# 尽管我们指定字段 size 的值为 2,我们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size
# 作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards 。
GET trade_orders/_doc/_search?scroll=1m
{
"query": { "match_all": {}},
"sort" : ["_doc"],
"size": 2
}

# personal_orders 下 personal_orders_jd_720328_xinbz891015 这个id 的查询
GET personal_orders/_doc/personal_orders_jd_720328_xinbz891015

# trade_orders 下 jd_120823795963 这个id 的查询
GET trade_orders/_doc/jd_120823795963

# es 的详情
GET _cat/indices?v

# index 的结构
GET trade_orders

# 通过 _search 查询
GET trade_orders/_search
{
"query": {
"match": {
"ItemIDs": "14084259599"
}
}
}

查询原理

分片及副本

Index 1:蓝色部分,有3个shard,分别是P1,P2,P3,位于3个不同的Node中,这里没有Replica。

Index 2:绿色部分,有2个shard,分别是P1,P2,位于2个不同的Node中。并且每个shard有一个replica,分别是R1和R2。基于系统可用性的考虑,同一个shard的primary和replica不能位于同一个Node中。这里Shard1的P1和R1分别位于Node3和Node2中,如果某一刻Node2发生宕机,服务基本不会受影响,因为还有一个P1和R2都还是可用的。因为是主备架构,当主分片发生故障时,需要切换,这时候需要选举一个副本作为新主,这里除了会耗费一点点时间外,也会有丢失数据的风险。

创建Index流程

建索引(Index, 就是创建数据表,es 中的 index 就是表)的时候,一个Doc先是经过路由规则定位到主Shard (主分片可能有多个),发送这个doc到主Shard上建索引,成功后再发送这个Doc到这个Shard的副本上建索引,等副本上建索引成功后才返回成功。

在这种架构中,索引数据全部位于Shard中,主Shard和副本Shard各存储一份。当某个副本Shard或者主Shard丢失(比如机器宕机,网络中断等)时,需要将丢失的Shard在其他Node中恢复回来,这时候就需要从其他副本(Replica)全量拷贝这个Shard的所有数据到新Node上构造新Shard。这个拷贝过程需要一段时间,这段时间内只能由剩余主副本来承载流量,在恢复完成之前,整个系统会处于一个比较危险的状态,直到failover结束。

这里就体现了副本(Replica)存在的一个理由,避免数据丢失,提高数据可靠性。副本(Replica)存在的另一个理由是读请求量很大的时候,一个Node无法承载所有流量,这个时候就需要一个副本来分流查询压力,目的就是扩展查询能力(支持读写分离)

部署层架构

Elasticsearch支持上述两种方式:可以参考之前的文章《分布式系统之数据分区》,请求路由的4中方式,这里实际上是其中两种。

混合部署(左图) Data Node和Transport Node 结合

不考虑MasterNode的情况下,还有两种Node,Data Node和Transport Node,这种部署模式下,这两种不同类型Node角色都位于同一个Node中,相当于一个Node具备两种功能:Data和Transport。

当有index或者query请求的时候,请求随机(自定义)发送给任何一个Node,这台Node中会持有一个全局的路由表,通过路由表选择合适的Node,将请求发送给这些Node,然后等所有请求都返回后,合并结果,然后返回给用户。一个Node分饰两种角色。

优势

好处就是使用极其简单,易上手,对推广系统有很大价值。最简单的场景下只需要启动一个Node,就能完成所有的功能。

劣势

缺点就是多种类型的请求会相互影响,在大集群如果某一个Data Node出现热点,那么就会影响途经这个Data Node的所有其他跨Node请求。如果发生故障,故障影响面会变大很多。

Elasticsearch中每个Node都需要和其余的每一个Node都保持连接。这种情况下,每个Node都需要和其他所有Node保持连接,而一个系统的连接数是有上限的,这样连接数就会限制集群规模。

分层部署(右图)

设置部分Node为Transport Node,专门用来做请求转发和结果合并。

其他Node可以设置为DataNode,专门用来处理数据。

优势

好处就是角色相互独立,不会相互影响,一般Transport Node的流量是平均分配的,很少出现单台机器的CPU或流量被打满的情况,而DataNode由于处理数据,很容易出现单机资源被占满,比如CPU,网络,磁盘等。独立开后,DataNode如果出了故障只是影响单节点的数据处理,不会影响其他节点的请求,影响限制在最小的范围内。

角色独立后,只需要Transport Node连接所有的DataNode,而DataNode则不需要和其他DataNode有连接。一个集群中DataNode的数量远大于Transport Node,这样集群的规模可以更大。另外,还可以通过分组,使Transport Node只连接固定分组的DataNode,这样Elasticsearch的连接数问题就彻底解决了。

可以支持热更新:先一台一台的升级DataNode,升级完成后再升级Transport Node,整个过程中,可以做到让用户无感知。

劣势

缺点是上手复杂,需要提前设置好Transport的数量,且数量和Data Node、流量等相关,否则要么资源闲置,要么机器被打爆。

数据层架构

数据存储

Elasticsearch的Index和meta,目前支持存储在本地文件系统中,同时支持niofs,mmap,simplefs,smb等不同加载方式,性能最好的是直接将索引LOCK进内存的MMap方式。默认,Elasticsearch会自动选择加载方式,另外可以自己在配置文件中配置。这里有几个细节,具体可以看官方文档。

索引和meta数据都存在本地,会带来一个问题:当某一台机器宕机或者磁盘损坏的时候,数据就丢失了。为了解决这个问题,可以使用Replica(副本)功能。

副本(Replica)

可以为每一个Index设置一个配置项:副本(Replicda)数,如果设置副本数为2,那么就会有3个Shard,其中一个是PrimaryShard,其余两个是ReplicaShard,这三个Shard会被Master尽量调度到不同机器,甚至机架上,这三个Shard中的数据一样,提供同样的服务能力。

副本(Replica)的目的有三个:

保证服务可用性:当设置了多个Replica的时候,如果某一个Replica不可用的时候,那么请求流量可以继续发往其他Replica,服务可以很快恢复开始服务。

保证数据可靠性:如果只有一个Primary,没有Replica,那么当Primary的机器磁盘损坏的时候,那么这个Node中所有Shard的数据会丢失,只能reindex了。

提供更大的查询能力:当Shard提供的查询能力无法满足业务需求的时候, 可以继续加N个Replica,这样查询能力就能提高N倍,轻松增加系统的并发度。

Replica 的问题

Replica带来成本浪费。为了保证数据可靠性,必须使用Replica,但是当一个Shard就能满足处理能力的时候,另一个Shard的计算能力就会浪费。

Replica带来写性能和吞吐的下降。每次Index或者update的时候,需要先更新Primary Shard,更新成功后再并行去更新Replica,再加上长尾,写入性能会有不少的下降。

分布式数据系统架构

基于本地文件系统的分布式系统

上图中是一个基于本地磁盘存储数据的分布式系统。Index一共有3个Shard,每个Shard除了Primary Shard外,还有一个Replica Shard。当Node 3机器宕机或磁盘损坏的时候,首先确认P3已经不可用,重新选举R3位Primary Shard,此Shard发生主备切换。然后重新找一台机器Node 7,在Node7 上重新启动P3的新Replica。由于数据都会存在本地磁盘,此时需要将Shard 3的数据从Node 6上拷贝到Node7上。如果有200G数据,千兆网络,拷贝完需要1600秒。如果没有replica,则这1600秒内这些Shard就不能服务。

为了保证可靠性,就需要冗余Shard,会导致更多的物理资源消耗。

这种思想的另外一种表现形式是使用双集群,集群级别做备份。

基于分布式文件系统的分布式系统(共享存储)-存储和计算分离

针对第一种架构中的问题,另一种思路是:存储和计算分离。

第一种思路的问题根源是数据量大,拷贝数据耗时多,那么有没有办法可以不拷贝数据?为了实现这个目的,一种思路是底层存储层使用共享存储,每个Shard只需要连接到一个分布式文件系统中的一个目录/文件即可,Shard中不含有数据,只含有计算部分。相当于每个Node中只负责计算部分,存储部分放在底层的另一个分布式文件系统中,比如HDFS。
上图中,Node 1 连接到第一个文件;Node 2连接到第二个文件;Node3连接到第三个文件。当Node 3机器宕机后,只需要在Node 4机器上新建一个空的Shard,然后构造一个新连接,连接到底层分布式文件系统的第三个文件即可,创建连接的速度是很快的,总耗时会非常短。

优势

在这种架构下,资源可以更加弹性,当存储不够的时候只需要扩容存储系统的容量;当计算不够的时候,只需要扩容计算部分容量。

存储和计算是独立管理的,资源管理粒度更小,管理更加精细化,浪费更少,结果就是总体成本可以更低。

负载更加突出,抗热点能力更强。一般热点问题基本都出现在计算部分,对于存储和计算分离系统,计算部分由于没有绑定数据,可以实时的扩容、缩容和迁移,当出现热点的时候,可以第一时间将计算调度到新节点上。

劣势

访问分布式文件系统的性能可能不及访问本地文件系统。在上一代分布式文件系统中,这是一个比较明显的问题,但是目前使用了各种用户态协议栈后,这个差距已经越来越小了。

HBase使用的就是这种架构方式。

参考

es 官网

es中文社区

eslaticsearch 基础概念

阮一峰的es文章翻译

ElasticSearch常用的基本查询语句详解

从Elasticsearch来看分布式系统架构设计

es官网游标查询

关于副本分片