Elasticsearch的CircuitBreaker
文章目录
本文主要以6.7版本为参考
Elasticsearch 具有多个熔断器用于防止各种请求操作造成 OOM , 每个熔断器限制了它可以使用的内存。 此外, Elasticsearch 还有一个父熔断器用于限制所有熔断器的内存总量。
支持在集群上执行API来更新部分熔断器的配置。
熔断器种类
内存熔断器类
-
parent circuit breaker:
父熔断器,会根据所有子熔断器计算结果觉定是否触发。
动态配置:
- indices.breaker.total.limit :JVM的内存比例,默认值
70%
- indices.breaker.total.limit :JVM的内存比例,默认值
-
fielddata circuit breaker:
字段数据熔断器允许 Elasticsearch 估计一个字段加载到 fielddata cache 需要的内存量, 如果加载该字段会导致超过限制, 则熔断器将停止载入并返回 error。
动态配置:
- indices.breaker.fielddata.limit :JVM的内存比例,默认值
60%
- indices.breaker.fielddata.overhead :估算因子,在计算内存是需要与估算因子相乘得到内存估算值。默认
1.03
- indices.breaker.fielddata.limit :JVM的内存比例,默认值
-
request circuit breaker:
请求熔断器使 Elasticsearch 可以防止每个请求的数据结构(例如, 用于在请求期间计算聚合的内存) 超过一定数量的内存 。
动态配置:
- indices.breaker.request.limit :JVM的内存比例,默认值
60%
- indices.breaker.request.overhead :估算因子,在计算内存是需要与估算因子相乘得到内存估算值。默认
1
- indices.breaker.request.limit :JVM的内存比例,默认值
-
in flight request circuit breaker:
进行中的请求熔路器使 ES 可以限制 transport 或 HTTP 级别上所有当前活动的即将传入请求的内存使用,使其不超过节点上的一定内存量。 内存使用情况取决于请求本身的内容长度。 该断路器还认为, 不仅需要内存来表示原始请求, 而且还需要将其作为结构化对象, 这由默认开销 反映出来。
动态配置:
- network.breaker.inflight_requests.limit :JVM的内存比例,默认值
100%
- network.breaker.inflight_requests.overhead :估算因子,在计算内存是需要与估算因子相乘得到内存估算值。默认
1
- network.breaker.inflight_requests.limit :JVM的内存比例,默认值
-
accounting circuit breaker:
计费断路器允许限制请求完成后未释放的内存中所保存内容的内存使用量。 这包括 Lucene 段内存之类的东西, 例如 Segment Memory。
动态配置:
- indices.breaker.accounting.limit :JVM的内存比例,默认值
100%
- indices.breaker.accounting.overhead :估算因子,在计算内存是需要与估算因子相乘得到内存估算值。默认
1
- indices.breaker.accounting.limit :JVM的内存比例,默认值
其他
- Script compilation 熔断器 :脚本编译熔断器限制一段时间内 Script 编译次数。
Elasticsearch 内存部分
ES使用的JVM内存的中存在几大类无法GC的缓存
-
QueryCache:
支持调用API进行清理。
实现类
org.elasticsearch.indices.IndicesQueryCache
查询缓存负责缓存查询结果。每个节点有一个查询缓存,由所有分片共享。查询缓存只缓存在过滤器上下文中使用的查询,即用来缓存filter查询。
以下设置是静态的,必须在群集中的每个数据节点
elasticsearch.yml
上配置:- indices.queries.cache.size :默认
10%
。
以下设置是静态的,索引级别
- index.queries.cache.enabled :是否对某个索引开启查询缓存,默认
true
。创建索引时配置settings中。
- indices.queries.cache.size :默认
-
RequestCache:
支持调用API进行清理。
实现类
org.elasticsearch.indices.IndicesRequestCache
分片级请求缓存模块在每个分片上缓存本地结果。这使得频繁使用的搜索请求(可能很繁重)几乎能立即返回结果。请求缓存非常适合日志场景,在日志场景中,只有最新的索引会被主动更新,而较早索引的结果将直接从缓存中提供。
以下设置是静态的,必须在群集中的每个节点
elasticsearch.yml
上配置:- indices.requests.cache.size : 默认
1%
以下设置是索引分片级别
- index.requests.cache.enable :开启
true
,关闭false
,创建索引时配置settings中。
- indices.requests.cache.size : 默认
-
FieldDataCache:
支持调用API进行清理。
实现类
org.elasticsearch.indices.fielddata.cache.IndicesFieldDataCache
字段数据缓存主要用于对字段进行排序 或计算聚合。它将所有字段值加载到内存中,以便基于文档快速访问这些值。为一个字段建立字段数据缓存的成本可能很高,因此建议有足够的内存来分配它,并保持它处于加载状态。
- indices.fielddata.cache.size:默认无界,属于静态配置。
-
SegmentsCache:
不支持调用API进行清理。
Lucene的segment需要加载到JVM的内存,从ES7(Lucene)已经换成堆外内存的方式实现。
-
indexing-buffer:
索引缓冲区用于存储新索引的文档。当缓冲区满时,缓冲区中的文档就会被写入磁盘上的一个区段。节点上的所有分片共享这个缓冲区。
以下设置是静态的,必须在群集中的每个数据节点上配置:
- indices.memory.index_buffer_size:可以配置成百分比或字节大小值的形式。默认值为 10%,这意味着分配给节点的堆总量的 10%将用作所有分片共享的索引缓冲区大小。
- indices.memory.min_index_buffer_size :当
index_buffer_size
指定为百分比,则可以使用此设置指定绝对最小值。默认值为48MB
。 - indices.memory.max_index_buffer_size :如果
index_buffer_size
指定为百分比,则可以使用此设置指定绝对最大值。默认值为无限制。
相关常用API
清除缓存API
shell
# 按照索引粒度清除缓存
POST /twitter/_cache/clear
POST /kimchy,elasticsearch/_cache/clear
# 清除所有索引的缓存
POST /_cache/clear
查询节点的部分内存使用情况和缓存命中情况。
shell
GET _cat/nodes?h=name,*heap*,*memory*,*Cache*&format=json
查询某个索引部分内存占用情况
shell
GET _cat/indices/twitter?h=*memory*&format=json
GET _cat/indices/?s=segmentsMemory:desc&v&h=index,segmentsCount,segmentsMemory,memoryTotal,mergesCurrent,mergesCurrentDocs,storeSize
GET _cat/segments/twitter?v
查询节点熔断器情况
shell
GET _nodes/stats/breaker
遇到的问题
在业务使用了别名或跨索引查询时,实际查询的索引数量过大,查询高峰期时,父熔断器没能敏捷的触发熔断限流,导致节点JVM出现OOM,最终节点挂掉不可用。
解决方式
- 可以适当调整熔断器的熔断值,减少熔断触发比例。版本7.X中
indices.breaker.request.limit
默认值为60%
,indices.breaker.fielddata.limit
的默认值为40%
- 或者将ES升级到7,。
为了增加父熔断器的触发灵敏度,ES7重新实现了触发校验,使用JDK自带的MemoryMXBean,几乎实时获取到内存的实际使用量,来进行校验判断
java
MemoryMXBean MEMORY_MX_BEAN = ManagementFactory.getMemoryMXBean();
return MEMORY_MX_BEAN.getHeapMemoryUsage().getUsed();
源码部分
核心部分类图
Aggregation Aggregation <<abstract>> CircuitBreakerService void registerBreaker(BreakerSettings breakerSettings) CircuitBreaker getBreaker(String name) AllCircuitBreakerStats stats() CircuitBreakerStats stats(String name) <<interface>> CircuitBreaker String PARENT = "parent" String FIELDDATA = "fielddata" String REQUEST = "request" String IN_FLIGHT_REQUESTS = "in_flight_requests" String ACCOUNTING = "accounting" void circuitBreak(String fieldName, long bytesNeeded) double addEstimateBytesAndMaybeBreak(long bytes, String label) long addWithoutBreaking(long bytes) long getUsed() long getLimit() double getOverhead() long getTrippedCount() String getName() <<Enumeration>> Type MEMORY PARENT NOOP BreakerSettings -String name -long limitBytes -double overhead -CircuitBreaker.Type type ChildMemoryCircuitBreaker BreakerSettings settings HierarchyCircuitBreakerService parent String name HierarchyCircuitBreakerService CircuitBreakerStats -String name -long limit -long estimated -long trippedCount -double overhead AllCircuitBreakerStats -CircuitBreakerStats[] allStats Node NodeStats s NoopCircuitBreaker NoneCircuitBreakerService IndicesService ActionModule NetworkModule SearchService NodeService RestController NodeStats
在节点启动类Node中,会创建一个熔断器服务(CircuitBreakerService,默认是HierarchyCircuitBreakerService实现),然后会注册相关的服务中。来实现对应的熔断服务。
熔断触发方法
在熔断器接口(CircuitBreaker)中声明了五种熔断器的名称。并且定义了触发熔断方法和增加断路器的字节数验证熔断方法。子级内存熔断器(ChildMemoryCircuitBreaker)中实现了以上的方法
java
/**
* 触发熔断
* @param fieldName 触发熔断的熔断器名称
* @param bytesNeeded 熔断时,需要的字节数
*/
public void circuitBreak(String fieldName, long bytesNeeded){
this.trippedCount.incrementAndGet();
final String message = "[" + this.name + "] Data too large, data for [" + fieldName + "]" +
" would be [" + bytesNeeded + "/" + new ByteSizeValue(bytesNeeded) + "]" +
", which is larger than the limit of [" +
memoryBytesLimit + "/" + new ByteSizeValue(memoryBytesLimit) + "]";
logger.debug("{}", message);
throw new CircuitBreakingException(message, bytesNeeded, memoryBytesLimit);
}
/**
* 增加断路器的字节数,并验证熔断
* @param bytes 增加的内存大小
* @param label 增加的内存label性表述
* @return 熔断器中已经使用的内存数
*/
double addEstimateBytesAndMaybeBreak(long bytes, String label) throws CircuitBreakingException{
// short-circuit on no data allowed, immediately throwing an exception
if (memoryBytesLimit == 0) {
circuitBreak(label, bytes);
}
long newUsed;
// -1代表没有限制
if (this.memoryBytesLimit == -1) {
newUsed = noLimit(bytes, label);
} else {
newUsed = limit(bytes, label);
}
// 需要去调用父熔断器进行校验
try {
parent.checkParentLimit(label);
} catch (CircuitBreakingException e) {
this.addWithoutBreaking(-bytes);
throw e;
}
return newUsed;
}
HierarchyCircuitBreakerService 分层熔断器服务类中实现了父熔断器的校验。
java
public void checkParentLimit(String label) throws CircuitBreakingException {
long totalUsed = 0;
// 汇总Node中所有其他内存熔断器以及使用的内存量
for (CircuitBreaker breaker : this.breakers.values()) {
totalUsed += (breaker.getUsed() * breaker.getOverhead());
}
long parentLimit = this.parentSettings.getLimit();
if (totalUsed > parentLimit) {
this.parentTripCount.incrementAndGet();
final StringBuilder message = new StringBuilder("[parent] Data too large, data for [" + label + "]" +
" would be [" + totalUsed + "/" + new ByteSizeValue(totalUsed) + "]" +
", which is larger than the limit of [" +
parentLimit + "/" + new ByteSizeValue(parentLimit) + "]");
message.append(", usages [");
message.append(String.join(", ",
this.breakers.entrySet().stream().map(e -> {
final CircuitBreaker breaker = e.getValue();
final long breakerUsed = (long)(breaker.getUsed() * breaker.getOverhead());
return e.getKey() + "=" + breakerUsed + "/" + new ByteSizeValue(breakerUsed);
})
.collect(Collectors.toList())));
message.append("]");
throw new CircuitBreakingException(message.toString(), totalUsed, parentLimit);
}
}
熔断信息输出
通过以上熔断异常,可以知道熔断的信息格式为
shell
[熔断器名称] Data too large, data for [最后触发熔断的内存标签] would be [已使用的内存大小] which is larger than the limit of [限制的内存大小]
ES6中的label
<reused_arrays>
:agg 等申请的 BigArrays 过大<http_request>
:http 请求过长<agg [" + name + "]>
:聚合使用的内存过大allocated_buckets
: 桶太多<transport_request>
:transport请求过长- ...
彩蛋
ES节点启动异常failed to read [id:58, legacy:false, file:/data01/common/nodes/0/indices/wuMpWBvvRGaqXgwOSV8c6A/_state/state-58.st]
详细集群日志
shell
2024-07-18T11:07:57,298 ERROR [main] org.elasticsearch.gateway.GatewayMetaState:<init>:107 - failed to read local state, exiting...
org.elasticsearch.ElasticsearchException: java.io.IOException: failed to read [id:58, legacy:false, file:/data01/common/nodes/0/indices/wuMpWBvvRGaqXgwOSV8c6A/_state/state-58.st]
at org.elasticsearch.ExceptionsHelper.maybeThrowRuntimeAndSuppress(ExceptionsHelper.java:150) ~[elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.gateway.MetaDataStateFormat.loadLatestState(MetaDataStateFormat.java:334) ~[elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.util.IndexFolderUpgrader.upgrade(IndexFolderUpgrader.java:90) ~[elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.util.IndexFolderUpgrader.upgradeIndicesIfNeeded(IndexFolderUpgrader.java:128) ~[elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.gateway.GatewayMetaState.<init>(GatewayMetaState.java:87) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[?:?]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) [?:1.8.0_231]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) [?:1.8.0_231]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) [?:1.8.0_231]
at org.elasticsearch.common.inject.DefaultConstructionProxyFactory$1.newInstance(DefaultConstructionProxyFactory.java:49) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.ConstructorInjector.construct(ConstructorInjector.java:86) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.ConstructorBindingImpl$Factory.get(ConstructorBindingImpl.java:116) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.ProviderToInternalFactoryAdapter$1.call(ProviderToInternalFactoryAdapter.java:47) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorImpl.callInContext(InjectorImpl.java:825) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:43) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.Scopes$1$1.get(Scopes.java:59) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:50) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.SingleParameterInjector.inject(SingleParameterInjector.java:42) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.SingleParameterInjector.getAll(SingleParameterInjector.java:66) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.ConstructorInjector.construct(ConstructorInjector.java:85) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.ConstructorBindingImpl$Factory.get(ConstructorBindingImpl.java:116) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.ProviderToInternalFactoryAdapter$1.call(ProviderToInternalFactoryAdapter.java:47) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorImpl.callInContext(InjectorImpl.java:825) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:43) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.Scopes$1$1.get(Scopes.java:59) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:50) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorBuilder$1.call(InjectorBuilder.java:191) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorBuilder$1.call(InjectorBuilder.java:183) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorImpl.callInContext(InjectorImpl.java:818) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorBuilder.loadEagerSingletons(InjectorBuilder.java:183) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorBuilder.loadEagerSingletons(InjectorBuilder.java:173) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorBuilder.injectDynamically(InjectorBuilder.java:161) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorBuilder.injectDynamically(InjectorBuilder.java:161) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.InjectorBuilder.build(InjectorBuilder.java:96) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.Guice.createInjector(Guice.java:96) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.Guice.createInjector(Guice.java:70) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.common.inject.ModulesBuilder.createInjector(ModulesBuilder.java:43) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.node.Node.<init>(Node.java:495) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.node.Node.<init>(Node.java:243) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.bootstrap.Bootstrap$5.<init>(Bootstrap.java:232) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:232) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:350) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:123) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:114) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:67) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:122) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.cli.Command.main(Command.java:88) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:91) [elasticsearch-5.4.2.4.jar:5.4.2.4]
at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:84) [elasticsearch-5.4.2.4.jar:5.4.2.4]
Caused by: java.io.IOException: failed to read [id:58, legacy:false, file:/data01/common/nodes/0/indices/wuMpWBvvRGaqXgwOSV8c6A/_state/state-58.st]
at org.elasticsearch.gateway.MetaDataStateFormat.loadLatestState(MetaDataStateFormat.java:327) ~[elasticsearch-5.4.2.4.jar:5.4.2.4]
... 46 more
说明索引的状态文件(.st)文件发生损坏。
shell
find /data00/common/nodes/0/indices/*/_state | grep state | grep ".st" | xargs ls -l | awk '{if($8=="16:32")print $0}' | awk '{print $9}'| xargs rm -rf
shell
/data02/common/nodes/0/indices/QaeXnJ8nQp2tkunaQSnThA/_state/
find /data02/common/nodes/0/indices/ | grep state | grep "\.st" | xargs ls -l | awk '{if($5==0)print $0}' | awk ' {print $9}' | xargs rm -rf