本文将从三个方面介绍Elasticsearch索引生命周期管理的特性,首先会介绍ES索引生命周期管理的基本原理,其次会通过一个常见的日志场景来一步步配置索引生命周期管理,最后向大家介绍在日常的ES运维工作中遇到的关于索引生命周期管理常见的问题及解决方法。
一、ES索引生命周期管理之原理篇
1.1 ES索引规划基本原则
随着腾讯云上ES产品功能日益完善以及内核性能的逐步增强,有越来越多的客户选择使用腾讯云ES集群来做搜索推荐等业务及日志分析等工作。当我们在给云上集群做运维工作时,总结出了如下几条索引规划的原则:
- 索引分片设置为数据节点的倍数
- 单个分片大小控制在30GB-50GB
- 单个节点总分片建议不超过1000个
- 集群总分片个数控制在3万以内
之所以要把索引分片设置为数据节点的倍数,主要是能够充分利用每个数据节点的性能,避免热点问题出现。而当我们能够严格按照前3条原则来实施的时候,集群的总分片个数也就能够很好的进行控制了,不至于集群总分片个数太大,造成Master节点压力较大,从而导致集群的元数据更新时延较高,例如创建索引、更新mapping等操作。
另外,对于日志分析类场景来说,单个分片大小建议控制在30-50GB的原因主要有两个:第一,如果分片大小太大(如300GB以上),就会导致分片恢复和搬迁的速度很慢,而如果将分片设置的太小,又会使得集群的总分片个数膨胀的比较厉害,影响集群的稳定性;另外一个原因是ES集群对分片的doc数量做了限制,上限是21亿条,如果单个分片太大的话,很有可能会触发doc数量的限制,导致集群red或者丢数据的情况。
常规做法:当我们把这些索引规划原则分享给客户的时候,客户通常的做法是提前对写入的数据量进行评估,然后规划集群规模,如节点配置、节点数量和磁盘容量、设置好分片索引,然后通过Logstash等组件按天/小时进行切换索引。
问题和痛点:
1)随着时间的推移,集群的数据量会越来越大,总分片个数越来越多,集群写入性能受到影响;
2)遇到运营活动等情况,会导致当天的索引数据量是平常的数倍,影响集群写入;
3)历史索引和新索引使用同样的存储介质,集群成本较大。
为了解决以上问题,Elasticsearch在6.6版本推出了索引生命周期管理(ILM)。
从图1可以看出,索引生命周期管理有两个最基本的概念,即Phase和Action。
ES将索引划分为四个阶段,分别的是Hot、Warm、Cold和Delete。在Hot阶段,主要处理时序数据的实时写入,承担热点数据的查询,支持Rollover Action;在Warm阶段,索引被设置为只读,不再写入和更新,主要是用来查询,如查询7天内的索引数据,支持Read-Only,Force-Merge,Shrink,Allocate等Action;等了在Cold阶段,索引同样不再更新, 查询更少,查询速度也会更慢,支持Frozen和Allocate Action;而到了Delete阶段的索引,则表示索引数据将被删除,集群内的磁盘空间迅速释放,通过会在删除索引前,先将索引备份到腾讯云COS中。下面分别介绍每个Phase支持的Action。
1.2 ILM Action:Rollover
Rollover是滚动的意思,能够实现根据索引的大小、文档数和创建时间自动切换到新的索引。当Rollover触发时,新的索引将会被创建,Alias别名自动指向新的索引,并将新索引的is_write_index属性设置为true。由于业务端在写入的时候并无法感知到何时触发Rollover,因此Rollover必须结合Alias一起使用。
设置索引Rollover conditions API:
代码语言:javascript复制POST /nginx_log/_rollover
{
"conditions": {
"max_age": "1d",
"max_docs": 100000000,
"max_size": "30gb"
}
}
如下图2所示,客户端只通过别名nginx_log向集群中写入数据,刚开始别名指向的索引是nginx_log_000001,随着数据不断写入,nginx_log_000001触发了设置的rollover的condition后,如size满足了30gb,则会自动创建新的索引nginx_log_000002,并将别名指向nginx_log_000002,新写入的数据都会写入到nginx_log_000002中,而nginx_log_000001将被设置为只读。
Rollover优势:
- 避免单个索引过大
- 提升索引写入性能
- 方便管理索引数据
Rollover避坑指南:
合理设置触发 Rollover的Condition,否则容易导致集群中大量的小索引,使得总分片数过多,影响查询性能和集群稳定性。
1.3 ILM Action:Shrink
Shrink作为降低索引主分片的利器,Shrink API可以实现将集群中存量的主分片数量收缩到一个很小的值,新索引的主分片数量必须是原索引主分片数量的因子。例如原索引主分片数量为20,那么Shrink后的索引主分片数量可设置为 1、2、3、5、10。不能是其他的值。执行Shrink操作的前提条件如下:
- 索引必须设置为只读
- 索引的健康状态必须是Green
- 所有的主分片或者副本分片都必须集中到一个节点上
首先将源索引设置为只读,并将分片汇集到某一个节点上:
代码语言:javascript复制PUT nginx-log-000001
{
"settings": {
"index.routing.allocation.require._name": "11260372000288532",
"index.blocks.write": true
}
}
然后对源索引执行Shrink操作,将源索引主分片收缩为1个:
代码语言:javascript复制POST /nginx-log-000001/_shrink/nginx-log-000001_shrink
{
"settings": {
"index.number_of_shards": 1
}
}
Shrink原理:
Shrink底层是通过硬连接来实现,如下图3所示,源索引原本有4个分片,将4哥分片组成的一份完整数据被汇聚到一个节点上后,每个分片号占用一个文件名称。
当我们执行了Shrink API后,内核层面通过硬连接方式将4个文件夹映射为一个文件夹,文件夹名称为0号分片。
当新索引的硬连接创建好之后,则可以将源索引执行删除了,最后的效果就如图5所示。
需要注意的是:
1)如果集群所在的文件系统不支持硬连接操作,则需要将索引中所有的segment文件拷贝到新索引上;
2)如果节点使用了多路径的方式挂载了多盘,则也需要执行复制操作。因为系统层面无法在多个文件系统之间执行硬连接操作。
1.3 ILM Action:Frozen
ES集群有三种健康状态,分别是Green,Yellow和Red。索引层面也有三种状态,分别是Open、Frozen和Close状态。如图6所示:
- Open状态的索引表示索引是可搜索并可读写的,倒排索引等数据结构加载进内存,因此搜索性能最高;
- Frozen状态的索引表示索引依然可以被搜索,但是无法被写入,且只占用磁盘空间,不占用内存空间,因此搜索较慢;
- Close状态的索引表示索引不可读写,不占用内存空间,只占用磁盘空间。但是需要注意的是Close索引无法正常Allocate。
Frozen适合的场景是时序数据 冷热分离的架构,尤其是当集群规模比较大的时候,数据持续写入导致节点JVM很高频繁触发GC和读写熔断时,可以将部分历史索引冻结,以释放节点内存,保障集群稳定性。
Frozen缩冻结索引API:
代码语言:javascript复制POST /nginx_log-000001/_freeze
解冻索引API
代码语言:javascript复制POST /nginx_log-000001/_unfreeze
需要注意的是,当索引被冻结后,默认是搜索不出来的,即通过正常的Search查询不会去查询被冻结的索引。因此,如果期望在查询数据时,也能查到被冻结的索引数据,则需要在Search查询中添加ignore_throttled=false参数,如下所示:
代码语言:javascript复制GET /nginx_log/_search?ignore_throttled=false
1.4 ILM Action:Allocate
Allocate可作用于warm和cold Phase。必须指定include、exclude、require中的一个选项。
- include: 将索引分片分配到至少满足其中一个属性的节点上;
- require: 将索引分片分配到满足所有属性的节点上;
- exclude: 将索引分片分配到不包含其中任何一个属性的节点上。
例如,我们需要将索引从hot属性节点上迁移到warm属性节点上,则可以使用require,api如下:
代码语言:javascript复制PUT nginx-log-000001/_settings
{
"index.routing.allocation.require.temperature": "warm"
}
再比如我们需要将一个节点上的分片驱逐到其他节点上,则可以使用exclude,api如下:
代码语言:javascript复制PUT _cluster/settings
{
"persistent": {
"cluster.routing.allocation.exclude._name": "160701276700027053211"
},
"transient": {
"cluster.routing.allocation.exclude._name": "160701276700027053211"
}
}
1.5 ILM Policy
Policy 是索引生命周期管理的发动机,有了Policy才能够真正的将索引的生命周期管理起来。让静态的索引联动起来。也就是说有了Policy就能够将独立的Action给串联起来,索引也只有关联了对应的Policy,才会按照Policy中定义的Action执行。
- 新增索引:在索引模版中指定Policy 名称
- 存量索引:在索引的settings中指定Policy 名称
如果我们是新创建了索引,后创建的Policy,那如果需要将存量的索引也关联Policy的话,则需要手动设置存量索引的settings,将存量索引与Policy进行关联:
代码语言:javascript复制PUT nginx-log_000001/_settings
{
"settings": {
"index.lifecycle.name":"nginx-log-ilm-policy"
}
}
如果想要查看Policy的详情,可以在Kibana上通过可视化界面查看,也可以直接通过API进行查看:
代码语言:javascript复制GET _ilm/policy
而对于新增的索引,若想在创建时就自动关联Policy,则需要在索引模板中进行设置,如下面的API所示:
代码语言:javascript复制PUT _template/nginx_log_template
{
"order": 100,
"index_patterns": [
"nginx-*"
],
"settings": {
"index": {
"lifecycle": {
"name": "nginx-log-ilm-policy",
"rollover_alias": "nginx-log"
},
"routing": {
"allocation": {
"require": {
"temperature": "hot"
}
}
},
"refresh_interval": "30s",
"number_of_shards": "5",
"translog": {
"sync_interval": "5s",
"durability": "async"
},
"max_result_window": "65536",
"unassigned": {
"node_left": {
"delayed_timeout": "5m"
}
},
"number_of_replicas": "1"
}
}
}
我们通过在索引模版中指定index.lifecycle.name为定义的Policy名称,以及index.lifecycle.rollover_alias为Rollover的别名即可。
二、ES索引生命周期管理之实践篇
在上面的原理篇,我们逐一介绍了索引生命周期管理中几个比较重要的Action。以及每个Action适用的Phase,同时也介绍了Policy的定义,与索引的关联绑定等。下面我们从实践操作出发,来感受下索引生命周期管理的强大功能。
2.1 实践效果
我们以常用的日志场景为例,结合腾讯云ES集群冷热分离架构,来逐步实施如何使用ILM,我们想要达到的生产效果如下:
1)将新索引实时写入到ES集群中的热节点上,当索引达到特定条件后,数据滚动 (Rollover) 写入到新索引;
2)索引在热节点上滚动完成后在hot阶段停留三天后,迁移 (Allocate) 到温节点,即进入warm阶段;
3)warm阶段将索引设置为只读 (Read-only),将副本设置为0,将主分片个数缩小 (Shrink) 到1个;
4)索引在温节点上停留7天后(从滚动更新时算起),进入delete阶段;
5)索引阶段delete阶段后执行删除 (Delete) 操作。
2.2 操作步骤
总体上,实施ILM一般都会按照如下四步来操作:
1)创建Policy
2)创建索引模版
3)创建初始索引
4)通过别名写入数据
下面我们进入详细操作环节:
第一步:创建Policy
Kibana路径:Kibana– Management– Index Lifecycle Policys – Create policy
在Kibana上的操作详情如下图8-10所示:
除了可以在Kibana上通过可视化界面来创建,我们也可以直接通过API来创建:
代码语言:javascript复制PUT _ilm/policy/nginx-log-ilm-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_age": "1d",
"max_size": "50gb",
"max_docs": 10000000
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "2d",
"actions": {
"allocate": {
"require": {
"temperature": "warm"
},
"number_of_replicas": 1
},
"forcemerge": {
"max_num_segments": 1
},
"shrink": {
"number_of_shards": 1
},
"set_priority": {
"priority": 50
}
}
},
"delete": {
"min_age": "7d",
"actions": {
"delete": {}
}
}
}
}
}
表1. 定义ILM的Policy各阶段说明
参数 | 说明 |
---|---|
Hot phase | 该策略设置索引只要满足其中任一条件:数据写入达到50GB、使用超过1天、doc数超过1千万,就会触发索引滚动更新。此时系统将创建一个新索引,该索引将重新启动策略,而旧索引将在滚动更新后等待3天进入warm阶段。 |
Warm phase | 索引进入warm阶段后,ILM会将索引副本设置为0,主分片收缩到1个。完成该操作后,索引将在7天(从滚动更新时算起)后进入delete阶段。 |
Delete phase | 索引进入delete阶段,将在7天后(从滚动更新时算起)被删除 |
注意事项:
1)Policy名称一旦创建后便不可修改,各Phase中的Action则可以修改;
2)Kibana上指定的min_age最小单位是小时,不便于测试,而API方式则可以指定到秒和分钟。
第二步:创建索引模版
Kibana路径:Kibana – Management – Index Management – Index Templates
索引模板的创建在原理篇中我们已经做了介绍,这里最主要的就是需要关联上第一步创建的policy,已经Rollover的别名。 表2是在索引模板中定义的一些参数属性说明。
表2. 索引模板中定义的参数属性说明
参数 | 说明 |
---|---|
index.routing.allocation.require.temperature | 指定索引新建时分片的节点 |
index.lifecycle.name | 指定关联的ILM策略名称 |
index.lifecycle.rollover_alias | 指定rollover的别名 |
index.refresh_interval | 指定索引refresh周期 |
index.number_of_shards | 指定索引的主分片数 |
index.number_of_replicas | 指定索引的副本数 |
第三步:创建初始索引
如果我们在Hot phase定义了Rollover,则必须要手动创建一个初始索引,并且将该初始索引与索引别名进行关联,创建初始索引使用如下api:
代码语言:javascript复制PUT nginx-log-000001
{
"aliases": {
"nginx-log":{
"is_write_index": true
}
}
}
is_write_index设置为true的意思就是表明当前的初始索引nginx-log-000001是可以写入的索引,即当我们通过别名nginx-log向集群中写入数据时,数据后直接写入到实体索引nginx-log-000001中。
在生产环境中,我们通常希望在索引名称上加上当前日期,以快速判断该索引创建的具体时间。例如通过下面的api创建的初始索引就是直接带上了年月日,索引名称中的转义字符如图11-12所示。
代码语言:javascript复制PUT
{
"aliases": {
" nginx-log ":{
"is_write_index": true
}
}
}
在创建初始索引过程中,需要注意下面三点:
- 使用rollover滚动索引时候必须先手动创建初始索引;
- 初始索引必须是以000001结尾,长度6位,否则策略不生效,滚动的索引名会自动 1;
- 如果需要在索引名称上以日期进行标记,则可以使用{now/d},具体的表达式可以参考官方文档。
第四步:通过别名写入数据
当我们创建好初始索引后,即可以直接通过索引别名向集群中写入数据了,我们使用如下api模拟写入几条数据:
代码语言:javascript复制POST _bulk
{ "create" : { "_index" : "nginx-log", "_id" : "1" } }
{ "message" : "11" }
{ "create" : { "_index" : "nginx-log", "_id" : "2" } }
{ "message" : "22" }
{ "create" : { "_index" : "nginx-log", "_id" : "3" } }
{ "message" : "33" }
{ "create" : { "_index" : "nginx-log", "_id" : "4" } }
{ "message" : "44" }
{ "create" : { "_index" : "nginx-log", "_id" : "5" } }
{ "message" : "55" }
{ "create" : { "_index" : "nginx-log", "_id" : "6" } }
{ "message" : "66" }
{ "create" : { "_index" : "nginx-log", "_id" : "7" } }
{ "message" : "77" }
当我们向集群中写入一些数据后,可以看到索引中已经有了7条doc。
如果我们持续往集群中写入数据,则会不停的滚动创建新的索引,并且会按照我们在policy中定义的那样,滚动完成后1天自动迁移到温节点,然后在温节点上自动执行Shrink操作、去除副本和ForceMerge、Read-Only等操作,然后再在温节点上停留7天后从集群中删除,即进入Delete Phase。
三、ES索引生命周期管理注意事项
通过我们对腾讯云ES客户大量的集群运维经验来看,总结整理了如下5点需要注意的问题:
3.1 min_age问题
- 如果 Policy 中没有设置 rollover,则每个阶段的 min_age 是以索引的创建时间为开始;
- 如果 Policy 中设置了 rollover Action,则每个阶段的 min_age 是从索引执行完 rollover 后开始计算;
- 索引从 snapshot 中恢复到集群后,如果索引关联了 Policy,则 min_age 同上,不是以索引恢复后的时间。
尤其是第三点,我们经常收到客户的工单反馈,从快照中恢复出来的索引很快就从集群中又被删除了,也不是人为操作。经过详细分析发现是由于该索引关联了ILM的Policy,而在该策略中定义了Delete Phase,也就是说在删除之前,会将索引备份到COS,备份成功后再进入到Delete阶段。因此在快照备份的时候,此时索引还处于Delete阶段之前。然后从快照总恢复到集群后,还是会从上一次备份前的min_age进行执行,当执行到Delete Phase后就会再次删除索引了。
3.2 ILM的执行周期
ILM 是周期性的执行,默认 10min触发一次。因此当索引满足了某一个 Condition 后,并不会立马执行 Action。如设置的 max_doc 为 10,可能会索引的 doc 超过了 10 后才开始 Rollover。可以通过下面的 API 进行调整执行周期:
代码语言:javascript复制PUT _cluster/settings
{
"transient": {
"indices.lifecycle.poll_interval":"10m"
}
}
通常我们在测试环境中会调用上面的api将interval设置的很小,如1分钟。但是在生产环境不建议将该值设置得过小,避免时间间隔太短给集群造成较大负载;另外还有一点需要注意的就是:当到达一个执行周期后,检查到了索引满足了某一个阶段的Condition,但是由于前一个阶段还没有执行完,则不会执行下一个阶段。举个例子,当我们设置的策略是索引在滚动完成后1天开始迁移到温节点,2天删除,但是由于在迁移到温节点过程中,由于迁移速度和并发度很低,可能迁移了3天。这时候已经满足了删除的Condition了,但是由于还没有完成Warm Phase,因此即使满足删除条件,也不会执行删除动作,而是会等Warm阶段执行完成后才开始执行下一步操作。
3.3 Policy更新及删除问题
关于ILM的Policy问题,这里重点需要关注三个问题:
1)Policy的名称一旦确定后将不可更改
2)Policy中定义的各Action均可更改
Policy更新后,对于新增的索引会立即使用最新版本的Policy;
对于正处于Policy执行中的索引,只有当前阶段的Action执行完成后,进入到下一个阶段的Action才会使用最新版本的Policy (ES会将当前执行的phase定义信息缓存到索引元数据中);
对于已经完整执行了Policy的存量索引,更新了Policy后不会再次执行。 例如,原Policy只包含了从热节点迁移到温节点的Action,索引在完成了迁移后,状态即为completed,这时候再在Policy中加入Delete的Action,则历史索引将不会被删除。
解决办法:
1) 移除再关联
先将索引的Policy 移除:
代码语言:javascript复制POST {index_name}/_ilm/remove
再重新关联Policy(适合索引较少的场景)。
代码语言:javascript复制PUT {index_name}/_settings
{
"lifecycle.name": "ilm_policy"
}
2)直接重启ILM
第二种解决方法就是直接重启下ILM,适合索引较多的场景。
代码语言:javascript复制POST _ilm/stop
POST _ilm/start
第三个需要关注的Policy问题就是Policy的删除
3)Policy需要等所有关联的索引都解关联后,才可被删除
3.4 从snapshpt中恢复索引的潜在问题
如果之前备份到COS中的索引关联了Policy,在恢复到集群中后,ILM依然会按照关联的Policy执行,并且min_age是从索引创建的时间或者从rollover滚动后的时间开始计算,而不是从快照中恢复完成的时间计算,如图14所示。
例如,如果我们之前给索引关联的Policy是有Delete的Action,那么在索引备份完成后Policy就会直接从Delete阶段执行,索引将再次被删除。也就是说备份之前是在哪个阶段,恢复后还是在那个阶段。 如果希望索引从快照中恢复后不再自动执行ILM,则可以按照下面的步骤来配置:
1)先停止ILM:
代码语言:javascript复制POST _ilm/stop
2)开始执行快照的恢复工作:
代码语言:javascript复制POST /_snapshot/my_backup/snapshot_name/_restore
3)对恢复完成后的索引关联的Policy进行修改,可以移除Policy,或者更新Policy:
代码语言:javascript复制POST {index_name}/_ilm/remove
4)重新启动ILM:
代码语言:javascript复制POST _ilm/start
5)查看ILM的状态:
代码语言:javascript复制GET _ilm/status
或者,直接在快照恢复的API中忽略Policy的相关配置:
代码语言:javascript复制POST /_snapshot/repository_name/snapshot_name/_restore
{
"ignore_index_settings":"index.lifecycle.name,index.lifecycle.rollover_alias"
}
3.5 ILM执行失败问题处理
当ILM在执行Policy的特定阶段时出错,ILM将会将该索引执行状态设置为”error”。然后停止Policy的进一步执行。例如,当我的Policy中有把索引的主分片最终Shrink到2个分片。然后我创建了一个新的索引只有一个主分片。但是这个索引也关联了该Policy,那在执行到该索引的时候就会报错。导致ILM执行不下去。可以通过 explain API来查看。找到执行失败的原因后,解决相关的问题,然后再重试ILM即可。
查看索引的ILM执行详情:
代码语言:javascript复制GET {index_name}/_ilm/explain
重试索引的ILM执行任务:
代码语言:javascript复制POST {index_name}/_ilm/retry
这里重试ILM的时候需要注意,必须是索引名称,不可以是索引别名,否则重启ILM不生效) (7.X版本会自动重试)
四、ILM索引生命周期管理总结
ILM分为四个Phase,分别是Hot、Warm、Cold和Delete。在Hot阶段支持Rollover Action,在Warm阶段支持Read-Only、Force-Merge、Shrink、Allocate等Action,在Cold阶段支持Frozen、Allocate等Action,在Delete阶段支持Delete的Action。
通常我们实施ILM的时候,一般通过如下四步进行:
第一步:创建Policy
第二步:创建索引模版
第三步:创建初始索引
第四步:通过别名写入数据
在本文中,我们也详细介绍了很多ILM相关的API,现汇总如下:
ILM相关API | API说明 |
---|---|
PUT _ilm/policy/<policy_id> | 创建ILM的Policy |
GET _ilm/policy<policy_id> | 获取ILM的Policy |
DELETE _ilm/policy/<policy_id> | 删除特定的Policy |
GET _ilm/status | 获取ILM的状态 |
POST _ilm/start | 启动ILM |
POST _ilm/stop | 停止ILM的执行 |
GET <index_name>/_ilm/explain | 获取ILM的执行详情 |
POST <index_name>/_ilm/retry | 重试某个索引的ILM执行 |