elasticsearch的熔断机制与熔断场景

2023-11-09 15:12:06 浏览数 (3)

一.什么是elasticsearch的熔断

circuit breakers(熔断器)是elasticsearch对于自身防止资源被过度消耗的一种保护机制。主要是为了防止业务elasticsearch时,资源被过度消耗,引起JVM的OutOfMemoryError。防止elasticsearch服务的JVM堆内存负载过高而导致服务不可用。通过熔断器的参数阈值约束,elasticsearch集群在响应客户端请求时当超过预设阈值后就会停止接受新的请求,并返回响应的错误信息。保护集群的稳定性。为此elasticsearch提供了多种熔断器。

二.elasticsearch熔断器的分类

Parent circuit breaker(父级熔断器)

父级熔断器:作为elasticsearch集群断路器中级别最高的熔断器。用于监控并管理整个集群的JVM堆内存使用情况。当父熔断出发后,集群将停止接受新的客户端请求,并返回熔断异常。有助于防止集群资源耗尽,请求堆积与性能下降。提高集群稳定性。

支持的参数:

代码语言:javascript复制
#父熔断器最大允许使用的堆内存上限额度。默认值为JVM堆内存空间的70%。可以根据集群实际情况进行动态调整。
indices.breaker.total.limit: "70%"
#父熔断器是否需要为子熔断器保留额度,默认值为true。
indices.breaker.total.use_real_memory: true

Field data circuit breaker(字段数据熔断器)

字段数据熔断器:用于评估将字段数据加载至字段内存缓存区所需要占用JVM堆内存额度的熔断器。是子熔断器的一种。主要监控字段数据缓存所消耗的堆内存资源。当达到过超过预设阈值时返回熔断错误,并停止缓存操作。

字段数据缓存是elasticsearch用于对聚合排序等操作进行加速的一种机制。将字段数据加载至内存中以便快速进行访问。

支持的参数:

代码语言:javascript复制
#字段数据熔断器能够使用的堆内存上限额度。默认值为JVM堆内存空间的40%。可以根据集群实际情况进行动态调整。
indices.breaker.fielddata.limit:"40%"
#定义字段数据缓存的内存开销比例。表示给字段数据缓存额外分配堆外内存的额度比例。通俗业务场景中使用默认值即可。
#避免由于比例失衡而家具内存开销。影响字段数据缓存的稳定性和效率。
indices.breaker.fielddata.overhead: 1.03

Request circuit breaker(请求熔断器)

请求熔断器:用于评估每一个客户端请求在请求elasticsearch时所需要使用的内存额度的熔断器,是子熔断器的一种。主要作用是防止单个请求消耗过多的内存资源。一般用于监控基数聚合等请求的内存的分配,以及聚合请求中使用的存储桶数量。当触发预设熔断参数时,就会结束该请求并返回熔断异常信息。

支持的参数:

代码语言:javascript复制
#请求熔断器能够使用的堆内存上限额度。默认值为JVM堆内存空间的60%。可以根据集群实际情况进行动态调整。
indices.breaker.request.limit: "60%"
#定义了请求熔断器的内存开销比例。表示分配给请求熔断器的实际内存之外,额外分配的堆外内存的比例。默认值为1
indices.breaker.request.overhead : 1

In flight requests circuit breaker(请求传输熔断器)

正在处理的请求熔断器:用于控制HTTP级别当前正在传输的请求的内存使用量的熔断器。是子熔断器的一种。避免由于网络传输占用的内存超出节点的特定内存额度限制。

正在处理的请求是指在网络层面上正在传输和处理的请求。这些请求可能包括搜索、索引、删除等操作。在传输和处理请求时,会占用一定的内存资源。

支持的参数:

代码语言:javascript复制
#用于控制HTTP级别当前正在传输的请求能够使用的堆内存额度。默认值为JVM堆内存空间的100%。受父熔断器的额度约束。
network.breaker.inflight_requests.limit: "100%"
#定义了正在处理的请求的内存开销比例,表示在分配给正在处理的请求的实际内存之外,额外分配的堆外内存比例。
#堆外内存用于处理请求的元数据和其他开销。默认值为2
network.breaker.inflight_requests.overhead: 2

Accounting requests circuit breaker(请求计数器熔断器)

请求计数器熔断器:用于记录并控制请求在完成后未释放的内存使用量的熔断器。

支持的参数:

代码语言:javascript复制
#用于控制请求计数器断路器能够使用的堆内存额度。默认值为JVM堆内存空间的100%。受父熔断器的额度约束。
indices.breaker.accounting.limit: "100%"
#定义了请求计数器熔断器器的内存开销比例。表示在分配给请求断路器实际内存之外,额外分配的堆外内存比例。默认值为1
indices.breaker.accounting.overhead: 1

Script compilation circuit breaker(脚本编译熔断器)

脚本编译熔断器:用于控制脚本编译过程中所使用的内存额度。

script在elasticsearch中被广泛用于各种查询聚合以及更新操作。脚本编译是将脚本转换为可执行代码的过程。在转换过程中会消耗一定的CPU和内存资源。

支持的参数:

代码语言:javascript复制
#用于控制特定时间内,允许为客户端请求编译脚本的数量。可以控制脚本编译的速率。默认值为75/5m;
# $CONTEXT 是一个占位符,表示特定的上下文名称,例如"search"或"update"。在实际使用时,需要将 $CONTEXT 替换为相应的上下文名称
script.context.$CONTEXT.max_compilations_rate: "75/5m"

Regex circuit breaker(正则表达式熔断器)

正则表达式熔断器:用于控制正则表达式在集群中的使用类型的熔断器。

正则表达式在elasticsearch中一般用于执行模式匹配或搜索等操作。性能较差的正则表达式会引起集群资源的过度消耗。影响集群稳定性。

支持的参数:

代码语言:javascript复制
#用于控制是否在集群中启用正则脚本。默认值为limited;
script.painless.regex.enabled: "limited"
    -true 启动正则表达式,不受复杂度限制。默认关闭正则表达式熔断器。
    -false 禁用正则表达式,使用任何正则表达式都会返回失败的错误。
    -limited 使用正则表达式,通过script.painless.regex.limit-factor参数设置集群的正则表达式复杂度。
#用于限制正则表达式在脚本中的长度。elasticsearch会通过数据脚本正则的长度来计算该限制。
#例如:"foobarbaz"的字符长度为9,如果"script.painless.regex.limit-factor"倍率是6,那么基于"foobarbaz"的正则表达式最大长度则为54(6*9),如果正则表达式超出这个长度限制,则会触发正则熔断器,返回熔断异常。
script.painless.regex.limit-factor

代码语言:javascript复制
static class MemoryUsage { 
        final long baseUsage; 
        final long totalUsage; 
        final long transientChildUsage; 
        final long permanentChildUsage; 
        MemoryUsage(final long baseUsage, final long totalUsage, final long transientChildUsage, final long permanentChildUsage) { 
            this.baseUsage = baseUsage; 
            this.totalUsage = totalUsage; 
            this.transientChildUsage = transientChildUsage; 
            this.permanentChildUsage = permanentChildUsage; 
        } 
    }

在这段代码中MemoryUsage类用于表示内存使用情况。它包含了几个字段,分别表示基础使用量(baseUsage)、总使用量(totalUsage)、瞬态子级使用量(transientChildUsage)和永久子级使用量(permanentChildUsage)。在breaker统计内存使用量时会调用该静态类,便于实现对内存的管理与监控。

代码语言:javascript复制
HierarchyCircuitBreakerService(
        Settings settings,
        List<BreakerSettings> customBreakers,
        ClusterSettings clusterSettings,
        Function<Boolean, OverLimitStrategy> overLimitStrategyFactory
    ) {
        super();
        HashMap<String, CircuitBreaker> childCircuitBreakers = new HashMap<>();
        childCircuitBreakers.put(
            CircuitBreaker.FIELDDATA,
            validateAndCreateBreaker(
                new BreakerSettings(
                    CircuitBreaker.FIELDDATA,
                    FIELDDATA_CIRCUIT_BREAKER_LIMIT_SETTING.get(settings).getBytes(),
                    FIELDDATA_CIRCUIT_BREAKER_OVERHEAD_SETTING.get(settings),
                    FIELDDATA_CIRCUIT_BREAKER_TYPE_SETTING.get(settings),
                    CircuitBreaker.Durability.PERMANENT
                )
            )
        );
        childCircuitBreakers.put(
            CircuitBreaker.IN_FLIGHT_REQUESTS,
            validateAndCreateBreaker(
                new BreakerSettings(
                    CircuitBreaker.IN_FLIGHT_REQUESTS,
                    IN_FLIGHT_REQUESTS_CIRCUIT_BREAKER_LIMIT_SETTING.get(settings).getBytes(),
                    IN_FLIGHT_REQUESTS_CIRCUIT_BREAKER_OVERHEAD_SETTING.get(settings),
                    IN_FLIGHT_REQUESTS_CIRCUIT_BREAKER_TYPE_SETTING.get(settings),
                    CircuitBreaker.Durability.TRANSIENT
                )
            )
        );
        childCircuitBreakers.put(
            CircuitBreaker.REQUEST,
            validateAndCreateBreaker(
                new BreakerSettings(
                    CircuitBreaker.REQUEST,
                    REQUEST_CIRCUIT_BREAKER_LIMIT_SETTING.get(settings).getBytes(),
                    REQUEST_CIRCUIT_BREAKER_OVERHEAD_SETTING.get(settings),
                    REQUEST_CIRCUIT_BREAKER_TYPE_SETTING.get(settings),
                    CircuitBreaker.Durability.TRANSIENT
                )
            )
        );
        for (BreakerSettings breakerSettings : customBreakers) {
            if (childCircuitBreakers.containsKey(breakerSettings.getName())) {
                throw new IllegalArgumentException(
                    "More than one circuit breaker with the name ["
                          breakerSettings.getName()
                          "] exists. Circuit breaker names must be unique"
                );
            }
            childCircuitBreakers.put(breakerSettings.getName(), validateAndCreateBreaker(breakerSettings));
        }
        this.breakers = Map.copyOf(childCircuitBreakers);
        this.parentSettings = new BreakerSettings(
            CircuitBreaker.PARENT,
            TOTAL_CIRCUIT_BREAKER_LIMIT_SETTING.get(settings).getBytes(),
            1.0,
            CircuitBreaker.Type.PARENT,
            null
        );
        logger.trace(() -> format("parent circuit breaker with settings %s", this.parentSettings));

        this.trackRealMemoryUsage = USE_REAL_MEMORY_USAGE_SETTING.get(settings);

        clusterSettings.addSettingsUpdateConsumer(
            TOTAL_CIRCUIT_BREAKER_LIMIT_SETTING,
            this::setTotalCircuitBreakerLimit,
            HierarchyCircuitBreakerService::validateTotalCircuitBreakerLimit
        );
        clusterSettings.addSettingsUpdateConsumer(
            FIELDDATA_CIRCUIT_BREAKER_LIMIT_SETTING,
            FIELDDATA_CIRCUIT_BREAKER_OVERHEAD_SETTING,
            (limit, overhead) -> updateCircuitBreakerSettings(CircuitBreaker.FIELDDATA, limit, overhead)
        );
        clusterSettings.addSettingsUpdateConsumer(
            IN_FLIGHT_REQUESTS_CIRCUIT_BREAKER_LIMIT_SETTING,
            IN_FLIGHT_REQUESTS_CIRCUIT_BREAKER_OVERHEAD_SETTING,
            (limit, overhead) -> updateCircuitBreakerSettings(CircuitBreaker.IN_FLIGHT_REQUESTS, limit, overhead)
        );
        clusterSettings.addSettingsUpdateConsumer(
            REQUEST_CIRCUIT_BREAKER_LIMIT_SETTING,
            REQUEST_CIRCUIT_BREAKER_OVERHEAD_SETTING,
            (limit, overhead) -> updateCircuitBreakerSettings(CircuitBreaker.REQUEST, limit, overhead)
        );
        clusterSettings.addAffixUpdateConsumer(
            CIRCUIT_BREAKER_LIMIT_SETTING,
            CIRCUIT_BREAKER_OVERHEAD_SETTING,
            (name, updatedValues) -> updateCircuitBreakerSettings(name, updatedValues.v1(), updatedValues.v2()),
            (s, t) -> {}
        );
        clusterSettings.addSettingsUpdateConsumer(USE_REAL_MEMORY_USAGE_SETTING, this::updateUseRealMemorySetting);

        this.overLimitStrategyFactory = overLimitStrategyFactory;
        this.overLimitStrategy = overLimitStrategyFactory.apply(this.trackRealMemoryUsage);
    }

HierarchyCircuitBreakerService类继承自CircuitBreakerService类,用于管理和控制熔断器。在这个类中该构造函数中定义了父熔断器与各个子熔断器。用于初始化熔断器对象。

三.熔断场景分析

1.fielddata字段数据聚合请求过多,超出熔断器阈值限制。

在集群触发熔断后通常我们在elasticsearch集群日志或客户端API返回的异常信息中回看到以下日志信息:

代码语言:javascript复制
error:elastic: Error 503 (Service Unavailable): 
[parent] Data too large, data for [<http_request>] would be [34212596438/31.8gb],
 which is larger than the limit of [30804738048/28.6gb], 
 real usage: [34211774560/31.8gb], 
new bytes reserved: [821878/802.6kb] [type=circuit_breaking_exception]

分析思路:

我们通过以下GET _cluster/settings获取集群熔断参数,先对参数进行排查。分析参数是否配置合理。

在当前场景中,我们通过熔断日志上下文结合监控信息分析。发现用户的查询请求在聚合大量的fielddata类型的字段,导致频繁触发fielddata熔断器,最终导致触发parent breaker。结合集群堆内存使用情况,最终调整了fielddata熔断器阈值。恢复了正常的业务查询请求响应。

2.索引大量写入数据导致集群出发parent breaker

分析思路:通过监控先对写入量较大的索引进行排查。找到相关索引后,分析索引参数是否合理。分片设计是否规范。可以先通过临时搬迁分片来分散写入压力。在业务端压力无法降低的情况下,通过扩容集群节点,并且加速分片搬迁,来尽快确保业务恢复。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞