You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by ck...@apache.org on 2019/06/18 01:25:50 UTC
[logging-log4j2] 01/02: LOG4J2-2606: Substantially improve async
logging performance under heavy load
This is an automated email from the ASF dual-hosted git repository.
ckozak pushed a commit to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit e3937fec5880b5f6b3d3173c819d47a1c1ad2c86
Author: Carter Kozak <ck...@apache.org>
AuthorDate: Tue May 21 18:39:13 2019 -0400
LOG4J2-2606: Substantially improve async logging performance under heavy load
This change applies synchronization to AsyncLoggerDisruptor blocking enqueue
operations when the asynchronous logging queue is completely full.
This new behavior may be disabled using the boolean property
`AsyncLogger.SynchronizeEnqueueWhenQueueFull` (default true).
Alternatives tested:
* All available lmax disruptor `WaitStrategy` implementations.
None of the available implementations provided a substantial
difference in throughput or CPU utilization. In all cases
my processor was stuck at or near 100% across all six cores.
* Based on feedback from https://github.com/LMAX-Exchange/disruptor/issues/266
I attempted avoiding the blocking enqueue entirely in favor of
`LockSupport.parkNanos` with values ranging from 1,000 through
1,000,000. 1,000,000 provided the most throughput among parkNanos
values tested, but is too long for most scenarios.
* Semaphore instead of synchronized. Testing with permits
equivalent to the available processor count yielded higher
CPU utilization and lower throughput than a semaphore
with a single permit. Given that most work occurs on
the (single) background thread, there's not much reason
to allow multiple enqueues simultaneously.
Updated benchmarks adding "ENQUEUE_UNSYNCHRONIZED" following
the old unsynchronized path, where "ENQUEUE" shows an improvement:
Benchmark (asyncLoggerType) (queueFullPolicy) Mode Cnt Score Error Units
ConcurrentAsyncLoggerToFileBenchmark.concurrentLoggingThreads ASYNC_CONTEXT ENQUEUE thrpt 3 1258165.831 ± 667148.832 ops/s
ConcurrentAsyncLoggerToFileBenchmark.concurrentLoggingThreads ASYNC_CONTEXT ENQUEUE_UNSYNCHRONIZED thrpt 3 454371.810 ± 409868.594 ops/s
ConcurrentAsyncLoggerToFileBenchmark.concurrentLoggingThreads ASYNC_CONTEXT SYNCHRONOUS thrpt 3 1053495.203 ± 636177.787 ops/s
ConcurrentAsyncLoggerToFileBenchmark.concurrentLoggingThreads ASYNC_CONFIG ENQUEUE thrpt 3 1276622.582 ± 127625.711 ops/s
ConcurrentAsyncLoggerToFileBenchmark.concurrentLoggingThreads ASYNC_CONFIG ENQUEUE_UNSYNCHRONIZED thrpt 3 611023.061 ± 993497.457 ops/s
ConcurrentAsyncLoggerToFileBenchmark.concurrentLoggingThreads ASYNC_CONFIG SYNCHRONOUS thrpt 3 1174098.357 ± 379968.531 ops/s
ConcurrentAsyncLoggerToFileBenchmark.singleLoggingThread ASYNC_CONTEXT ENQUEUE thrpt 3 1397715.099 ± 292151.880 ops/s
ConcurrentAsyncLoggerToFileBenchmark.singleLoggingThread ASYNC_CONTEXT ENQUEUE_UNSYNCHRONIZED thrpt 3 1351109.640 ± 1013899.613 ops/s
ConcurrentAsyncLoggerToFileBenchmark.singleLoggingThread ASYNC_CONTEXT SYNCHRONOUS thrpt 3 1265741.941 ± 1020413.924 ops/s
ConcurrentAsyncLoggerToFileBenchmark.singleLoggingThread ASYNC_CONFIG ENQUEUE thrpt 3 1501815.413 ± 375077.112 ops/s
ConcurrentAsyncLoggerToFileBenchmark.singleLoggingThread ASYNC_CONFIG ENQUEUE_UNSYNCHRONIZED thrpt 3 1516153.045 ± 874311.252 ops/s
ConcurrentAsyncLoggerToFileBenchmark.singleLoggingThread ASYNC_CONFIG SYNCHRONOUS thrpt 3 1493252.704 ± 947903.342 ops/s
---
.../logging/log4j/core/async/AsyncLogger.java | 4 +-
.../core/async/AsyncLoggerConfigDelegate.java | 5 ++
.../core/async/AsyncLoggerConfigDisruptor.java | 16 +++-
.../log4j/core/async/AsyncLoggerDisruptor.java | 85 +++++++++++++++++++---
.../logging/log4j/core/async/DisruptorUtil.java | 11 +++
.../jmh/ConcurrentAsyncLoggerToFileBenchmark.java | 59 ++++++++++++---
...rentAsyncLoggerToFileBenchmark-asyncConfig.xml} | 6 +-
.../ConcurrentAsyncLoggerToFileBenchmark.xml | 2 +-
src/changes/changes.xml | 3 +
9 files changed, 164 insertions(+), 27 deletions(-)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
index 3bbdc41..7b6d16b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
@@ -178,7 +178,7 @@ public class AsyncLogger extends Logger implements EventTranslatorVararg<RingBuf
final EventRoute eventRoute = loggerDisruptor.getEventRoute(translator.level);
switch (eventRoute) {
case ENQUEUE:
- loggerDisruptor.enqueueLogMessageInfo(translator);
+ loggerDisruptor.enqueueLogMessageWhenQueueFull(translator);
break;
case SYNCHRONOUS:
logMessageInCurrentThread(translator.fqcn, translator.level, translator.marker, translator.message,
@@ -328,7 +328,7 @@ public class AsyncLogger extends Logger implements EventTranslatorVararg<RingBuf
final EventRoute eventRoute = loggerDisruptor.getEventRoute(level);
switch (eventRoute) {
case ENQUEUE:
- loggerDisruptor.getDisruptor().getRingBuffer().publishEvent(this,
+ loggerDisruptor.enqueueLogMessageWhenQueueFull(this,
this, // asyncLogger: 0
location, // location: 1
fqcn, // 2
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDelegate.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDelegate.java
index 0e230bb..5e01f8b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDelegate.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDelegate.java
@@ -46,6 +46,11 @@ public interface AsyncLoggerConfigDelegate {
*/
EventRoute getEventRoute(final Level level);
+ /**
+ * Enqueues the {@link LogEvent} on the mixed configuration ringbuffer.
+ * This method must only be used after {@link #tryEnqueue(LogEvent, AsyncLoggerConfig)} returns <code>false</code>
+ * indicating that the ringbuffer is full, otherwise it may incur unnecessary synchronization.
+ */
void enqueueEvent(LogEvent event, AsyncLoggerConfig asyncLoggerConfig);
boolean tryEnqueue(LogEvent event, AsyncLoggerConfig asyncLoggerConfig);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDisruptor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDisruptor.java
index b29dda3..be34e09 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDisruptor.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDisruptor.java
@@ -186,6 +186,8 @@ public class AsyncLoggerConfigDisruptor extends AbstractLifeCycle implements Asy
private EventTranslatorTwoArg<Log4jEventWrapper, LogEvent, AsyncLoggerConfig> translator;
private volatile boolean alreadyLoggedWarning = false;
+ private final Object queueFullEnqueueLock = new Object();
+
public AsyncLoggerConfigDisruptor() {
}
@@ -369,7 +371,19 @@ public class AsyncLoggerConfigDisruptor extends AbstractLifeCycle implements Asy
}
private void enqueue(final LogEvent logEvent, final AsyncLoggerConfig asyncLoggerConfig) {
- disruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig);
+ if (synchronizeEnqueueWhenQueueFull()) {
+ synchronized (queueFullEnqueueLock) {
+ disruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig);
+ }
+ } else {
+ disruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig);
+ }
+ }
+
+ private boolean synchronizeEnqueueWhenQueueFull() {
+ return DisruptorUtil.ASYNC_CONFIG_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL
+ // Background thread must never block
+ && backgroundThreadId != Thread.currentThread().getId();
}
@Override
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerDisruptor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerDisruptor.java
index 7a7e546..e8c0121 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerDisruptor.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerDisruptor.java
@@ -20,7 +20,9 @@ package org.apache.logging.log4j.core.async;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
+import com.lmax.disruptor.EventTranslatorVararg;
import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.AbstractLifeCycle;
import org.apache.logging.log4j.core.jmx.RingBufferAdmin;
import org.apache.logging.log4j.core.util.Log4jThreadFactory;
@@ -32,6 +34,7 @@ import com.lmax.disruptor.TimeoutException;
import com.lmax.disruptor.WaitStrategy;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
+import org.apache.logging.log4j.message.Message;
/**
* Helper class for async loggers: AsyncLoggerDisruptor handles the mechanics of working with the LMAX Disruptor, and
@@ -43,6 +46,8 @@ class AsyncLoggerDisruptor extends AbstractLifeCycle {
private static final int SLEEP_MILLIS_BETWEEN_DRAIN_ATTEMPTS = 50;
private static final int MAX_DRAIN_ATTEMPTS_BEFORE_SHUTDOWN = 200;
+ private final Object queueFullEnqueueLock = new Object();
+
private volatile Disruptor<RingBufferLogEvent> disruptor;
private String contextName;
@@ -202,32 +207,94 @@ class AsyncLoggerDisruptor extends AbstractLifeCycle {
return false;
}
- public boolean tryPublish(final RingBufferLogEventTranslator translator) {
+ boolean tryPublish(final RingBufferLogEventTranslator translator) {
try {
+ // Note: we deliberately access the volatile disruptor field afresh here.
+ // Avoiding this and using an older reference could result in adding a log event to the disruptor after it
+ // was shut down, which could cause the publishEvent method to hang and never return.
return disruptor.getRingBuffer().tryPublishEvent(translator);
} catch (final NullPointerException npe) {
// LOG4J2-639: catch NPE if disruptor field was set to null in stop()
- LOGGER.warn("[{}] Ignoring log event after log4j was shut down: {} [{}] {}", contextName,
- translator.level, translator.loggerName, translator.message.getFormattedMessage()
- + (translator.thrown == null ? "" : Throwables.toStringList(translator.thrown)));
+ logWarningOnNpeFromDisruptorPublish(translator);
return false;
}
}
- void enqueueLogMessageInfo(final RingBufferLogEventTranslator translator) {
+ void enqueueLogMessageWhenQueueFull(final RingBufferLogEventTranslator translator) {
try {
// Note: we deliberately access the volatile disruptor field afresh here.
// Avoiding this and using an older reference could result in adding a log event to the disruptor after it
// was shut down, which could cause the publishEvent method to hang and never return.
- disruptor.publishEvent(translator);
+ if (synchronizeEnqueueWhenQueueFull()) {
+ synchronized (queueFullEnqueueLock) {
+ disruptor.publishEvent(translator);
+ }
+ } else {
+ disruptor.publishEvent(translator);
+ }
} catch (final NullPointerException npe) {
// LOG4J2-639: catch NPE if disruptor field was set to null in stop()
- LOGGER.warn("[{}] Ignoring log event after log4j was shut down: {} [{}] {}", contextName,
- translator.level, translator.loggerName, translator.message.getFormattedMessage()
- + (translator.thrown == null ? "" : Throwables.toStringList(translator.thrown)));
+ logWarningOnNpeFromDisruptorPublish(translator);
}
}
+ void enqueueLogMessageWhenQueueFull(
+ final EventTranslatorVararg<RingBufferLogEvent> translator,
+ final AsyncLogger asyncLogger,
+ final StackTraceElement location,
+ final String fqcn,
+ final Level level,
+ final Marker marker,
+ final Message msg,
+ final Throwable thrown) {
+ try {
+ // Note: we deliberately access the volatile disruptor field afresh here.
+ // Avoiding this and using an older reference could result in adding a log event to the disruptor after it
+ // was shut down, which could cause the publishEvent method to hang and never return.
+ if (synchronizeEnqueueWhenQueueFull()) {
+ synchronized (queueFullEnqueueLock) {
+ disruptor.getRingBuffer().publishEvent(translator,
+ asyncLogger, // asyncLogger: 0
+ location, // location: 1
+ fqcn, // 2
+ level, // 3
+ marker, // 4
+ msg, // 5
+ thrown); // 6
+ }
+ } else {
+ disruptor.getRingBuffer().publishEvent(translator,
+ asyncLogger, // asyncLogger: 0
+ location, // location: 1
+ fqcn, // 2
+ level, // 3
+ marker, // 4
+ msg, // 5
+ thrown); // 6
+ }
+ } catch (final NullPointerException npe) {
+ // LOG4J2-639: catch NPE if disruptor field was set to null in stop()
+ logWarningOnNpeFromDisruptorPublish(level, fqcn, msg, thrown);
+ }
+ }
+
+ private boolean synchronizeEnqueueWhenQueueFull() {
+ return DisruptorUtil.ASYNC_LOGGER_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL
+ // Background thread must never block
+ && backgroundThreadId != Thread.currentThread().getId();
+ }
+
+ private void logWarningOnNpeFromDisruptorPublish(final RingBufferLogEventTranslator translator) {
+ logWarningOnNpeFromDisruptorPublish(
+ translator.level, translator.loggerName, translator.message, translator.thrown);
+ }
+
+ private void logWarningOnNpeFromDisruptorPublish(
+ final Level level, final String fqcn, final Message msg, final Throwable thrown) {
+ LOGGER.warn("[{}] Ignoring log event after log4j was shut down: {} [{}] {}{}", contextName,
+ level, fqcn, msg.getFormattedMessage(), thrown == null ? "" : Throwables.toStringList(thrown));
+ }
+
/**
* Returns whether it is allowed to store non-JDK classes in ThreadLocal objects for efficiency.
*
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java
index 9e946f9..0299541 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java
@@ -46,6 +46,17 @@ final class DisruptorUtil {
private static final int RINGBUFFER_DEFAULT_SIZE = 256 * 1024;
private static final int RINGBUFFER_NO_GC_DEFAULT_SIZE = 4 * 1024;
+ /**
+ * LOG4J2-2606: Users encountered excessive CPU utilization with Disruptor v3.4.2 when the application
+ * was logging more than the underlying appender could keep up with and the ringbuffer became full,
+ * especially when the number of application threads vastly outnumbered the number of cores.
+ * CPU utilization is significantly reduced by restricting access to the enqueue operation.
+ */
+ static final boolean ASYNC_LOGGER_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL = PropertiesUtil.getProperties()
+ .getBooleanProperty("AsyncLogger.SynchronizeEnqueueWhenQueueFull", true);
+ static final boolean ASYNC_CONFIG_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL = PropertiesUtil.getProperties()
+ .getBooleanProperty("AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull", true);
+
private DisruptorUtil() {
}
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ConcurrentAsyncLoggerToFileBenchmark.java b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ConcurrentAsyncLoggerToFileBenchmark.java
index f765437..6eda1ee 100644
--- a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ConcurrentAsyncLoggerToFileBenchmark.java
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ConcurrentAsyncLoggerToFileBenchmark.java
@@ -39,6 +39,9 @@ import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
@@ -71,17 +74,19 @@ public class ConcurrentAsyncLoggerToFileBenchmark {
@State(Scope.Benchmark)
public static class BenchmarkState {
- @Param({"ENQUEUE", "SYNCHRONOUS"})
+ @Param({"ENQUEUE", "ENQUEUE_UNSYNCHRONIZED", "SYNCHRONOUS"})
private QueueFullPolicy queueFullPolicy;
+ @Param({"ASYNC_CONTEXT", "ASYNC_CONFIG"})
+ private AsyncLoggerType asyncLoggerType;
+
private Logger logger;
@Setup
public final void before() {
- new File("target/testRandomlog4j2.log").delete();
- System.setProperty("log4j.configurationFile", "ConcurrentAsyncLoggerToFileBenchmark.xml");
- System.setProperty("Log4jContextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
+ new File("target/ConcurrentAsyncLoggerToFileBenchmark.log").delete();
System.setProperty("log4j2.is.webapp", "false");
+ asyncLoggerType.setProperties();
queueFullPolicy.setProperties();
logger = LogManager.getLogger(ConcurrentAsyncLoggerToFileBenchmark.class);
}
@@ -89,23 +94,55 @@ public class ConcurrentAsyncLoggerToFileBenchmark {
@TearDown
public final void after() {
((LifeCycle) LogManager.getContext(false)).stop();
- new File("target/testRandomlog4j2.log").delete();
+ new File("target/ConcurrentAsyncLoggerToFileBenchmark.log").delete();
logger = null;
}
@SuppressWarnings("unused") // Used by JMH
public enum QueueFullPolicy {
- ENQUEUE("Default"),
- SYNCHRONOUS(SynchronousAsyncQueueFullPolicy.class.getName());
+ ENQUEUE(Collections.singletonMap("log4j2.AsyncQueueFullPolicy", "Default")),
+ ENQUEUE_UNSYNCHRONIZED(new HashMap<>() {{
+ put("log4j2.AsyncQueueFullPolicy", "Default");
+ put("AsyncLogger.SynchronizeEnqueueWhenQueueFull", "false");
+ put("AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull", "false");
+ }
+ }),
+ SYNCHRONOUS(Collections.singletonMap("log4j2.AsyncQueueFullPolicy",
+ SynchronousAsyncQueueFullPolicy.class.getName()));
- private final String queueFullPolicy;
+ private final Map<String, String> properties;
+
+ QueueFullPolicy(Map<String, String> properties) {
+ this.properties = properties;
+ }
- QueueFullPolicy(String queueFullPolicy) {
- this.queueFullPolicy = queueFullPolicy;
+ void setProperties() {
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ System.setProperty(entry.getKey(), entry.getValue());
+ }
}
+ }
+
+ @SuppressWarnings("unused") // Used by JMH
+ public enum AsyncLoggerType {
+ ASYNC_CONTEXT,
+ ASYNC_CONFIG;
+ // TODO(ckozak): Consider adding ASYNC_APPENDER
void setProperties() {
- System.setProperty("log4j2.AsyncQueueFullPolicy", queueFullPolicy);
+ switch (this) {
+ case ASYNC_CONTEXT:
+ System.setProperty("log4j.configurationFile", "ConcurrentAsyncLoggerToFileBenchmark.xml");
+ System.setProperty("Log4jContextSelector",
+ "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
+ break;
+ case ASYNC_CONFIG:
+ System.setProperty("log4j.configurationFile",
+ "ConcurrentAsyncLoggerToFileBenchmark-asyncConfig.xml");
+ break;
+ default:
+ throw new IllegalStateException("Unknown type: " + this);
+ }
}
}
}
diff --git a/log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark.xml b/log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark-asyncConfig.xml
similarity index 84%
copy from log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark.xml
copy to log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark-asyncConfig.xml
index f14b3b3..36e309e 100644
--- a/log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark.xml
+++ b/log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark-asyncConfig.xml
@@ -17,13 +17,13 @@
-->
<Configuration status="OFF">
<Appenders>
- <RandomAccessFile name="RandomAccessFile" fileName="target/testRandomlog4j2.log" immediateFlush="false">
+ <RandomAccessFile name="RandomAccessFile" fileName="target/ConcurrentAsyncLoggerToFileBenchmark.log" immediateFlush="false">
<PatternLayout pattern="%d %p [%t] %c{1} %X{transactionId} - %m%n"/>
</RandomAccessFile>
</Appenders>
<Loggers>
- <Root level="info" includeLocation="false">
+ <AsyncRoot level="info" includeLocation="false">
<appender-ref ref="RandomAccessFile"/>
- </Root>
+ </AsyncRoot>
</Loggers>
</Configuration>
diff --git a/log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark.xml b/log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark.xml
index f14b3b3..3b0e043 100644
--- a/log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark.xml
+++ b/log4j-perf/src/main/resources/ConcurrentAsyncLoggerToFileBenchmark.xml
@@ -17,7 +17,7 @@
-->
<Configuration status="OFF">
<Appenders>
- <RandomAccessFile name="RandomAccessFile" fileName="target/testRandomlog4j2.log" immediateFlush="false">
+ <RandomAccessFile name="RandomAccessFile" fileName="target/ConcurrentAsyncLoggerToFileBenchmark.log" immediateFlush="false">
<PatternLayout pattern="%d %p [%t] %c{1} %X{transactionId} - %m%n"/>
</RandomAccessFile>
</Appenders>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index a932d67..2ef304c 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -103,6 +103,9 @@
Fix a race allowing events not to be recorded when a RoutingAppender purge policy attempts to delete an idle
appender at exactly the same time as a new event is recorded.
</action>
+ <action issue="LOG4J2-2606" dev="ckozak" type="fix">
+ Asynchronous logging when the queue is full no longer results in heavy CPU utilization and low throughput.
+ </action>
</release>
<release version="2.11.2" date="2019-02-04" description="GA Release 2.11.2">
<action issue="LOG4J2-2500" dev="rgoers" type="fix">