前言
随着大数据和人工智能行业的发展与成熟,各个行业各种业务场景下OLAP(联机分析处理)的需求越来越强;人工智能中的NLP(自然语言识别)的发展为文本分析以及全文检索带上了一个新的台阶,在这种背景下,作为上述两种需求的集大成者的elasticsearch的应用越来越广泛,elasticsearch中存储的数据也越来越多,在elasticsearch给我们带来很多便利的同时也带来了很多问题:
- 查询数据越来越慢,聚合的速度慢的离谱,聚合的数据量大一些的话,可能出现超时失败,甚至OOM
- 磁盘和内存资源以肉眼可见的速度快速消耗,甚至出现满载的情况
- JVM频繁GC,fullGC的频率逐渐变高,甚至由于GC卡顿导致系统不可用的情况发生
上述的种种情况可能让你怀疑人生,难道是elasticsearch不行了?impossible!作为经过无数版本迭代,无数场景验证的成熟NOSQL数据库,这些问题根本不够看,早就提供了优化以及解决方案,现在让我们开启一波优化操作吧,超神还是超鬼就在这一波了
正文
去翻翻官网的优化方案,一顿操作,各种调整参数,优化JVM,优化存储结构等等,效果确实不错,解决了不少问题,甚为欣喜,愉快的回去继续处理业务需求。
2 months later,之前问题再次出现了,而且能优化的参数都优化了,怎么破?赶紧去网上取经,查看别人的成功经验,站在巨人的肩膀上,于是一个成熟的方案就出现在眼前:
-
将索引按照某些条件由一个拆分成多个,配合模板和别名机制按条件生成索引(如如果是时序数据,可以按照时间如天级别进行索引的创建与维护),在这种操作下,按照时间段的search就会被限制在一个或者几个index中,减少了数据扫描范围,查询以及聚合的效率都会提升,消耗的资源也变少了
-
根据数据的特点,将数据分为冷热温数据,进行分级管理:热数据配备较好的硬件资源,如较大的内存和性能更好的CPU,SSD硬盘等;温数据和冷数据使用相对较差的硬件,这样在整个硬件消耗可控的情况下,尽量充分利用硬件资源
-
对部分温冷数据执行shrink以及forcemerge操作,暂时不用的数据可以close甚至delete掉(delete的时候尽量使用delete index,不用使用delete_by_query,delete_by_query的效率以及资源占用实在太感人了)
-
对数据定期做snapshot,保证数据的安全性
这一顿操作和重构后,突然发现整个世界突然美好了,一切都变得协调了,把这些操作写成脚本定时执行后,终于可以和其他小伙伴们快乐的玩耍了,再也不用对着日志苦逼的排查问题加上优化了。
随着业务需求的推进以及集群数据的演进,index越来越多,模板越来越多,各种elasticsearch的需求也越来越多,随之而来的就是维护的脚本越来越多,鸭梨山大,继续学习后 ,终于来到了旧版本(elasticsearch 6.6版本之前)的终极解决方案:
curator+rollover
在这套方案中,rollover负责索引的迭代管理,别名的管理;curator负责在上层对rollover command以及其他支持的command进行管理:
rollover
上面的按时间创建索引的方法虽然能够通过索引名称判断数据的新旧,能通过索引名称来过滤search数据的索引范围,提高search效率。但是如果数据不是按照时间均匀分布,出现数据波动的话,数据过大则可能导致索引数据量超过设计标准,导致性能受到影响;数据过小则会造成资源浪费。为了弥补和解决这个问题,引入了rollover api。
Rollover 的原理是使用一个别名指向真正的索引(要求索引的后缀必须是可以递增的数值类型,默认是000001这6位数字),当指向的索引满足一定条件(文档数或时间或索引大小),会新创建一个前缀相同,后缀递增的新索引,并将别名更新到实际指向的索引。
下面举例说明(所有的命令在kibana中执行):
首先创建一个模板方便后续管理 :
PUT /_template/taochy_template
{
"index_patterns": [
"taochy-*"
],
"settings": {
"refresh_interval": "30s",
"number_of_shards": "1",
"number_of_replicas": "0"
},
"mappings": {
"properties": {
"name": {
"type": "keyword"
}
}
}
}
创建索引:
PUT /taochy-000001
{
"aliases": {
"taochy_write_alias": {
"is_write_index": true
}
}
}
设置允许写入写入"is_write_index":true ,否则无法写入数据,集群只能规定一个写入的索引
插入三条数据:
POST /taochy_write_alias/_bulk?refresh=true
{"name":"aa"}
{"name":"bb"}
{"name":"cc"}
命令返回结果:
{
"took" : 37,
"errors" : false,
"items" : [
{
"create" : {
"_index" : "taochy-000001",
"_type" : "_doc",
"_id" : "wVvFttIBUTVfQxRWwXyM",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1,
"status" : 201
}
},
{
"create" : {
"_index" : "taochy-000001",
"_type" : "_doc",
"_id" : "wlvFttIBUTVfQxRWwXyM",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1,
"status" : 201
}
},
{
"create" : {
"_index" : "taochy-000001",
"_type" : "_doc",
"_id" : "w1vFttIBUTVfQxRWwXyM",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1,
"status" : 201
}
}
]
}
现在所有的数据都在taochy-000001上,现在执行rollover:
POST /taochy_write_alias/_rollover
{
"conditions": {
"max_age": "1d",
"max_docs": 3,
"max_size": "20gb"
}
}
上述rollover命令指定了三个规则,即时间最长1天 ,数据最多3条,容量最大20GB,这三个条件是or的关系,有一条满足即触发rollover操作
查看执行结果:
{
"acknowledged" : true,
"shards_acknowledged" : true,
"old_index" : "taochy-000001",
"new_index" : "taochy-000002",
"rolled_over" : true,
"dry_run" : false,
"conditions" : {
"[max_docs: 3]" : true,
"[max_size: 20gb]" : false,
"[max_age: 1d]" : false
}
}
从上面结果看出来,已经创建了新的索引taochy-000002,原因是最大数据条数已经达到3条了("[max_docs: 3]" : true),这里还有另外一个操作,就是rollover将别名指向了新生成的taochy-000002了,后续数据会写入taochy-000002中。
再写入一条记录:
POST /taochy_write_alias/_doc
{"name":"dd"}
查看执行结果:
{
"_index" : "taochy-000002",
"_type" : "_doc",
"_id" : "BdbMttIBgpLCCHbxhihi",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
结果符合预期,数据存储在taochy-000002中。
curator
curator允许操作index和snapshot,支持下列command的执行和管理:
- 通过别名添加或删除索引
- 修改shard以及routing相关的属性
- 修改index shard的replica个数
- index的create、delete、close、forcemerge以及reindex操作
- index的rollover操作
- snapshot的create、delete、restore操作
curator通过配置文件的编排 ,将需要执行的所有command按照既定的规则顺序执行,然后通过crontab定时调用执行,如果有需求的话,可以将curator命令进行可视化开发,这样后续的命令编排以及管理可以在界面上进行操作,降低使用门槛和犯错几率,完美 !问题解决。
ILM
简介
上面的curator+rollover方案在功能上基本上能满足大部分场景的要求了,但是使用起来还是有些复杂,且不够自动化。所以在elasticsearch6.6版本开始,elasticsearch把这两个组件进行了融合,搞出了个ILM(Index Lifecycle Management ,简称 ILM),所谓 Lifecycle(生命周期)是把索引定义了四个阶段:
- Hot:索引可写入,也可查询,也就是我们通常说的热数据,为保证性能数据通常都是在内存中或者SSD上;
- Warm:索引不可写入,但可查询,介于热和冷之间,数据来源于hot层,可以进行数据周期设置,shrink,forcemerge,reduce replica等操作,数据可以根据需求存在内存以及硬盘上;如果土豪玩家或者有特殊需求的场景,数据也可以按照需求放在内存以及SSD上;
- Cold:索引不可写入,但很少被查询,数据来源于warm层,可以进行数据周期设置,freeze等操作,数据查询的慢点也可接受,基本不再使用的数据,数据通常存储在大容量的磁盘上;
- Delete:索引按照设置的数据时间规则可被安全的删除。
这 4 个阶段是 ES 定义的一个索引从生到死的过程, Hot -> Warm -> Cold -> Delete 4 个阶段只有 Hot 阶段是必须的,其他 3 个阶段根据业务的需求可选。
使用
下面以纯hot阶段以及delete节点来进行使用说明,先创建ilm的policy,kibana上有图形界面可以操作,也可以通过脚本或代码进行预制,kibana操作界面如下:
上面规约了Hot阶段重新生成索引的规则,这部分功能其实就是对于rollover api的封装,使用方法参照上面的rollover章节的介绍,下面看看这个规则生成后是个什么样子:
执行命令:
GET _ilm/policy
查找已经创建的policy,结果如下:
"policy-taochy" : {
"version" : 4,
"modified_date" : "2021-03-03T09:30:37.470Z",
"policy" : {
"phases" : {
"hot" : {
"min_age" : "0ms",
"actions" : {
"rollover" : {
"max_size" : "10kb",
"max_age" : "1d",
"max_docs" : 2
},
"set_priority" : {
"priority" : 75
}
}
},
"delete" : {
"min_age" : "3d",
"actions" : {
"delete" : {
"delete_searchable_snapshot" : true
}
}
}
}
}
}
建立索引模板:
PUT /_template/taochy_template
{
"index_patterns": [
"taochy-*"
],
"aliases": {
"taochy_read_alias": {}
},
"settings": {
"index": {
"lifecycle": {
"name": "policy-taochy",
"rollover_alias": "taochy_write_alias"
},
"refresh_interval": "30s",
"number_of_shards": "1",
"number_of_replicas": "0"
}
},
"mappings": {
"properties": {
"name": {
"type": "keyword"
}
}
}
}
和上面rollover的时候大体一样,有两点不同:
- 在模板加上了全局查询别名taochy_read_alias,方面后续数据跨索引查询
- 在setting里面关联了lifecycle相关的配置,配置成刚才创建的policy,以及rollover需要的写别名taochy_write_alias
创建索引:
PUT /taochy-000001
{
"aliases": {
"taochy_write_alias": {
"is_write_index": true
}
}
}
这个和上面的rollover一致
查看此时taochy-000001的index属性:
GET taochy-000001
返回结果:
{
"taochy-000001" : {
"aliases" : {
"taochy_read_alias" : { },
"taochy_write_alias" : { }
},
"mappings" : {
"properties" : {
"name" : {
"type" : "keyword"
}
}
},
"settings" : {
"index" : {
"lifecycle" : {
"name" : "policy-taochy",
"rollover_alias" : "taochy_write_alias"
},
"refresh_interval" : "30s",
"number_of_shards" : "1",
"provided_name" : "taochy-000001",
"creation_date" : "1614760997464",
"priority" : "75",
"number_of_replicas" : "0",
"uuid" : "tLh8-5KXR8CSNibQWfleGQ",
"version" : {
"created" : "7090099"
}
}
}
}
}
可以看到刚才配置的lifecycle属性以及两个别名的配置
写入三条数据:
POST /taochy_write_alias/_bulk?refresh=true
{"name":"aa"}
{"name":"bb"}
{"name":"cc"}
返回结果:
{
"took" : 37,
"errors" : false,
"items" : [
{
"create" : {
"_index" : "taochy-000001",
"_type" : "_doc",
"_id" : "wVvFttIBUTVfQxRWwXyM",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1,
"status" : 201
}
},
{
"create" : {
"_index" : "taochy-000001",
"_type" : "_doc",
"_id" : "wlvFttIBUTVfQxRWwXyM",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1,
"status" : 201
}
},
{
"create" : {
"_index" : "taochy-000001",
"_type" : "_doc",
"_id" : "w1vFttIBUTVfQxRWwXyM",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1,
"status" : 201
}
}
]
}
数据写入到taochy-000001,符合预期;
再次执行上面的语句,返回结果如下:
{
"took" : 26,
"errors" : false,
"items" : [
{
"create" : {
"_index" : "taochy-000002",
"_type" : "_doc",
"_id" : "wVvFttIBUTVfQxRWwXyM",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1,
"status" : 201
}
},
{
"create" : {
"_index" : "taochy-000002",
"_type" : "_doc",
"_id" : "wlvFttIBUTVfQxRWwXyM",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1,
"status" : 201
}
},
{
"create" : {
"_index" : "taochy-000002",
"_type" : "_doc",
"_id" : "w1vFttIBUTVfQxRWwXyM",
"_version" : 1,
"result" : "created",
"forced_refresh" : true,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1,
"status" : 201
}
}
]
}
数据写入到taochy-000002,符合预期,另外,这里需要修改一个全局配置,就是elasticsearch加载ilm的周期,默认是10分钟,可以改成3秒,否则没10分钟才会检查一次,在这十分钟之内所有数据都会写入到taochy-000001,配置项修改如下:
PUT _cluster/settings
{
"transient": {
"indices.lifecycle.poll_interval": "3s"
}
}
此时再去看taochy-000001和taochy-000002的属性
{
"taochy-000001" : {
"aliases" : {
"taochy_read_alias" : { }
},
"mappings" : {
"properties" : {
"name" : {
"type" : "keyword"
}
}
},
"settings" : {
"index" : {
"lifecycle" : {
"name" : "policy-taochy",
"rollover_alias" : "taochy_write_alias"
},
"refresh_interval" : "30s",
"number_of_shards" : "1",
"provided_name" : "taochy-000001",
"creation_date" : "1614760997464",
"priority" : "75",
"number_of_replicas" : "0",
"uuid" : "tLh8-5KXR8CSNibQWfleGQ",
"version" : {
"created" : "7090099"
}
}
}
}
}
{
"taochy-000002" : {
"aliases" : {
"taochy_read_alias" : { },
"taochy_write_alias" : { }
},
"mappings" : {
"properties" : {
"name" : {
"type" : "keyword"
}
}
},
"settings" : {
"index" : {
"lifecycle" : {
"name" : "policy-taochy",
"rollover_alias" : "taochy_write_alias"
},
"refresh_interval" : "30s",
"number_of_shards" : "1",
"provided_name" : "taochy-000002",
"creation_date" : "1614760997464",
"priority" : "75",
"number_of_replicas" : "0",
"uuid" : "tLh8-5KXR8CSNibQWfleGQ",
"version" : {
"created" : "7090099"
}
}
}
}
}
从上面的对比可以看出来,taochy-000001只剩下读索引,所以通过写别名写入的数据不会再写入到该索引;而taochy-000002既有读索引也有些索引,新数据会写入到该索引,符合上面 rollover api的特性。
总结
随着elasticsearch版本的迭代以及功能场景的扩展,索引生命周期的管理变得重要且必须,所以在6.6版本之后推出了ilm的功能,这个功能在之前curator+rollover的基础上进行了功能的封装,方便了大家的使用,可以说是一个相当成熟且完善的功能,但是大家在使用的时候也要注意评估使用场景以及使用方法,目前在我看来ilm使用起码有两点需要注意:
- 由于写别名只能指向最新的index,所以有数据修改需求的场景该需求可能不合适,或者说不能直接使用,需要通过方案级别进行设计来达到目标。
- 由于当前读别名只能配置一个,也没办法配置选择数据类型(冷,热,温),所以search需要查询的数据量可能会覆盖所有数据,过滤条件尤其是时间过滤可能没办法直接起作用,这点我没有找到源生的解决方案,如果有人知道的话,麻烦留言告知,不甚感谢。我能想到的方案就是在代码里面异步的处理每个索引的别名, 在rollover后将前一个index的creation_date追加到该index的别名后面。这个方案要求乱序数据不能太多,且对乱序数据不是很敏感,补救的方法就是根据页面上乱序数据的时间特点以及对数据量的评估,向前追加合适个数的index加入到search的index中,放弃index的模糊查询而是用index的精确查询来提升效率。
虽然有上述的问题,但是ilm还是不失为是一个优秀的功能,值得大家使用,我也相信在后续版本中这个功能也会不断完善,大家一起期待吧!