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">