[TOC]
概述
基本概念
elasticsearch是什么
官网:Elasticsearch 是一个实时的分布式搜索分析引擎,它能让你以前所未有的速度和规模,去探索你的数据。 它被用作全文检索、结构化搜索、分析以及这三个功能的组合:
- 使用 java 语言开发的一套开源的全文搜索引擎
- 用于搜索、日志管理、安全分析、指标分析、业务分析、应用性能监控等多个领域
- 底层基于 Lucene 开源库开发,提供 restAPI,可以被任何语言调用
- 支持分布式部署,可水平扩展
集群(cluster)和节点(node)
集群(Cluster)
- Elasticsearch 集群部署使其可以随时可用和并按需扩容,并保证数据的安全性
- 通过启动参数 cluster.name 修改集群名称,默认名称为 elasticsearch
节点(Node)
- 一个节点是一个 Java 进程实例,一台机器可以运行多个实例,一般情况下一台机器只允许一个节点
- 一个集群有一个或者多个节点
- 通过启动参数 node.name 定义节点名称
- 每个节点都保存了集群的状态信息,只有 Master 节点可以修改集群的状态信息
- 集群状态信息包括:所有节点信息、索引、Mapping、Settings、分片路由等信息
三种类型的节点
Master-eligible 节点:
1 | - 每个节点启动,默认自己是一个 Master-eligible 节点 |
Data 节点:
1 | - 保存分片数据的节点 |
Coordinating 节点
1 | - 接收客户端请求,将请求分发到合适的节点,最终再对结果进行汇集 |
如何启动节点
1 | bin/elasticsearch -E node.name=node1 -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 | curl -X DELETE 'localhost:9200/accounts' |
文档(Document)
- 文档是所有可搜索数据的最小单位,类似关系数据库中某张表中的一行记录
- 文档会被序列化成 JSON 格式,JSON 对象由字段组成
- 每个字段都有对应的字段类型,类型可以自己指定,也可以使用 ElasticSearch 自动推算
- JSON 文档支持数组和嵌套
- 每个文档都有一个唯一性 ID,可以自己指定,也可以系统自动生成
一个文档主要的元信息
1 | 1. _index: 文档所属的索引名 |
Mapping (索引映射)
见 elasticesearch 索引mapping
Index Template (索引模板)
见 elasticesearch 索引模板
安装
windows
1 | wget https://mirrors.huaweicloud.com/elasticsearch/7.7.0/elasticsearch-7.7.0-windows-x86_64.zip |
基本操作
集群信息查询
1 | curl -X GET 'http://localhost:9200/_cat/indices?v' |
新建和删除 Index
1 | 新建 |
index 操作
按理说默认是 type, 但是这里使用 type = person
新增
向指定的 /Index/Type 发送 PUT 请求,就可以在 Index 里面新增一条记录。新增记录的时候,也可以不指定 Id,这时要改成 POST 请求。
1 | curl -X PUT -H "Content-Type:application/json" 'localhost:9200/accounts/person/1' --data '{ |
返回
1 | { |
查询
1 | 查询, 向/Index/Type/Id发出 GET 请求,就可以查看这条记录。 |
返回
1 | { |
删除记录
1 | 删除记录就是发出 DELETE 请求。 |
返回
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 | curl -X PUT -H "Content-Type:application/json" 'localhost:9200/accounts/person/1' -d ' |
返回
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 | curl 'localhost:9200/accounts/person/_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 | curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{"query": {"term": {"num": 4}}}' |
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 | curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{"exists": {"field": "title"}}' |
bool 过滤(should, must, must_not)
bool 过滤器将多个小查询组合成一个大查询,查询语法有如下特点:
- 子查询可以任意顺序出现
- 可以嵌套多个查询,包括bool 查询也可以
- 如果bool查询中没有must条件,should中必须至少满足一条才会返回结果。
bool 过滤器包括四个操作符,must
、must_not
、should
和filter
,这四个都是数组,数组里面是对应的判断条件
- must: 过滤子句,必须匹配。贡献算分,相当于 and。
- must_not:过滤子句,必须不能匹配,但不贡献算分,相当于 not。
- should: 选择性匹配,至少满足一条。贡献算分,相当于 or。
- filter: 过滤子句,必须匹配,但不贡献算分。 相当于 and, 和 must 非常像,但是不计算得分
官方例子
1 | POST _search |
查询
match 查询
match查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。
如果你使用 match 查询一个全文本字段,它会在真正查询之前用分析器先分析match一下查询字符:
如果用match下指定了一个确切值,在遇到数字,日期,布尔值或者not_analyzed 的字符串时,它将为你搜索你给定的值:
做精确匹配搜索时,你最好用过滤语句,因为过滤语句可以缓存数据。
1 | curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_s |
multi_match 查询
multi_match查询允许你做match查询的基础上同时搜索多个字段,在多个字段中同时查一个:
1 | curl -H "Content-Type:application/json" 'localhost:9200/accounts/person/_search' --data '{ |
嵌套查询(内部对象和嵌套对象)
nested 查询
object新增(object 索引)
1 | curl -X DELETE 'localhost:9200/my_index' |
object查询失败
1 | 没有查到,符合预期,需要使用 nested, 正如我们在 对象数组 中讨论的一样, |
虽然 object 类型 (参见 内部对象) 在存储 单一对象 时非常有用,但对于对象数组的搜索而言,毫无用处。**嵌套对象 ** 就是来解决这个问题的。将 comments 字段类型设置为 nested 而不是 object 后,每一个嵌套对象都会被索引为一个 隐藏的独立文档 ,举例如下:
nested 查询成功
1 | curl -X DELETE 'localhost:9200/my_index' |
参考
https://www.elastic.co/guide/en/elasticsearch/reference/7.9/nested.html
分页查询
在 Elasticsearch 中,搜索一般包括两个阶段,query 和 fetch 阶段,可以简单的理解,query 阶段确定要取哪些doc(doc_id),fetch 阶段取出具体的 doc。
具体查询见下方 kibana 查询。
Query 阶段
- Client 发送一次搜索请求,node1 接收到请求,然后,node1 创建一个大小为 from + size 的优先级队列用来存结果,我们管 node1 叫 coordinating node。
- coordinating node将请求广播到涉及到的 shards,每个 shard 在内部执行搜索请求,然后,将结果存到内部的大小同样为 from + size 的优先级队列里,可以把优先级队列理解为一个包含 top N 结果的列表。
- 每个 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 阶段
- coordinating node 发送 GET 请求到相关shards。
- shard 根据 doc 的 _id 取到数据详情,然后返回给 coordinating node。
- 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 | 保持游标查询窗口一分钟。 |
初始化时需要像普通 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 | GET _search |
查询原理
分片及副本
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使用的就是这种架构方式。