【ES实战】Elasticsearch的CircuitBreaker

Elasticsearch的CircuitBreaker

本文主要以6.7版本为参考

Elasticsearch 具有多个熔断器用于防止各种请求操作造成 OOM , 每个熔断器限制了它可以使用的内存。 此外, Elasticsearch 还有一个父熔断器用于限制所有熔断器的内存总量。

支持在集群上执行API来更新部分熔断器的配置。

熔断器种类

内存熔断器类

  1. parent circuit breaker

    父熔断器,会根据所有子熔断器计算结果觉定是否触发。

    动态配置:

    • indices.breaker.total.limit:JVM的内存比例,默认值70%
  2. fielddata circuit breaker

    字段数据熔断器允许 Elasticsearch 估计一个字段加载到 fielddata cache 需要的内存量, 如果加载该字段会导致超过限制, 则熔断器将停止载入并返回 error。

    动态配置:

    • indices.breaker.fielddata.limit:JVM的内存比例,默认值60%
    • indices.breaker.fielddata.overhead:估算因子,在计算内存是需要与估算因子相乘得到内存估算值。默认1.03
  3. request circuit breaker

    请求熔断器使 Elasticsearch 可以防止每个请求的数据结构(例如, 用于在请求期间计算聚合的内存) 超过一定数量的内存 。

    动态配置:

    • indices.breaker.request.limit:JVM的内存比例,默认值60%
    • indices.breaker.request.overhead:估算因子,在计算内存是需要与估算因子相乘得到内存估算值。默认1
  4. in flight request circuit breaker

    进行中的请求熔路器使 ES 可以限制 transport 或 HTTP 级别上所有当前活动的即将传入请求的内存使用,使其不超过节点上的一定内存量。 内存使用情况取决于请求本身的内容长度。 该断路器还认为, 不仅需要内存来表示原始请求, 而且还需要将其作为结构化对象, 这由默认开销 反映出来。

    动态配置:

    • network.breaker.inflight_requests.limit:JVM的内存比例,默认值100%
    • network.breaker.inflight_requests.overhead:估算因子,在计算内存是需要与估算因子相乘得到内存估算值。默认1
  5. accounting circuit breaker

    计费断路器允许限制请求完成后未释放的内存中所保存内容的内存使用量。 这包括 Lucene 段内存之类的东西, 例如 Segment Memory。

    动态配置:

    • indices.breaker.accounting.limit:JVM的内存比例,默认值100%
    • indices.breaker.accounting.overhead:估算因子,在计算内存是需要与估算因子相乘得到内存估算值。默认1

其他

  1. 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中。
  • RequestCache

    支持调用API进行清理。

    实现类org.elasticsearch.indices.IndicesRequestCache

    分片级请求缓存模块在每个分片上缓存本地结果。这使得频繁使用的搜索请求(可能很繁重)几乎能立即返回结果。请求缓存非常适合日志场景,在日志场景中,只有最新的索引会被主动更新,而较早索引的结果将直接从缓存中提供。

    以下设置是静态的,必须在群集中的每个节点elasticsearch.yml上配置:

    • indices.requests.cache.size: 默认1%

    以下设置是索引分片级别

    • index.requests.cache.enable:开启true,关闭false,创建索引时配置settings中。
  • 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

# 按照索引粒度清除缓存
POST /twitter/_cache/clear
POST /kimchy,elasticsearch/_cache/clear
# 清除所有索引的缓存
POST /_cache/clear

查询节点的部分内存使用情况和缓存命中情况。

GET _cat/nodes?h=name,*heap*,*memory*,*Cache*&format=json

查询某个索引部分内存占用情况

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

查询节点熔断器情况

GET _nodes/stats/breaker

遇到的问题

在业务使用了别名或跨索引查询时,实际查询的索引数量过大,查询高峰期时,父熔断器没能敏捷的触发熔断限流,导致节点JVM出现OOM,最终节点挂掉不可用。

解决方式

  1. 可以适当调整熔断器的熔断值,减少熔断触发比例。版本7.X中indices.breaker.request.limit默认值为60%indices.breaker.fielddata.limit的默认值为40%
  2. 或者将ES升级到7,。

为了增加父熔断器的触发灵敏度,ES7重新实现了触发校验,使用JDK自带的MemoryMXBean,几乎实时获取到内存的实际使用量,来进行校验判断

	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)中实现了以上的方法

    /**
     * 触发熔断
     * @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 分层熔断器服务类中实现了父熔断器的校验。

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);
    }
}

熔断信息输出

通过以上熔断异常,可以知道熔断的信息格式为

[熔断器名称] 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]

详细集群日志

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)文件发生损坏。

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
/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
  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

顧棟

若对你有帮助,望对作者鼓励一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值