ElasticSearch+Java后台第一次上线遇坑总结

最近开发了个ES项目,由于是第一次开发,因此在上线时遇到了一堆坑,现总结如下。

一、Java项目报错“all shards failed”

上线后,好多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.准确性提高后,响应速度可能会相应变慢,不过本项目可以接受。(还没有测试是否真的变慢了)

二、Java项目报错“too_many_buckets_exception”

同样,测试数据少,生产数据多,导致这个问题也没有测试出来,一上生产就报错了。
解决方法:
也是修改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重启后就失效了。

三、某个数据显示为上限10000

java中查询考试次数时,本人选择了返回报文中的hits数作为返回结果;同理,测试数据小于10000,还没看出来问题;上生产后,发现这个值显示为10000,实际上考试次数应该远大于10000的。

解决方法:
不再使用hits数作为返回结果,而是使用CardinalityAggregationBuilder根据id去重(当然不会有重复的,是唯一id),然后用返回结果Cardinality对象的getValue()得到的数字,就是考试次数。(每一个id对应每一条数据,每一条数据就是一次考试信息)

四、ES查询到了目标日期早一天的数据导致补0方法错误

  1. 之前ES查询回来的数据,是按天分开的;
//根据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”}

  1. 因为某天没有数据的话,那天就不存在,需要手动补0,所以写了个java补0的方法;
    例如补上{“2021-01-02”:“0”}

  2. 结果上生产发现,所有的结果都被0覆盖了

  3. 排查后,发现是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);
 
  }
}

五、ES查询报错Data too large

  1. 上线后,试了下导出400多万条数据的excel报表,结果导出失败;
  2. 这400多万条数据都需要从ES中查询出来;
  3. 导出失败后,再点按钮从ES查询其它信息,结果其它接口也报错了:Data too large
  4. 百度发现,可能是ES的缓存满了,没有来得及清理,导致了这个情况
  5. 虽然放着不管、等会后其它接口就又可以使用了,不过还是在配置文件中增加了2个配置:
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%,那就应该优化查询语句,而不是调高该值。

  1. 同时,需要修改ES的另一个配置文件,/config/jvm.options,修改其中的Xms与Xmx,默认是1g,修改为10g,如下:
-Xms10g
-Xmx10g
  1. 修改完后重启ES。

注意:
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 "

六、生产数据量大时导出excel失败,报错Fail to save

生产环境下,导出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 ...

问题分析:

  1. 多次试验发现,导出数据量少些时正常,导出数据量大时就会报错;大概是35秒后还没有导出成功,就会报这个错。
  2. 代码中是xSSFWorkbook.write(responseOutputStream)时报错,百度发现,可能是写一个已经关闭的流、就会报这个错。
  3. 百度发现,java中,httpConnection有两个重要的属性:http.connection.timeout和http.socket.timeout。connection timeout是建立连接的超时时间,socket timeout表示的是等待服务端响应数据的超时时间。
    如果不设置,默认时间与系统有关,30秒左右,不是整秒数。
  4. 所以当java发送get或post请求、所需要的连接时间较长时,应该手动设置。

解决方法:

  1. java代码中用到了HttpPost对象发送请求、获取另一台服务器上的excel报表,因此增加设置超时时间的代码,设置为10分钟,如下:
//建立连接的超时时间
httpPost.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 600000); 
//等待服务端响应数据的超时时间
httpPost.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 600000);
  1. 暂未测试,调高nginx配置中的keepalive
upstream my_project {
  server 10.1.2.3:8080;
  server 10.1.2.4:8080;
  keepalive 1024;
}
  1. 暂未测试,调高nginx配置中的3个timeout
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;
}
  1. 暂未测试,java导出数据量大的excel报表时,使用BigExcelWriter代替ExcelWriter。

你可能感兴趣的:(2020.4——)