最近开发了个ES项目,由于是第一次开发,因此在上线时遇到了一堆坑,现总结如下。
上线后,好多java中查询ES的接口都报这个错,导致前端数据直接显示"N/A"了;尴尬的一批。
大概是java代码中用到了聚合的地方报错,记不清了;
总之原因就是生产数据比测试多太多,因此测试的时候没报错,生产报错了;
解决方法就是修改ElasticSearch的配置,在linux系统中用PUT请求修改的样例如下:
curl -u elastic:#123456@ -H 'Content-Type:application/json' -d '{
"index": {
"max_result_window": 2147483647
}
}' -XPUT "http://127.0.0.1:9200/exam_data/_settings"
说明:
1.生产装的ES是有账号密码的,-u是输入账号密码,账号是elastic,密码是#123456@,用:分割
2.-H是输入header
3.-d是输入参数,抄上方的代码时注意不要输入多余的空格与回车,否则会报错无法执行
4.-X是请求类型,PUT就是PUT请求
5.url中的exam_data是索引名,_settings是固定写法,修改设置的意思。
6.上方代码把返回窗口max_result_window修改为了2^32-1。
然后java代码中就可以这样写了:
TermsAggregationBuilder tab = AggregationBuilders.terms("userId").field("userId.keyword")
.order(InternalOrder.count(false)).size(Integer.MAX_VALUE).shardSize(Integer.MAX_VALUE);
说明:
1.由于需要数据准确性,因此将size与shardSize设置为最大;上方ES的max_result_window也是最大了,因此可以返回结果。
2.准确性提高后,响应速度可能会相应变慢,不过本项目可以接受。(还没有测试是否真的变慢了)
同样,测试数据少,生产数据多,导致这个问题也没有测试出来,一上生产就报错了。
解决方法:
也是修改ES配置,如下:
curl -u elastic:#123456@ -H 'Content-Type:application/json' -d '{
"persistent": {
"search.max_buckets":2147483647
}
}' -XPUT "http://127.0.0.1:9200/_cluster/settings"
或者:
curl -u elastic:#123456@ -H 'Content-Type:application/json' -d '{
"transient": {
"search.max_buckets":2147483647
}
}' -XPUT "http://127.0.0.1:9200/_cluster/settings"
说明:
1.这个请求将ES允许返回的最大buckets设置为最大值,2^32-1
2.这次是对所有索引生效的,_cluster。
3.persistent是永久生效;transient是临时生效,ES重启后就失效了。
java中查询考试次数时,本人选择了返回报文中的hits数作为返回结果;同理,测试数据小于10000,还没看出来问题;上生产后,发现这个值显示为10000,实际上考试次数应该远大于10000的。
解决方法:
不再使用hits数作为返回结果,而是使用CardinalityAggregationBuilder根据id去重(当然不会有重复的,是唯一id),然后用返回结果Cardinality对象的getValue()得到的数字,就是考试次数。(每一个id对应每一条数据,每一条数据就是一次考试信息)
//根据ES中的数据的commitTime字段,按天聚合
//之后获取commitTime按天聚合后的时间,bucket.getKeyAsString()
//以及获取每个桶的数量,bucket.getDocCount()
DateHistogramAggregationBuilder dhab = AggregationBuilders
.dateHistogram("group_by_commitTime")
.field("commitTime")
.fixedInterval(DateHistogramInterval.DAY);
总之,返回结果自己处理后类似一个map,{“2021-01-01”:“123”,“2021-01-03”:“222”}
因为某天没有数据的话,那天就不存在,需要手动补0,所以写了个java补0的方法;
例如补上{“2021-01-02”:“0”}
结果上生产发现,所有的结果都被0覆盖了
排查后,发现是ES返回了查询日期前一天的数据;
例如,查询2021年1月1日-2021年1月31日的数据,结果返回了2020年12月31日-2021年1月31日的数据;
由于代码中是从查询开始日期[2021年1月1日]开始循环匹配的,没有的补0;
结果2020年12月31日 < 2021年1月1日 ,导致所有结果都匹配失败,全部填写为0了。
下方是修复后的补0方法:
//某天没有数据的,补0
public static void fillDate(String startTime, String endTime, ArrayList result) throws Exception{
//假设传入参数的样式
startTime = "2021-07-01";
endTime = "2022-08-08";
MyBean my1 = new MyBean("2021-07-01","5");
MyBean my2 = new MyBean("2021-07-03","3");
MyBean my3 = new MyBean("2021-07-05","1");
//该list中的数据需要是有序的,日期从小到大
result = new ArrayList<>();
result.add(my1);
result.add(my2);
result.add(my3);
//准备补从开始日期到结束日期、没有数据的为0
//例如new MyBean("2021-07-02","0")然后装入list等
if(StringUtils.isNotEmpty(startTime) && StringUtils.isNotEmpty(endTime)){
//准备返回的list
ArrayList newResult = new ArrayList<>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = sdf.parse(startTime);
Date endDate = sdf.parse(endTime);
//借助calendar,从开始日期,每次加1,进行补
Calendar c = Calendar.getInstance();
c.setTime(startDate);
Iterator iterator = result.iterator();
MyBean fromListBean = null;
if(iterator.hasNext()){
fromListBean = iterator.next();
//TODO 问题就在这里,ES查询到了目标日期前一天的数据,因此这里处理一下
//预处理,如果结果集的第一个小于开始时间,那就让它next,指向大于等于开始时间,或者置空
//不知道为什么,ES查询到了这样的多余数据
//比如是2020-12-31
long day1 = sdf.parse(fromListVo.getDate()).getTime();
//比如是2021-01-01
long day2 = c.getTime().getTime();
while(day1 < day2){
if(iterator.hasNext()){
fromListVo = iterator.next();
day1 = sdf.parse(fromListVo.getDate()).getTime();
}else{
fromListVo = null;
break;
}
}
}
//当没有到最后一天时
while(c.getTime().getTime() <= endDate.getTime()){
//如果list为空
if(fromListBean == null){
MyBean bean = new MyBean();
bean.setDate(sdf.format(c.getTime()));
bean.setValue("0");
newResult.add(bean);
}
else{
String date = fromListBean.getDate();
String compareDate = sdf.format(c.getTime());
//如果日期相等
if(StringUtils.equals(compareDate,date)){
newResult.add(fromListVo);
//然后指向下一个元素,如果没有了,设为null
if(iterator.hasNext()){
fromListBean = iterator.next();
}else{
fromListBean = null;
}
}
//如果不相等,newBean,补0
else{
MyBean bean = new MyBean();
bean.setDate(sdf.format(c.getTime()));
bean.setValue("0");
newResult.add(bean);
}
}
//加一天
c.add(Calendar.DAY_OF_MONTH, 1);
}
//使用新的list; 没有写return,所以这样写
result.clear();
result.addAll(newResult);
}
}
indices.fielddata.cache.size: 40%
indices.breaker.fielddata.limit: 60%
说明:
size是 fielddata 分配的堆空间大小,默认为unbounded,Elasticsearch 永远都不会从 fielddata 中回收数据;
随着时间的推移,fielddata把堆空间用完,没法再给新的查询数据分配内存,内存就会溢出。
配置为40%后,缓存如果超出40%,fielddata就会把旧数据交换出去。(删除旧数据,防止缓存占满堆)
limit是断路器,默认就是60%。如果单个查询需要超过堆内存的 60%,就会返回错误信息,防止OOM(out of memory)发生。
正常情况下,单个查询超过60%,那就应该优化查询语句,而不是调高该值。
-Xms10g
-Xmx10g
注意:
limit也可以用PUT请求修改:
PUT /_cluster/settings
{
"persistent" : {
"indices.breaker.fielddata.limit" : "60%"
}
}
size则只能修改elasticsearch.yml文件,修改后重启项目,如下:
indices.fielddata.cache.size: 40%
如果使用PUT请求修改size,会报错不支持动态修改:
"reason": "persistent setting [indices.fielddata.cache.size], not dynamically updateable "
生产环境下,导出400多万条excel时,按照步骤五配置后,旧错误好了,又报一个新错误,Fail to save:
OpenXML4JRuntimeException: Fail to save: an error occurs while saving the package: The part /docProps/core.xml failed to be saved in the stream with marshaller ...
有时候还会报一个类似的错误:
OpenXML4JRuntimeException: The part /xl/sharedStrings.xml failed to be saved in the stream with marshaller ...
问题分析:
解决方法:
//建立连接的超时时间
httpPost.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 600000);
//等待服务端响应数据的超时时间
httpPost.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 600000);
upstream my_project {
server 10.1.2.3:8080;
server 10.1.2.4:8080;
keepalive 1024;
}
location ~ /my_project(.*) {
set $name my_project;
expires -1;
proxy_set_header Host $http_host;
proxy_http version 1.1;
proxy_set_header Connection "";
proxy_pass http://$name/my_project$1$is_args$args;
proxy_redirect off;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 2g;
add_header Access-Control-Allow-Origin *;
proxy_request_buffering off;
}