New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhance real memory circuit breaker with G1 GC #58674
Changes from 1 commit
05ec98a
b6b565a
c8ac1af
1d007ca
66d642d
3eacf32
addeede
110925d
cfde2a7
fea2ddc
845dc10
3f61f93
55a1774
ed460f9
7d148d0
263f474
79bd399
facd321
14e574c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,9 @@ | |
import org.elasticsearch.common.settings.Setting; | ||
import org.elasticsearch.common.settings.Setting.Property; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.common.unit.ByteSizeUnit; | ||
import org.elasticsearch.common.unit.ByteSizeValue; | ||
import org.elasticsearch.monitor.jvm.JvmInfo; | ||
|
||
import java.lang.management.ManagementFactory; | ||
import java.lang.management.MemoryMXBean; | ||
|
@@ -40,6 +42,7 @@ | |
import java.util.List; | ||
import java.util.Map; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
import java.util.function.LongSupplier; | ||
import java.util.stream.Collectors; | ||
|
||
import static org.elasticsearch.indices.breaker.BreakerSettings.CIRCUIT_BREAKER_LIMIT_SETTING; | ||
|
@@ -104,7 +107,16 @@ public class HierarchyCircuitBreakerService extends CircuitBreakerService { | |
// Tripped count for when redistribution was attempted but wasn't successful | ||
private final AtomicLong parentTripCount = new AtomicLong(0); | ||
|
||
private final DoubleCheckStrategy doubleCheckStrategy; | ||
|
||
public HierarchyCircuitBreakerService(Settings settings, List<BreakerSettings> customBreakers, ClusterSettings clusterSettings) { | ||
this(settings, customBreakers, clusterSettings, | ||
// hardcode interval, do not want any tuning of it outside code changes. | ||
createDoubleCheckStrategy(JvmInfo.jvmInfo(), HierarchyCircuitBreakerService::realMemoryUsage, System::currentTimeMillis, 5000)); | ||
} | ||
|
||
HierarchyCircuitBreakerService(Settings settings, List<BreakerSettings> customBreakers, ClusterSettings clusterSettings, | ||
DoubleCheckStrategy doubleCheckStrategy) { | ||
super(); | ||
HashMap<String, CircuitBreaker> childCircuitBreakers = new HashMap<>(); | ||
childCircuitBreakers.put(CircuitBreaker.FIELDDATA, validateAndCreateBreaker( | ||
|
@@ -168,6 +180,8 @@ public HierarchyCircuitBreakerService(Settings settings, List<BreakerSettings> c | |
CIRCUIT_BREAKER_OVERHEAD_SETTING, | ||
(name, updatedValues) -> updateCircuitBreakerSettings(name, updatedValues.v1(), updatedValues.v2()), | ||
(s, t) -> {}); | ||
|
||
this.doubleCheckStrategy = doubleCheckStrategy; | ||
} | ||
|
||
private void updateCircuitBreakerSettings(String name, ByteSizeValue newLimit, Double newOverhead) { | ||
|
@@ -231,7 +245,7 @@ public CircuitBreakerStats stats(String name) { | |
breaker.getTrippedCount()); | ||
} | ||
|
||
private static class MemoryUsage { | ||
static class MemoryUsage { | ||
final long baseUsage; | ||
final long totalUsage; | ||
final long transientChildUsage; | ||
|
@@ -268,6 +282,10 @@ private MemoryUsage memoryUsed(long newBytesReserved) { | |
|
||
//package private to allow overriding it in tests | ||
long currentMemoryUsage() { | ||
return realMemoryUsage(); | ||
} | ||
|
||
static long realMemoryUsage() { | ||
try { | ||
return MEMORY_MX_BEAN.getHeapMemoryUsage().getUsed(); | ||
} catch (IllegalArgumentException ex) { | ||
|
@@ -290,7 +308,7 @@ public long getParentLimit() { | |
public void checkParentLimit(long newBytesReserved, String label) throws CircuitBreakingException { | ||
final MemoryUsage memoryUsed = memoryUsed(newBytesReserved); | ||
long parentLimit = this.parentSettings.getLimit(); | ||
if (memoryUsed.totalUsage > parentLimit) { | ||
if (memoryUsed.totalUsage > parentLimit && doubleCheckMemoryUsed(memoryUsed).totalUsage > parentLimit) { | ||
this.parentTripCount.incrementAndGet(); | ||
final StringBuilder message = new StringBuilder("[parent] Data too large, data for [" + label + "]" + | ||
" would be [" + memoryUsed.totalUsage + "/" + new ByteSizeValue(memoryUsed.totalUsage) + "]" + | ||
|
@@ -324,6 +342,14 @@ public void checkParentLimit(long newBytesReserved, String label) throws Circuit | |
} | ||
} | ||
|
||
MemoryUsage doubleCheckMemoryUsed(MemoryUsage memoryUsed) { | ||
if (this.trackRealMemoryUsage) { | ||
return doubleCheckStrategy.doubleCheck(memoryUsed); | ||
} else { | ||
return memoryUsed; | ||
} | ||
} | ||
|
||
private CircuitBreaker validateAndCreateBreaker(BreakerSettings breakerSettings) { | ||
// Validate the settings | ||
validateSettings(new BreakerSettings[] {breakerSettings}); | ||
|
@@ -334,4 +360,113 @@ private CircuitBreaker validateAndCreateBreaker(BreakerSettings breakerSettings) | |
this, | ||
breakerSettings.getName()); | ||
} | ||
|
||
private static DoubleCheckStrategy createDoubleCheckStrategy(JvmInfo jvmInfo, LongSupplier currentMemoryUsageSupplier, | ||
LongSupplier timeSupplier, long minimumInterval) { | ||
if (jvmInfo.useG1GC().equals("true") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In line with an earlier comment, we could pass in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also part of 3f61f93 |
||
// messing with GC is "dangerous" so we apply an escape hatch. Not intended to be used. | ||
&& Boolean.parseBoolean(System.getProperty("es.real_memory_circuit_breaker.g1.double_check.enabled", "true"))) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mind using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, fixed in 845dc10 |
||
return new G1DoubleCheckStrategy(jvmInfo, currentMemoryUsageSupplier, timeSupplier, minimumInterval); | ||
} else { | ||
return memoryUsed -> memoryUsed; | ||
} | ||
} | ||
|
||
interface DoubleCheckStrategy { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe name it OverLimitStrategy? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ++, see 1d007ca |
||
MemoryUsage doubleCheck(MemoryUsage memoryUsed); | ||
} | ||
|
||
static class G1DoubleCheckStrategy implements DoubleCheckStrategy { | ||
private final long g1RegionSize; | ||
private final LongSupplier currentMemoryUsageSupplier; | ||
private final LongSupplier timeSupplier; | ||
|
||
private long lastCheckTime = Long.MIN_VALUE; | ||
private final long minimumInterval; | ||
|
||
private long blackHole; | ||
private final Object lock = new Object(); | ||
|
||
G1DoubleCheckStrategy(JvmInfo jvmInfo, LongSupplier currentMemoryUsageSupplier, | ||
LongSupplier timeSupplier, long minimumInterval) { | ||
assert minimumInterval > 0; | ||
this.currentMemoryUsageSupplier = currentMemoryUsageSupplier; | ||
this.timeSupplier = timeSupplier; | ||
this.minimumInterval = minimumInterval; | ||
long g1RegionSize = jvmInfo.getG1RegionSize(); | ||
if (g1RegionSize <= 0) { | ||
|
||
this.g1RegionSize = fallbackRegionSize(jvmInfo); | ||
} else { | ||
this.g1RegionSize = g1RegionSize; | ||
} | ||
} | ||
|
||
static long fallbackRegionSize(JvmInfo jvmInfo) { | ||
// mimick JDK calculation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add a link or reference to this calculation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in c8ac1af |
||
long averageHeapSize = | ||
(jvmInfo.getMem().getHeapMax().getBytes() + JvmInfo.jvmInfo().getMem().getHeapMax().getBytes()) / 2; | ||
long regionSize = Long.highestOneBit(averageHeapSize / 2048); | ||
if (regionSize < ByteSizeUnit.MB.toBytes(1)) { | ||
regionSize = ByteSizeUnit.MB.toBytes(1); | ||
} else if (regionSize > ByteSizeUnit.MB.toBytes(32)) { | ||
regionSize = ByteSizeUnit.MB.toBytes(32); | ||
} | ||
return regionSize; | ||
} | ||
|
||
@Override | ||
public MemoryUsage doubleCheck(MemoryUsage memoryUsed) { | ||
long maxHeap = JvmInfo.jvmInfo().getMem().getHeapMax().getBytes(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this appears to be a consistent value, maybe we just keep it as a final long that is a class member. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 3eacf32 |
||
boolean leader; | ||
synchronized (lock) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the locality of the usage, not really, it is just a personal preference since it avoids thinking about external synchronization on this. I am OK turning it into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with it; just curious. |
||
leader = timeSupplier.getAsLong() >= lastCheckTime + minimumInterval; | ||
doubleCheckingRealMemoryUsed(leader); | ||
if (leader) { | ||
logger.info("attempting to trigger G1GC due to high heap usage [{}]", memoryUsed.baseUsage); | ||
long localBlackHole = 0; | ||
// number of allocations, corresponding to (approximately) number of free regions + 1 | ||
long allocationCount = (maxHeap - memoryUsed.baseUsage) / g1RegionSize + 1; | ||
// allocations of half-region size becomes single humongous alloc, thus taking up a full region. | ||
int allocationSize = (int) (g1RegionSize >> 1); | ||
long maxUsageObserved = memoryUsed.baseUsage; | ||
for (long i = 0; i < allocationCount; ++i) { | ||
long current = currentMemoryUsageSupplier.getAsLong(); | ||
if (current >= maxUsageObserved) { | ||
maxUsageObserved = current; | ||
} else { | ||
// we observed a memory drop, so some GC must have occurred | ||
break; | ||
} | ||
localBlackHole += new byte[allocationSize].hashCode(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it possible for this to trigger an OOM? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes and no. In theory yes, if there is really no collectible heap left. But if that was the case, just creating the Notice that we only need 1 region of space free or collectible space for this to succeed. |
||
} | ||
|
||
blackHole += localBlackHole; | ||
logger.trace("black hole [{}]", blackHole); | ||
long now = timeSupplier.getAsLong(); | ||
assert now > this.lastCheckTime; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unfortunately neither There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, removed in b6b565a |
||
this.lastCheckTime = now; | ||
} | ||
} | ||
|
||
final long current = currentMemoryUsageSupplier.getAsLong(); | ||
if (current < memoryUsed.baseUsage) { | ||
if (leader) { | ||
logger.info("GC did bring memory usage down, before [{}], after [{}]", memoryUsed.baseUsage, current); | ||
} | ||
return new MemoryUsage(current, memoryUsed.totalUsage - memoryUsed.baseUsage + current, | ||
memoryUsed.transientChildUsage, memoryUsed.permanentChildUsage); | ||
} else { | ||
if (leader) { | ||
logger.info("GC did not bring memory usage down, before [{}], after [{}]", memoryUsed.baseUsage, current); | ||
} | ||
// prefer original measurement when reporting if heap usage was not brought down. | ||
return memoryUsed; | ||
} | ||
} | ||
|
||
void doubleCheckingRealMemoryUsed(boolean leader) { | ||
// for tests to override. | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the method call can be replaced with
overLimitStrategy.apply(memoryUsed)
and the construction of the OverLimitStrategy will be responsible for handling the behavior of what to do?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, done in 3f61f93