介绍
Elasticsearch 是一个开源的搜索分析引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库--无论是开源还是私有。
主要特性
一个分布式的实时文档存储,每个字段可以被索引与搜索;
一个分布式实时分析搜索引擎;
能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据。
使用场景
全文检索、结构化搜索、分析以及这三个功能的组合。
术语
集群
一个或多个拥有相同cluster.name配置的节点组成。当有节点加入或移除时,集群会重新分布所有数据。集群状态:status 字段指示着当前集群在总体上是否工作正常。它的三种颜色含义如下:green所有的主分片和副本分片都正常运行。yellow所有的主分片都正常运行,但不是所有的副本分片都正常运行。red有主分片没能正常运行。
下图是集群、节点、索引、分片的关系:
节点
一个运行中的 Elasticsearch 实例。当一个节点被选举成为 主 节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
分片
一个 Lucene 的实例,它本身就是一个完整的搜索引擎。一个分片是一个底层的 工作单元 ,它仅保存了全部数据中的一部分。文档被存储和索引到分片内,应用程序是直接与索引而不是与分片进行交互。一个分片可以是 主分片或者副本分片。
主分片:
索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。索引建立的时候就已经确定了主分片数,不可修改。
副本分片:
一个副本分片只是一个主分片的拷贝。 副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。副本分片数可以随时修改。
分段
Lucene Segments,片段本身是完全功能的倒排索引。片段是不可变的,这允许Lucene增量地向索引添加新文档,而无需从头开始重建索引。对于每个搜索请求,搜所有段都会被搜素,每个段消耗CPU周期,文件句柄和内存。这意味着分段数越多,搜索性能就越低。
文档
通常可以理解为对象,指最顶层或根对象,这个根对象被序列化成json并存储到elasticsearch中,指定了唯一ID;含三个必须的元数据:_index文档放在哪,_type文档对象的类型,_id文档唯一标志;
索引 (_index)
保存数据的地方(对应关系数据库的database),指向一个或者多个物理分片的逻辑命名空间 。索引名必须小写,不能以下划线开头,不能包含逗号。
类型(_type)
在索引中对数据进行逻辑分区(对应关系数据库的table),长度限制为256个字符。
id(_id)
文档Document的唯一标志(对应关系数据库的row),组合_index和_type唯一标志一个文档。可以自定义,es也可以自动生成,自动生成的id是URL-safe、 基于 Base64 编码且长度为20个字符的 GUID 字符串。
下图是index、type、document的关系:
版本号(_version)
每个文档都有一个版本号,对文档进行修改(含删除),_verison会递增;
倒排索引
功能是将文档的所有文本根据分词器拆分成单独的词(我们称为词条或tokens),创建一个包含所有不重复词条的排序列表,列出每个词条出现过文档。示例如下:
倒排索引由单词词典(Term Dictionary)和倒排列表(Posting List)组成。直接通过内存查找Term,不读磁盘,但是如果Term太多,Term dictionary也会很大,放内存不现实,于是有了Term Index,就像字典里的索引页一样,A开头的有哪些Term,分别在哪页,可以理解Term index是一颗树,
这棵树不会包含所有的Term,它包含的是Term的一些前缀。通过Term index可以快速地定位到Term dictionary的某个offset,然后从这个位置再往后顺序查找。
所以Term index不需要存下所有的Term,而仅仅是他们的一些前缀与Term Dictionary的block之间的映射关系,再结合FST(Finite State Transducers)的压缩技术,可以使Term index缓存到内存中。从Term index查到对应的Term dictionary的block位置之后,再去磁盘上找Term,大大减少了磁盘随机读的次数。
数据输入及输出
Elasticsearch存储和检索复杂的数据结构,序列化成为JSON文档,以实时的方式查询。
- 提供了一套完整的基于RESTful web接口(PUT、POST、GET、DELETE),实现数据的新增、查询、更新、删除等功能;
- 通过elasticsearch-jdbc插件从数据库导入数据;
- 通过logstash等工具从kafka、数据库、redis、文件等输入或输出;
工作原理及流程
节点选举
- 对所有可以成为master的节点(node.master: true)根据nodeid排序,每次选举每个节点都把自己知道的节点排序,选第一个,暂定为master;
- 如果对某个节点的投票数达到一定值(可以成为master的节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master;
- 对于brain split问题,需要把候选master节点最小值设置为可以成为master节点数n/2+1(quorum)
索引过程
- 当用户向一个节点提交了一个索引新文档的请求,节点会计算新文档应该加入到哪个分片(shard)中;
- 每个节点都存储有每个分片存储在哪个节点的信息,因此协调节点会将请求发送给对应的节点;
- 注意这个请求会发送给主分片,等主分片完成索引,会并行将请求发送到其所有副本分片,保证每个分片都持有最新数据;
- 每次写入新文档时,都会先写入内存中,并将这一操作写入一个translog文件(transaction log)中,此时如果执行搜索操作,这个新文档还不能被索引到;
- ES会每隔1秒时间(这个时间可以修改)进行一次刷新操作(refresh),此时在这1秒时间内写入内存的新文档都会被写入一个文件系统缓存(filesystem cache)中,并构成一个分段(segment)。此时这个segment里的文档可以被搜索到,但是尚未写入硬盘,即如果此时发生断电,则这些文档可能会丢失;
- 不断有新的文档写入,则这一过程将不断重复执行。每隔一秒将生成一个新的segment,而translog文件将越来越大;
- 每隔30分钟或者translog文件变得很大,则执行一次fsync操作。此时所有在文件系统缓存中的segment将被写入磁盘,而translog将被删除(此后会生成新的translog)。
搜索过程
- 当一个节点接收到一个搜索请求,则这个节点就变成了协调节点;
- 查询会广播到索引中每一个分片的拷贝,每个分片在本地执行搜索并构建一个匹配文档大小from+size的优先队列(查询Filesystem cache和Memory buffer,所以是近实时的),每个分片返回自己优先队列中所有文档给协调节点;
- 协调节点合并这些文档到自己的优先队列中生成全局排序的结果列表;
更新/删除过程
- ES的索引是不能修改的,因此更新和删除操作并不是直接在原索引上直接执行;
- 每一个磁盘上的segment都会维护一个del文件,用来记录被删除的文件。每当用户提出一个删除请求,文档并没有被真正删除,索引也没有发生改变,而是在del文件中标记该文档已被删除。因此,被删除的文档依然可以被检索到,只是在返回检索结果时被过滤掉了。每次在启动segment合并工作时,那些被标记为删除的文档才会被真正删除。
- 更新文档会首先查找原文档,得到该文档的版本号。然后将修改后的文档写入内存,此过程与写入一个新文档相同。同时,旧版本文档被标记为删除,同理,该文档可以被搜索到,只是最终被过滤掉。
水平扩容
- 分片自动迁移到新节点;
- 主副分片自动负载均衡;
- 扩容后每个节点的分片减少,每个分片的cpu、内存、IO资源会增加,性能提升;
- 扩容的极限是每个节点一个分片,超出极限的话,可以新增副本分片数
扩容示例如下:
故障过程
master主节点宕机es处理流程
- 选择master节点;
- 新的master会把挂掉的主分片对应的某个副本分片提升为主分片,集群状态变为yellow,因为副本分片少了;
- 故障机重启,新master会把副本分片复制到该节点,集群状态变为green。
集群故障前:
集群node1主节点挂掉后:
注:node1节点挂掉后,node2节点的R2,node3节点的R1提升为主分片。
优化策略
索引优化
- 基于日期模板动态创建索引;通过索引模板自动创建索引;
- 通过rollover API滚动创建索引;通过crontab任务定时检查max_docs或 max_age这个配置是否满足滚动条件,满足则新建索引并把别名指向新的索引;
- 通过别名管理索引;别名可以关联索引或聚合多个索引;过滤器也可以设置别名,给不同的人群返回不同的数据;
- 使用es自动生成id;
储存优化
- 冷热数据分离;设置机器节点的属性(elasticsearh.yml->node.attr.box_type: hot)区分冷热节点, 为活跃索引创建模板,由此模板创建的索引将分配给带有box_type:hot标记的节点。理想情况下,活跃索引(接收所有写操作的索引)应该在每个热节点上都有一个分片,以便将索引负载分解成尽可能多的机器上。
- 凌晨定时对索引force merge;根据策略合并段,减少分片中段的个数,提高性能(检索时会轮训每个段)。该操作很消耗系统资源;
索引生命周期管理
使用curator工具管理索引的生命周期;支持创建索引、关闭索引、删除索引、更改分片路由、生成索引快照等
配置优化
- 合理设置分词器;分词器的作用是按一定的规则切分文本中的词,分词器选择不合理直接影响搜索结果和准确性;
- 禁用wildcard防止误操作,删除或更新多个索引;
写入优化
bulk批量写入;
硬件扩展
新增机器、机器升级;
API常见操作
集群健康
GET /_cluster/health
索引分片设置
PUT /blogs
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
副本分片数目调整
PUT /blogs/_settings
{
"number_of_replicas" : 2
}
创建文档 - 指定id
PUT /website/blog/123
{
"title": "My first blog entry",
"text": "Just trying this out...",
"date": "2014/01/01"
}
创建文档 - 不指定id
POST /website/blog/
{
"title": "My second blog entry",
"text": "Still trying this out...",
"date": "2014/01/01"
}
创建文档
PUT /website/blog/123?op_type=create
{
"title": "My third blog entry ",
"text": "Test op_type",
"date": "2019/01/02"
}
注:如果没有该文档(id为123),则创建(根据op_type=create值)成功(status=201),已存在则报错(status=409)。
删除文档
DELETE /website/blog/123
注:删除_version值也会增加。
更新文档
PUT /website/blog/123
{
"title": "My first blog entry",
"text": "I am starting to get the hang of this...",
"date": "2014/01/02"
}
注:已将旧文档标记为已删除,并增加一个全新的文档。后续elasticsearch会清理掉。
更新文档按条件
PUT /website/blog/1?version=1
{
"title": "My first blog entry",
"text": "Starting to get the hang of this..."
}
注:当文档version=1时才修改成功
部分更新文档
POST /website/blog/1/_update
{
"doc" : {
"tags" : [ "testing" ],
"views": 0
}
}
注:没有该字段则新增,有则更新该字段内容。
查询文档
GET /website/blog/123?pretty
查询部分文档
GET /website/blog/123?_source=title,text
查询文档不显示元数据
GET /website/blog/123/_source
查询多个文档
GET /_mget
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : 2
},
{
"_index" : "website",
"_type" : "pageviews",
"_id" : 1,
"_source": "views"
}
]
}
分页查询
GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10
轻量搜索
GET /_all/blog/_search?q=title:first
注:查询在 blog 类型中 tile 字段包含 first 单词的所有文档。
mapping查询
GET /websit/_mapping/blog
查询指定分片
GET /_search?routing=user_1,user2
注:routing 值来限定只搜索几个相关的分片
游标查询
GET /website/_search?scroll=1m
{
"query": { "match_all": {}},
"sort" : ["_doc"],
"size": 1000
}
注:scroll=1m 游标查询窗口保持1分钟,_doc最有效的排序顺序
游标查询返回下一批结果
GET /_search/scroll
{
"scroll": "1m",
"scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAANZgFlNqYjVPbW9wUktlV0FzNzRYTWNyX3cAAAAAAADWYhZTamI1T21vcFJLZVdBczc0WE1jcl93AAAAAAAA1mEWU2piNU9tb3BSS2VXQXM3NFhNY3JfdwAAAAAAANZjFlNqYjVPbW9wUktlV0FzNzRYTWNyX3cAAAAAAADWZBZTamI1T21vcFJLZVdBczc0WE1jcl93"
}
注:scroll_id为上一次查询返回的
删除索引
DELETE /my_index
删除多个索引
DELETE /index_one,index_two
DELETE /index_*
删除全部索引
DELETE /_all
DELETE /*
索引模板
PUT _template/template_2
{
"index_patterns": "temp*",
"order": 0,
"settings": {
"number_of_shards": 5
},
"aliases": {
"temp-query": {}
},
"mappings": {
"doc": {
"properties": {
"person_name": {
"type": "keyword"
},
"gender_id": {
"type": "long"
},
"bureau_id": {
"type": "long"
}
}
}
}
}
注:先执行上面创建模板,然后执行下面这条后会自动创建temp11索引并保存数据
PUT /temp11/doc/1
{
"person_name": "张三",
"gender_id": 1,
"bureau_id": 2
}
创建索引别名
POST /_aliases
{
"actions": [
{"add": {"index": "delay_blog", "alias": "dblog"}}
]
}
注:创建delay_blog索引的别名为dblog
通过别名查询
GET /dblog/test/2
其他
- 每个查询在每个分片的单个线程中执行,可以并行处理多个分片(多核处理器堆分片性能会有巨大提升);
- 文档不能修改,只能被替换;
- 文档中每个字段的所有数据都是默认被索引的;即每个字段都有为了快速检索设置的专用倒排序索引;
- 每个分片的开销取决于分段的数量和大小,因此可以通过force merge操作按照策略强制合并分段,较少开销提升查询性能;
- 主分片个数必须在创建索引时确认,不能修改。
建议
- 分片大小控制在30G-50G比较通用;
- elasticsearch推荐最大JVM堆空间是30-32G;
- 每节点每索引一个分片,每个分片都是Luncene的一个index,而Luncene的一个index可以存21亿个文档;
- 增加副本分片数可以提交查询性能,但可能会降低写入性能。
部署环境介绍
Elasticsearch6.3.1: https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.1.tar.gz
Kibana6.3.1: https://artifacts.elastic.co/downloads/kibana/kibana-6.3.1-darwin-x86_64.tar.gz
Chrome Elasticsearch Head插件:https://github.com/mobz/elasticsearch-head
注:以上来自网络,个人整理分析归档。