一.什么是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腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!