es优化记录
-
背景:
由于我们的系统日志随着时间的推移越来越多,日志就是系统的操作日志,其实理论上也不会去查很久之前的数据,但是你如果去查就是会超时报错。我们用的是Scroll API 来进行滚动翻页的
-
原因:
由于es无法跳页查询只能从第一页开始翻,直到翻到指定的页码为止,比如我们需要查询第20487页的数据,所以,当分页数据为每页10条,那就需要翻页两千多次,那么接口就会超时了。
- 解决方案:
领导给了我两个解决方案,让我选择一个。
- 让我把这个报错修复好,可以接受查询这个日志的时候慢一点,但是不能接受报错
- 这个索引其实是配置了生命周期的,但是就是没有生效,让生命周期生效也可以(生命周期配置了删除近90天的数据,生效的话只查询近90天的数据肯定就不会报错了)
3.1 代码层面的解决方案
进行es查询时进行分批次查询。
3.1.1 具体实施细节
假如前端的要求是每页10条数据,要查询第20487页的数据。那么分批次查询我们可以每批次查询1000条数据,再从里面截取
a. 计算出这10条数据存在于第几批次;
b. 计算出这10条数据存在于这一批次的哪个区间;
c. 把这个区间从这一批次中截取出这10条数据即可。
3.1.2 代码示例
public IPage<LogsDto> getList(Integer currentPage,
Integer pageSize,
String keyword,
String logType,
String operateType,
String userName,
String ip,
String requestUrl,
String createTimeBegin,
String createTimeEnd,
String sort,
String order) throws ParseException {
BoolQueryBuilder boolQueryBuilder = getSearchQuery(keyword, logType, operateType, userName, ip, requestUrl, createTimeBegin, createTimeEnd);
FieldSortBuilder sortBuilder = **this**.getSortBuilder(sort, order);
List<LogsDto> logsDtoList = new ArrayList<>();
int scrollTimeInMillis = 60000;
int segmentSize = 1000; // 每次滚动查询的段大小
int segmentCount =segmentSize / pageSize;
// 定位出在第几页
int startPage = currentPage / segmentCount;
// 定位在第几页的第几页
**int** innerPage = currentPage % segmentCount;
NativeSearchQuery initialQuery = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withSort(sortBuilder)
.withPageable(PageRequest.of(0, segmentSize))
.build();
SearchScrollHits<LogsDocument> searchScrollHits = elasticsearchRestTemplate.searchScrollStart(scrollTimeInMillis, initialQuery, LogsDocument.**class**, IndexCoordinates.of(config.getIndexName()));
String scrollId = searchScrollHits.getScrollId();
for (int i = 0; i < startPage; i++) {
searchScrollHits = elasticsearchRestTemplate.searchScrollContinue(scrollId, scrollTimeInMillis, LogsDocument.**class**, IndexCoordinates.of(config.getIndexName()));
scrollId = searchScrollHits.getScrollId();
**if** (!searchScrollHits.hasSearchHits()) {
**break**;
}
}
if (searchScrollHits.hasSearchHits()) {
Pageable pageable = PageRequest.of(innerPage, pageSize);
Integer offset = Integer.valueOf(pageable.getOffset() + "");
int pSize = pageable.getPageSize();
int sub = offset + pSize;
int size = searchScrollHits.getSearchHits().size();
int min = Math.min(sub, size);
List<SearchHit<LogsDocument>> hits = searchScrollHits.getSearchHits().subList(offset,min);
for (SearchHit<LogsDocument> hit : hits) {
LogsDto dto = **new** LogsDto();
BeanUtils.copyProperties(hit.getContent(), dto);
dto.setCreateTime(LocalDateTime.ofInstant(hit.getContent().getCreateTime().toInstant(), ZoneId.systemDefault()));
logsDtoList.add(dto);
}
}
elasticsearchRestTemplate.searchScrollClear(Collections.singletonList(scrollId));
long totals = elasticsearchRestTemplate.count(**new** NativeSearchQueryBuilder().withQuery(boolQueryBuilder).build(), LogsDocument.**class**);
long allPages = (totals + pageSize - 1) / pageSize; // 计算总页数
IPage<LogsDto> logsDtoIPage = new Page<>();
logsDtoIPage.setCurrent(currentPage);
logsDtoIPage.setPages(allPages);
logsDtoIPage.setSize(pageSize);
logsDtoIPage.setRecords(logsDtoList);
logsDtoIPage.setTotal(totals);
return logsDtoIPage;
}
3.2 es配置生命周期来解决
这种解决方案其实是抛弃了历史的数据,因为历史数据其实访问的次数几乎为0,因为这些日志只是记录了哪些用户登陆操作系统,干了些什么,所有半年/一年前的数据其实并没有什么作用了
想要的达到的程度:
1、删除不需要或者价值不高的数据,仅保留近段时间,如近半年或者近三个月的数据;
2、让索引滚动更新,防止单个索引数据过于庞大。
3.2.1 具体实施细节
很重要的一点:
首先判断现在你现有的索引名称是否以数据结尾,最好是以6位数字结尾,-000001,如果不是,则生命周期是无法生效的。其实我们现在的索引就配置了生命周期,是仅保留的近90天的数据,但是没有生效,最后排查就是这个原因
3.2.1.1 新建生命周期策略
3.2.1.1.1 Kibana配置
设置hot阶段的策略(这里只做测试),这里我设置索引大小为20kb、文档数为3、30秒钟自动滚动,就是说只要达到其中一个条件就自动根据索引模板创建索引。
设置删除阶段,6分钟之后删除索引数据。
3.2.1.1.2. 命令配置
PUT _ilm/policy/my_policy{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_age": "30d",
"max_docs": 2,
"max_size": "5gb"
}
}
},
"delete": {
"min_age": "5m",
"actions": {
"delete": {}
}
}
}
}
}
3.2.1.2 创建索引模板并使用策略
3.2.1.2.1 Kibana配置
"index": {
"lifecycle": {
"name": "rj-log-ilm-policy",
"rollover_alias": "new-rj-log"
},
"number_of_shards": "1",
"number_of_replicas": "1"
}
}
注意:索引的别名(alias)需要和rollover_alias字段配置的相同
3.2.1.2.2 命令配置
PUT _index_template/new-rj-log-template
{
"version": 1.0,
"priority": 1,
"index_patterns": ["new-rj-log-*"],
"template": {
"settings":{
"number_of_shards": 1,
"number_of_replicas": 1,
"index.lifecycle.name": "rj-log-ilm-policy",
"index.lifecycle.rollover_alias": "new-rj-log" #这个别名记得和索引别名用的要一致不然会报illegal_argument_exception: index.lifecycle.rollover_alias [ilm_alias] does not point to index [ilm_index-000001]
}
}
}
3.2.1.3 创建索引并给索引绑定生命周期策略
PUT /new-rj-log-000001
{
"settings": {
"index": {
"lifecycle": {
"name": "rj-log-ilm-policy",
"rollover_alias": "new-rj-log"
},
"number_of_shards": "1",
"number_of_replicas": "0"
}
},
"mappings": {***...},
"aliases": {
"new-rj-log": {
"is_write_index": true
}
}
}
{
"settings": {
"index": {
"lifecycle": {
"name": "rj-log-ilm-policy",
"rollover_alias": "new-rj-log"
},
"number_of_shards": "1",
"number_of_replicas": "0"
}
},
"mappings": {},
"aliases": {
"new-rj-log": {
"is_write_index": true
}
}
}
{
"settings": {
"index": {
"lifecycle": {
"name": "rj-log-ilm-policy",
"rollover_alias": "new-rj-log"
},
"number_of_shards": "1",
"number_of_replicas": "0"
}
},
"mappings": {},
"aliases": {
"new-rj-log": {
"is_write_index": true
}
}
}
1、rollover_alias:new-rj-log。索引别名,索引的别名要和索引模板一样。
2、s_write_index(写索引):true。
3、这是给索引绑定生命周期策略:
"index": {
"lifecycle": {
"name": "rj-log-ilm-policy",
"rollover_alias": "new-rj-log"
}
3.2.1.4 测试验证
索引创建成功后,发现索引每隔30秒就会生成一个索引值,或者每插入3条数据就会生成一个索引。
到此说明配置的索引策略生效了,这里默认的情况,生成的索引的自增是6位的,比如000001。等待5分钟之后你会发现,有些索引已经被删除了,前面的两个索引已经被删除了。
- 内容拓展
4.1 内容一
策略中阶段中的各个动作都是可选的,可以只配置某个动作,也可以配置多个动作(如果某个动作失败了,后面的动作不会执行)。
4.2 内容二
如果索引需要重建,在重建之后需要把之前的数据或者一部分数据拷贝进新建的索引当中,可以使用一下命令进行索引之间的数据复制。
POST /_reindex?pretty
{
"source": {
"index": "rj-log",
"query": {
"range": {
"createTime": {
"gte": 1712831463000,
"lt": 1720693863000
}
}
}
},
"dest": {
"index": "new-rj-log-000001"
}
}
source.index:表示旧的索引名称
dest.index:表示新的索引名称
注意:新旧索引的字段映射要保持一致
执行成功之后会返回以下结果:
{
"took" : 16245,
"timed_out" : false,
"total" : 30293,
"updated" : 0,
"created" : 30293,
"deleted" : 0,
"batches" : 31,
"version_conflicts" : 0,
"noops" : 0,
"retries" : {
"bulk" : 0,
"search" : 0
},
"throttled_millis" : 0,
"requests_per_second" : -1.0,
"throttled_until_millis" : 0,
"failures" : [ ]
}
total:表示一共复制过去的文档数量。
tips:数据在各个阶段如何流转
| 阶段 | 描述 |
|---|---|
| hot | 主要处理时序数据的实时写入 |
| warm | 可以用来查询,索引变成 read-only,不可以写入数据(如分片重新分配、数据压缩等) |
| cold | 可以进行极其少量的查询,不可以写入数据 |
| delete | 数据将被删除 |
假设配置如下:
热阶段:索引达到50GB或30天后滚动更新。
温阶段:持续时间为10天。
冷阶段:持续时间为10天。
删除阶段:从索引进入此阶段开始计算,持续时间为40天。
现在,我们来估算数据从被创建到被删除的时间:
热阶段:
如果索引在30天内就达到了50GB,那么它将在这30天结束时滚动更新,并进入温阶段。
如果索引在30天内没有达到50GB,但超过了30天,它也会滚动更新并进入温阶段。
假设索引在25天时达到了50GB并滚动更新(这只是为了说明,实际情况可能不同)。
温阶段:
索引进入温阶段后,会持续10天。
冷阶段:
索引从温阶段过渡到冷阶段后,会再持续10天。
删除阶段:
索引进入删除阶段后,会根据40天的计时来考虑删除。但是,请注意,这里的40天是从索引进入删除阶段时开始计算的,而不是从索引被创建时开始。
现在,我们来计算总时间:
假设索引在热阶段用了25天(达到50GB)。
加上温阶段的10天。
再加上冷阶段的10天。
此时,索引已经经过了45天,但还没有进入删除阶段。一旦索引进入删除阶段(这通常是在冷阶段结束后自动发生的,除非策略中有其他条件阻止它进入),它将再等待40天才会被删除。
因此,从索引被创建到被删除的总时间将是:
45天(热阶段+温阶段+冷阶段) + 40天(删除阶段) = 85天
但是,请注意这个计算是基于一些假设的,实际情况可能会有所不同。特别是,如果索引在热阶段就用了30天(比如因为数据写入速度较慢),那么总时间将会更长。另外,如果集群的ILM策略执行频率较低,或者存在其他影响索引生命周期的因素,也可能会导致实际时间与估算时间有所不同。
因此,为了准确了解数据从被创建到被删除所需的时间,建议在实际环境中监控索引的生命周期,并根据需要进行调整。