在上一篇文章中,我们介绍了Elasticsearch java client的一些基本用法,为了达到生产级别的使用标准,下面介绍一些进阶的用法。
1.客户端tcp连接超时
在我们创建客户端时,实际上创建的是RestClient,而底层使用的是apache的HttpClient,在创建后长时间无操作时这个连接可能会被关闭,此时客户端并不知晓,直接使用就会提示下文中的错误。再次请求又是正常的,因为客户端会重新创建连接。
java.net.SocketTimeoutException: 30,000 milliseconds timeout on connection http-outgoing-6 [ACTIVE]
at org.elasticsearch.client.RestClient.extractAndWrapCause(RestClient.java:915)
at org.elasticsearch.client.RestClient.performRequest(RestClient.java:300)
at org.elasticsearch.client.RestClient.performRequest(RestClient.java:288)
at co.elastic.clients.transport.rest_client.RestClientTransport.performRequest(RestClientTransport.java:147)
at co.elastic.clients.elasticsearch.ElasticsearchClient.search(ElasticsearchClient.java:1833)
Caused by: java.net.SocketTimeoutException: 30,000 milliseconds timeout on connection http-outgoing-6 [ACTIVE]
at org.apache.http.nio.protocol.HttpAsyncRequestExecutor.timeout(HttpAsyncRequestExecutor.java:381)
at org.apache.http.impl.nio.client.InternalIODispatch.onTimeout(InternalIODispatch.java:92)
at org.apache.http.impl.nio.client.InternalIODispatch.onTimeout(InternalIODispatch.java:39)
at org.apache.http.impl.nio.reactor.AbstractIODispatch.timeout(AbstractIODispatch.java:175)
at org.apache.http.impl.nio.reactor.BaseIOReactor.sessionTimedOut(BaseIOReactor.java:263)
at org.apache.http.impl.nio.reactor.AbstractIOReactor.timeoutCheck(AbstractIOReactor.java:492)
at org.apache.http.impl.nio.reactor.BaseIOReactor.validate(BaseIOReactor.java:213)
at org.apache.http.impl.nio.reactor.AbstractIOReactor.execute(AbstractIOReactor.java:280)
at org.apache.http.impl.nio.reactor.BaseIOReactor.execute(BaseIOReactor.java:104)
at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker.run(AbstractMultiworkerIOReactor.java:588)
at java.lang.Thread.run(Thread.java:748)
这个问题在Github的官方Issues上面也有相关的讨论:github.com/elastic/ela…
解决问题的关键在于让连接能够保持tcp keepalive,有两种方案:
方案一:在客户端中显式的开启keepalive选项
RestClient httpClient = RestClient.builder(new HttpHost(hostName, port))
.setHttpClientConfigCallback(hc -> hc
.setDefaultIOReactorConfig(IOReactorConfig.custom().setSoKeepAlive(true).build())
).build();
另外,还需要设置系统层面的tcp keepalive探测时间,默认值7200s太长可能会被主动关闭,建议修改为300s,默认配置如下:
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
方案二:在客户端设置keepalive策略,在超过指定时间后由客户端自行关闭,使用时再重新创建
RestClient httpClient = RestClient.builder(new HttpHost(hostName, port))
.setHttpClientConfigCallback(hc -> hc
.setKeepAliveStrategy((response, context) -> Duration.ofMinutes(5).toMillis()))
.build();
2.聚合统计
在Elasticsearch中Aggregation 分为3种类型:
- Metric: 计算类型,对字段进行计算平均值、求和等;
- Bucket: 分组统计,根据字段或者范围将文档分组到桶中进行统计;
- Pipeline:对聚合结果再次进行聚合统计;
在分组统计中有2个参数需要特别关注:Size、Shard size。官方文档:www.elastic.co/guide/en/el…
Size:在使用terms对字段进行分桶时,默认值返回top 10文档,即只有10个统计结果,通过设置size的大小可以返回所需大小,最大值不超过search.max_buckets。
MultiTermsAggregation aggregation = MultiTermsAggregation.of(s -> s.terms(
MultiTermLookup.of(t->t.field("product")),
MultiTermLookup.of(t->t.field("user"))
).size(100));
Shard size:在上一篇文章中,我们介绍过Elasticsearch的查询过程,需要从每个分片获取结果后,再由协调节点进行合并排序。由于数据分布不均匀的缘故,如果每个分片只获取size大小的文档,可能会出现统计偏差。
Elasticsearch的解决方案是获取比所需更多的文档,在一定程度上避免这个问题,也就是Shard size参数的用途。默认值:Shard size = size * 1.5 + 10,在数据偏斜严重的情况下,可以适当调大这个参数,当然也意味着更多的性能损耗。
3.数据快照
之前我们介绍过search_after 能够实现深度分页功能,而在一些大批量数据导出的场景下,通常需要保持数据游标不变来导出完整的数据,类似于快照的功能。而这就需要用到 point in time (PIT) 。
//获取pit id
OpenPointInTimeRequest openRequest = OpenPointInTimeRequest.of(o -> o
.index(getIndex())
.keepAlive(Time.of(t->t.time("10m"))));
OpenPointInTimeResponse openResponse = elasticsearchClient.openPointInTime(openRequest);
//查询数据
SearchRequest searchRequest = new SearchRequest.Builder()
.size(pageSize)
.sort(sortOptions)
.pit(p -> p.id(params.getPit()));
.build();
elasticsearchClient.search(searchRequest);
//关闭pit
ClosePointInTimeRequest closeRequest = ClosePointInTimeRequest.of(c -> c.id(pit));
elasticsearchClient.closePointInTime(closeRequest);
4.获取搜索结果数量
在默认情况下search接口返回的hits size最大值是10000,如果需要获取实际的结果总数,需要开启TrackHits
SearchRequest searchRequest = new SearchRequest.Builder()
.trackTotalHits(TrackHits.of(t->t.enabled(true)));
.build();
elasticsearchClient.search(searchRequest);
5.并发写入
一般情况下,Elasticsearch数据的写入会通过mq来进行触发,理论上可以通过mq的有序性来控制并发写入导致的数据覆盖问题,现实情况中考虑到性能、可靠性,较少采用这种方式。
方案一:增加version数据版本字段,通过CAS操作来实现乐观锁;
方案二:使用分布式锁,确保同一时间单个文档只有一个线程在执行更新操作,重试操作可以由mq来实现;
6.数据库事务
假设你正在使用Elasticsearch存储订单数据,在业务代码中的执行步骤如下:
- 更新MySQL中订单表数据;
- 发送订单变更的mq通知;
- 消费mq消息,从MySQL读取最新的数据写入Elasticsearch;
在运行一段时间后,你可能会发现Elasticsearch的数据与MySQL不一致,不是最新的版本;仔细分析上述过程会发现一个问题,在执行第2步操作时第1步的数据事务还没提交完成,将导致第3读取的不是最新数据。提供一种解决问题的思路,在事务提交完成后再发送mq消息。
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter(){
@Override
public void afterCommit() {
//发送mq
}
});
今天就先写到这里,你学"废"了吗。