前言
最近公司使用的ElasticSearch生产环境出现了好几次OOM问题,我们现在用的版本是:6.4.0,这个是非常老的版本,已经到了不得不升级的时候。
我们老大把升级的任务交给我了,配合DBA一起完成这个事情,然后写一个文档给其他团队参考,到时候他们按照这个文档调整相关的代码,完成升级工作。
1 如何选择版本?
目前ElasticSearch官方的最新版本是8.8.2,但我们公司为了考虑私有化部署,兼容阿里云服务器上的版本,目前阿里云上最新的只有:8.5.0、7.16.0和7.10.0,因此我们不打算升级到最新版。
我们打算用阿里云的最新版本:8.5.0。
于是DBA再dev环境,安装好了8.5.0版本的ElasticSearch服务器。
马上遇到第一个问题,之前老版本的ElasticSearch用的分词器是hanlp,但是这个分词器在阿里云上不支持,我们必须换成ik。
于是DBA找了一个跟ElasticSearch相同的版本的ik插件安装了。
通过kibana的dev tool工具,我测试了一下ElasticSearch的安装情况,顺便把ik分词器一起测试了一些,功能正常。
接下来,需要我调整代码了。
本来我以为很简单,找一个8.5.0版本的ElasticSearch版本的客户端,在代码中替换一下就搞定了。
但后面发现我们代码中使用了ElasticSearch的高级客户端elasticsearch-rest-high-level-client,它里面有个类:RestHighLevelClient,这个类在ElasticSearch8.0以上的版本已经被移除了,推荐使用一个第三方的elasticsearch-java。
为什么要抛弃elasticsearch-rest-high-level-client:
- 客户端"too heavy",相关依赖超过 30 MB,且很多都是非必要相关的;api 暴露了很多服务器内部接口
- 一致性差,仍需要大量的维护工作。
- 客户端没有集成 json/object 类型映射,仍需要自己借助字节缓存区实现。
但是这个类涉及的代码很多,大概有20多个方法使用了这个类。
如何直接升级pom.xml文件中ElasticSearch版本版本,就不得不重新这20多个方法,开发和测试工作量都不小。
没办法,只能使用一个兼容版本:7.x,虽说这些版本已经将RestHighLevelClient标记为@Deprecated(标识废弃了),但其功能还是可以使用的。
那么,我们具体要使用哪个版本呢?
首先有个很现实的问题我们不得不考虑:我们这边经常做私有化部署,必须要兼容阿里云上的版本,目前阿里云上有7.16.0和7.10.0两个版本可以选择。
于是,我打算使用7.16.3,它是7.16.x系列最新的版本,可能修复了一些bug。虽说已经有7.17.x的版本了,但目前7.16.3已经够用了。
2 编译不了
我将pom.xml文件中的elasticsearch和elasticsearch-rest-high-level-client的jar包升级到7.16.3之后,遇到的第一个问题是项目编译不了了。
idea很快帮我们识别了无法编译的类,原来是这些类中有些import的类已经不存在了,比如:
import org.elasticsearch.action.search.SearchResponse
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;idea帮我们标红了。
我删除无效引入,然后重新导入引入之后,这些标红很快消失了。
但还有地方在报错,比如:
SearchResponse secondResponse = esRestTemplate.sourceSearch(secondSearch);
if(response.getHits().totalHits > 0) {
...
}totalHits在新版本中不是int类型的了。
这个问题很快被解决了,totalHits新版本中是一个对象类型,该对象有个value属性,因此可以这样改造:
SearchResponse secondResponse = esRestTemplate.sourceSearch(secondSearch);
if(response.getHits().totalHits.value > 0) {
...
}还有type的问题:
BulkRequest bulkRequest = new BulkRequest();
IndexRequest indexRequest = new IndexRequest(INDEX);
indexRequest.type(TYPE);
indexRequest.id(logEntity.getId());
indexRequest.source(JSONUtil.fromObjToJson(logEntity), XContentType.JSON);
bulkRequest.add(indexRequest);
try {
BulkResponse bulkItemResponses = esClient.bulk(bulkRequest, RequestOptions.DEFAULT);
if (bulkItemResponses.hasFailures()) {
log.error("写es失败");
}
} catch (IOException e) {
log.error("写es异常", e);
}在新的版本中已经不支持type。
因此,需要把indexRequest.type(TYPE);这行代码注释掉。
经过这三次调整之后,代码可以顺利编译成功了。
3 index创建不了
我将项目中applicationContext-dev.properties文件中的elasticsearch的host、port和security-user改成新的。
然后将项目正常启动了。
于是,准备手动创建index开始测试功能。
用之前的创建脚本:
put es_test_index
{
"settings":{
"index":{
"analysis.analyzer.default.type":"ik_max_word"
}
},
"mappings":{
"item":{
"properties":{
"site_id":{
"type":"long",
"index":true
},
"content":{
"type":"text"
}
}
}
}
}其中item是我们自定义的type名称。
在kibana中执行,一直都报错:root mapping definition has unsupported parameters。
其实elasticsearch6时,官方就提到了elasticsearch7会删除type,并且elasticsearch6时已经规定每一个index只能有一个type。在elasticsearch7中使用默认的_doc作为type,官方说在8.x版本会彻底移除type。
我们需要将脚本改成这样:
put es_test_index
{
"settings":{
"index":{
"analysis.analyzer.default.type":"ik_max_word"
}
},
"mappings":{
"properties":{
"site_id":{
"type":"long",
"index":true
},
"content":{
"type":"text"
}
}
}
}需要直接把item,即type去掉。
修改之后,index创建成功了。
4 空指针异常
索引创建好了,我在本地启动job,尝试往新的elasticsearch数据库中写入数据。
但很快被无情打脸,该job还没执行完,就直接退出了,报了一个空指针异常。
我不一会儿就查到原因了,原来之前这里把type去掉了:
BulkRequest bulkRequest = new BulkRequest();
IndexRequest indexRequest = new IndexRequest(esIndex);
//indexRequest.type(esType);
indexRequest.id(getId());
indexRequest.source(JSONUtil.fromObjToJson(data), XContentType.JSON);
bulkRequest.add(indexRequest);
}
try {
return esClient.bulk(bulkRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
log.warn("保存数据失败", e);
return null;
}底层报了一个type为空,但调用了它的一个方法,就导致了空指针异常。
有没有办法重新设置type呢?
其实IndexRequest有个setTypeOp()方法,该方法有:CREATE、INDEX、UPDATE、DELETE四个值。
我们这里只需要设置setTypeOp()为CREATE即可。
这样调整之后,空指针的问题被解决了。
es_test_index索引的数据被正常写入到了elasticsearch数据库。
5 字段值不存在异常
上面我们已经将数据成功写入新的elasticsearch数据库了,现在开始测试查询功能是否正常。
使用命令:
GET es_test_index/_search
{
}很快出现了下面这个异常: A document doesn‘t have a value for a field! Use doc[<field>].size()==0 to check if
原因是我们写数据时,有个upstreamBrandName字段,可以允许为空,有些数据该字段有值,根本有些数据没有该字段。
而从elasticsearch7之后,如果doc['field'].value 中的field不存在,将抛出异常。
有些博客说,通过设置jvm参数: -Des.scripting.exception_for_missing_value,可以控制是否抛出异常。
我激动了一把。
找DBA尝试设置了几次后,发现没效果。
原来这个参数是给elasticsearch6用的,建议提前开启这个参数,跟elasticsearch7保持一直,也就是elasticsearch6总字段如果查询时不存在,也可以抛这个异常。
没办法,我们不得不加一个判断:
"script" : "if(doc[\"field\"].size() >0) return doc[\"field\"].value;else return null;"如果该字段的size大于0,才允许查询该字段的值。
幸好项目中只有一个地方有这个问题,最终该问题被解决了。
6 替换分词器
经过之前的代码调整之后,elasticsearch的写入和查询目前已经没有问题了。
但我们还需要改造一个非常重要的东西:分词器。
之前elasticsearch6的时候,我们用的分词器是:hanlp,但在阿里云上只支持ik。
因此,我们不得不将项目中的分词器,也改成ik了。
目前GitHub上最新版是:6.6.2,很久都没有更新过了,elasticsearch7以上用这个版本没啥问题。
项目中很快替换了分词器jar包。
但有个问题:分词需要热更新。
之前使用hanlp的时候,有个同事从GitHub上下载了elasticsearch-hanlp的源码,改造了一下,提供了一个动态往hanlp中添加分词的接口,该接口在后台管理系统中,有个UI界面的可以直接调用。
现在也用这种方式?
很快就发现行不通。
阿里云上elasticsearch的插件elasticsearch-ik,默认已经安装好,并且不允许卸载和重新安装。
也就是说,我们根本不能直接安装外部的elasticsearch-ik的jar包。
那么,该如何实现热更新呢?
其实elasticsearch-ik提供了一个配置文件:config/IKAnalyzer.cfg.xml,这个文件下有这几行配置:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">mydict.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">http://192.168.1.88/es_new_word.txt</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>其中ext_dict和ext_stopwords参数,可以配置本地的分词和停止分词,另外,remote_ext_dict和remote_ext_stopwords可以配置远程的分词和停止分词。
也就是说elasticsearch-ik对分词的支持有两种:本地和远程。
如果修改本地的配置文件,需要重新elasticsearch服务器才能生效,因此,该方法被否决了。
那么,我们可以提供一个获取扩展分词的接口或者txt文件。
这很容易办到。
但接下来,有个问题:如果elasticsearch-ik调用远程接口太频繁了,而这个接口的数据太多,经常超时,可能会出现问题。
带着这个疑问,我阅读了elasticsearch-ik的源码,发现它内部有个job,每隔60秒,调用一次配置的远程接口,将获取到的数据加载到内存中。
其实60秒也不太频繁。
虽说目前数据库中分词的数据不多,不过为了保险起见,我在远程接口中,增加了一个二级缓存,查询分词时,优先从二级缓存中查询数据,没有查到再从数据库中查询数据,然后放入二级缓存中。
这样改造之后,分词器的问题被解决了。
总结
有时候,基础组件升级,不是一件非常容易的事情,需要考虑点很多。
从选择版本,到解决不同版本差异化带来的一些问题。
一个大的原则是:能够尽可能小的改动的基础之上,使用更多的新的功能。
不要一味地迷恋最新的版本。
我们尽可能使用稳定的版本。
不光要考虑内网环境,还需要考虑云服务器,比如:阿里云,上面支持的功能有哪些,它可能对我们的技术方案,有一些决定性的因素。
升级完之后,要多回归测试,避免一些意想不到的问题。
有个好习惯是,把自己在升级过程中遇到的问题,用文档记录下来,以便于后面能够快速复盘,同时也能分享给大家,让大家少踩坑,少走一些弯路。